Introducción

Tengo una VM en Oracle Cloud con varios servicios en Docker (el blog, Nginx Proxy Manager, Umami y WireGuard) y un Intel NUC en casa que hace de servidor doméstico. Hasta ahora, saber cuánta CPU, memoria o disco usaba cada máquina significaba entrar por SSH y ejecutar docker stats o htop. Funciona, pero no deja histórico ni avisa de nada.

Quería algo sencillo: un solo panel con las dos máquinas, el estado de los contenedores y un histórico. La opción clásica es Prometheus + Grafana, pero para dos máquinas me pareció demasiado montaje. Así llegué a Beszel.

En este artículo explico qué es Beszel, cómo se compara con Prometheus + Grafana, y los pasos exactos que seguí para instalar el servidor (Hub) en Oracle Cloud y los agentes en la VM y en el NUC. También cuento lo que me tocó ajustar en el camino.

Conceptos básicos

Si es tu primera vez con monitoreo, estos tres conceptos te ayudarán a seguir el resto:

  • Servidor y agente. En muchas herramientas de monitoreo hay un servidor central que guarda los datos y muestra los paneles, y un programa pequeño (agente) instalado en cada máquina que mides. El agente recoge las métricas y las envía al servidor. En Beszel, el servidor se llama Hub.
  • Proxy inverso. Es el programa que recibe las visitas a tu dominio (por ejemplo beszel.tudominio.com) y las reenvía al servicio correcto dentro de tu servidor. Yo uso Nginx Proxy Manager.
  • WebSocket. Es una conexión que se mantiene abierta entre dos programas para intercambiar datos continuamente. Los agentes de Beszel la usan para enviar sus métricas al Hub.

Qué es Beszel

Según su documentación, Beszel es una plataforma ligera de monitoreo de servidores que incluye estadísticas de Docker, datos históricos y alertas. Tiene interfaz web, configuración sencilla, múltiples usuarios, autenticación OAuth, API y respaldos automáticos.

Se compone de dos piezas:

  • Hub: una aplicación web construida sobre PocketBase. Es el panel donde ves los sistemas conectados, el histórico y las alertas.
  • Agent: un programa que corre en cada máquina que quieres monitorear y envía las métricas al Hub.

Entre las métricas que soporta están CPU, memoria, disco, red, carga media, temperatura, estado y consumo de cada contenedor Docker o Podman, datos S.M.A.R.T. de los discos, y monitores de red (ICMP, TCP, HTTP y DNS).

Cómo se conectan el Hub y el Agent

Hay dos modos, y entender la diferencia evita muchos dolores de cabeza:

ModoQuién inicia la conexiónQué necesita
WebSocketEl Agent se conecta al HubVariable HUB_URL en el Agent. No hace falta abrir puertos en la máquina del Agent
SSHEl Hub se conecta al AgentEl Agent escucha en el puerto 45876, que debe ser alcanzable desde el Hub

Para mi NUC, que está detrás del router de mi proveedor de internet sin poder abrir puertos fácilmente, el modo WebSocket es justo lo que necesitaba.

Beszel frente a Prometheus + Grafana

Prometheus es un conjunto de herramientas de monitoreo y alertas de código abierto. Su servidor recolecta y almacena métricas como series de tiempo, y las obtiene consultando (pull) por HTTP a los objetivos que le configuras. Su ecosistema tiene varios componentes, casi todos opcionales: exportadores, un gestor de alertas (Alertmanager) y otros. Para visualizar los datos se usa habitualmente Grafana, que permite consultar, visualizar y generar alertas sobre métricas, logs y trazas, vengan de donde vengan.

Son herramientas muy potentes, pero resuelven un problema más amplio que el mío.

AspectoBeszelPrometheus + Grafana
Piezas a instalarHub + un Agent por máquinaServidor Prometheus, exportadores para el sistema y los contenedores (la documentación de Prometheus tiene guías para Node Exporter y cAdvisor), Grafana y, opcionalmente, Alertmanager
Métricas de DockerIncluidas en el AgentRequieren un exportador adicional
PanelesListos al instalarLos construyes o importas desde la biblioteca de la comunidad
RecolecciónEl Agent envía al Hub (o el Hub consulta al Agent por SSH)Prometheus consulta a los objetivos
AlertasConfigurables desde la interfaz, con decenas de servicios de notificaciónReglas en Prometheus con Alertmanager, o alertas de Grafana
Consultas y flexibilidadLimitada a las métricas que Beszel soportaPromQL y un modelo de datos multidimensional, pensado para sistemas complejos
Logs y trazasNo es su objetivoGrafana puede explorarlos si añades las fuentes de datos adecuadas
Curva de aprendizajeBajaMás piezas que aprender y mantener

El propio proyecto Beszel afirma ser más pequeño y consumir menos recursos que las soluciones líderes. No hice una comparación de consumo, así que no pondré cifras ajenas. Lo que sí puedo mostrar es lo que consume en mi caso, según su propio panel: el Hub usa alrededor de 17 MB de memoria y los Agents entre 13 y 15 MB (medido en un momento puntual, no es un promedio).

Cuándo conviene cada uno

  • Beszel encaja si tienes pocas máquinas, quieres ver rápido CPU, memoria, disco y contenedores, y prefieres instalar poco y mantener menos.
  • Prometheus + Grafana encaja si necesitas métricas propias de tus aplicaciones, consultas avanzadas, muchas fuentes de datos o un ecosistema de observabilidad completo con logs y trazas. La documentación de Prometheus destaca su fortaleza en arquitecturas dinámicas orientadas a servicios.

No son excluyentes: puedes empezar con Beszel y crecer después si lo necesitas.

Arquitectura

Este es el diseño final:

                     INTERNET
                        │
                   ┌────▼────┐
                   │Cloudflare│  (DNS + proxy, HTTPS)
                   └────┬────┘
                        │
        ┌───────────────▼────────────────┐
        │        VM en Oracle Cloud       │
        │                                 │
        │   Nginx Proxy Manager           │
        │   (SSL + Websockets)            │
        │            │ 172.17.0.1:8090    │
        │            ▼                    │
        │      Beszel Hub  ◄─ WebSocket ─ Agent local
        │                  (172.17.0.1)   │
        │                  Docker socket  │
        └────────────▲────────────────────┘
                     │ WebSocket saliente (HTTPS)
                     │
             ┌───────┴────────┐
             │   Intel NUC    │
             │  Agent + Docker│
             └────────────────┘

Decisiones de diseño:

  • El Hub corre en la VM de Oracle y se publica con Cloudflare y Nginx Proxy Manager, igual que mis otros servicios.
  • El Agent de la VM se conecta al Hub por WebSocket a través de 172.17.0.1:8090, y deja el socket Unix como respaldo (modo SSH). Así no se abre el puerto 45876.
  • El Agent del NUC se conecta de forma saliente a la URL pública del Hub. No abro puertos en casa ni uso VPN para esto.
  • No hay Prometheus, Grafana ni base de datos externa.

Requisitos previos

  • Una VM con Docker y Docker Compose. En mi caso, Oracle Linux en ARM (aarch64).
  • Nginx Proxy Manager funcionando y publicando servicios con SSL.
  • Un dominio en Cloudflare y un subdominio libre para el Hub (yo usé beszel).
  • Una segunda máquina con Docker para el Agent remoto (mi NUC con EndeavourOS).

Instalación del Hub en Oracle Cloud

1. Verificar el entorno

Antes de instalar, comprobé la arquitectura, la gateway de la red Docker por defecto, que el puerto 8090 estuviera libre y el estado de SELinux:

uname -m                                        # aarch64
docker network inspect bridge | grep Gateway    # "Gateway": "172.17.0.1"
ss -tlnp | grep 8090                            # sin salida: el puerto está libre
getenforce                                      # Enforcing

Dos datos de aquí condicionan el resto: la gateway 172.17.0.1 (por donde Nginx Proxy Manager alcanzará el Hub) y SELinux en modo Enforcing.

2. Crear la carpeta y el docker-compose.yml

Entra en la carpeta antes de crear el archivo, para no dejarlo en el directorio equivocado:

mkdir -p ~/Proyectos/beszel
cd ~/Proyectos/beszel
vim docker-compose.yml

Primero levanté solo el Hub, porque todavía no tengo la KEY ni el TOKEN del Agent:

services:
  beszel:
    image: henrygd/beszel:latest
    container_name: beszel
    restart: unless-stopped
    environment:
      APP_URL: https://beszel.tudominio.com
    ports:
      - 172.17.0.1:8090:8090
    volumes:
      - ./beszel_data:/beszel_data:z
      - ./beszel_socket:/beszel_socket:z

Qué cambié respecto al ejemplo oficial y por qué:

  • APP_URL apunta a mi dominio público. La documentación recomienda definirla porque se usa en enlaces de notificaciones y en la configuración que genera para los agentes.
  • 172.17.0.1:8090:8090 publica el puerto solo en la gateway de Docker. Nginx Proxy Manager puede llegar ahí, pero el puerto no queda expuesto en la IP pública de la VM. El ejemplo oficial usa 8090:8090, que lo publica en todas las interfaces.
  • :z en los volúmenes le pide a Docker que etiquete las carpetas para SELinux. Lo agregué de forma preventiva por tener SELinux en Enforcing; no probé sin él, así que no sé si era imprescindible.
docker compose up -d
docker logs beszel --tail 20
curl -I http://172.17.0.1:8090     # debe responder 200

3. DNS en Cloudflare

Creé un registro A para el subdominio apuntando a la IP de la VM, con el proxy de Cloudflare activado (nube naranja), igual que el resto de mis subdominios:

TipoNombreContenidoProxy
Abeszel<IP_PÚBLICA_DE_LA_VM>Proxied

4. Proxy Host en Nginx Proxy Manager

En NPM creé un Proxy Host con estos valores:

  • Domain Names: beszel.tudominio.com
  • Scheme: http
  • Forward Hostname / IP: 172.17.0.1
  • Forward Port: 8090
  • Block Common Exploits: activado
  • Websockets Support: activado

El Websockets Support es obligatorio: la documentación indica que el proxy debe reenviar conexiones WebSocket para que los agentes puedan conectarse. Sin eso, el Agent del NUC no funcionaría.

En la pestaña SSL solicité un certificado nuevo de Let’s Encrypt y activé Force SSL.

5. Crear el usuario administrador

Al abrir https://beszel.tudominio.com por primera vez, Beszel pide crear la cuenta de administrador. Después de crearla ya puedes entrar al panel.

Agent local (VM de Oracle)

Quiero monitorear también la propia VM, así que necesita su Agent. Como el Hub y el Agent están en la misma máquina, la documentación recomienda conectarlos con un socket Unix, porque localhost no funciona entre contenedores en redes distintas.

1. Obtener la KEY y el TOKEN

En el panel, pulsa Agregar sistema (Add System) y rellena:

  • Nombre: el que quieras (yo puse bta-1).
  • Host / IP: /beszel_socket/beszel.sock

El diálogo muestra la KEY y el TOKEN. Cópialos y no pulses todavía el botón de agregar.

2. Agregar el Agent al compose

Añadí este servicio debajo del Hub, en el mismo archivo:

  beszel-agent:
    image: henrygd/beszel-agent:latest
    container_name: beszel-agent
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./beszel_agent_data:/var/lib/beszel-agent:z
      - ./beszel_socket:/beszel_socket:z
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      LISTEN: /beszel_socket/beszel.sock
      HUB_URL: http://172.17.0.1:8090
      TOKEN: "<TOKEN>"
      KEY: "<KEY>"

Notas:

  • network_mode: host es necesario para que el Agent lea las estadísticas de red de la máquina.
  • LISTEN apunta al socket Unix compartido con el Hub. Es el canal de respaldo (modo SSH): el Agent solo lo usa si falla la conexión WebSocket.
  • HUB_URL: el ejemplo oficial usa http://localhost:8090. Como mi puerto está publicado en 172.17.0.1, lo cambié a esa dirección.
  • El montaje del socket de Docker (docker.sock) le permite al Agent leer el estado de los contenedores. Más abajo explico por qué conviene tratarlo con cuidado.
  • KEY y TOKEN aparecen aquí como marcadores. La KEY es la clave pública del Hub; el TOKEN, en cambio, autentica al Agent, así que trátalo como un secreto: no lo subas a un repositorio ni lo muestres en capturas. Si prefieres no dejarlo en el compose, Beszel acepta TOKEN_FILE y KEY_FILE para leer ambos valores desde archivos protegidos.
docker compose up -d
docker logs beszel-agent --tail 30

3. Confirmar

Vuelve al diálogo y pulsa Agregar sistema. Tras unos segundos, el sistema debe pasar a verde y mostrar los contenedores de la VM.

En los logs del Agent se ve la secuencia completa: el WebSocket devuelve 401 mientras el sistema no está agregado en el Hub, el Agent levanta el servidor SSH en el socket como respaldo y, al pulsar Agregar sistema, conecta por WebSocket y lo detiene:

WARN WebSocket connection failed err="unexpected status code: 401"
INFO Starting SSH server addr=/beszel_socket/beszel.sock network=unix
WARN WebSocket connection failed err="unexpected status code: 401"
INFO WebSocket connected host=172.17.0.1:8090
INFO Stopping SSH server

Agent en el Intel NUC

El NUC se conecta de forma saliente al Hub, así que no hace falta abrir ningún puerto en casa.

Este Agent no usa el Cloudflare Tunnel del NUC. El túnel sirve para el tráfico que entra hacia mis servicios en casa, y aquí la conexión va en sentido contrario: el Agent sale hacia el Hub por HTTPS.

1. Generar la configuración desde el Hub

En el panel, pulsa Agregar sistema otra vez. Beszel genera un docker-compose.yml ya preparado con la KEY, el TOKEN y la HUB_URL de tu Hub. Es la forma más cómoda de no equivocarse.

2. Crear el compose en el NUC

mkdir -p ~/Proyectos/beszel-agent
cd ~/Proyectos/beszel-agent
vim docker-compose.yml

Con :set paste activado en vim para no romper la indentación del YAML. Este es el contenido que generó Beszel:

services:
  beszel-agent:
    image: henrygd/beszel-agent
    container_name: beszel-agent
    restart: unless-stopped
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./beszel_agent_data:/var/lib/beszel-agent
      # monitor other disks / partitions by mounting a folder in /extra-filesystems
      # - /mnt/disk/.beszel:/extra-filesystems/sda1:ro
    environment:
      LISTEN: 45876
      KEY: "<KEY>"
      TOKEN: "<TOKEN>"
      HUB_URL: https://beszel.tudominio.com

La clave está en HUB_URL: apunta a la URL pública del Hub, por lo que el Agent abre una conexión WebSocket saliente atravesando Cloudflare y Nginx Proxy Manager.

3. Levantarlo y comprobar

docker compose up -d
docker logs beszel-agent --tail 50

Los logs mostraron lo que buscaba: el Agent detectó el disco y la interfaz de red, y la línea clave:

INFO WebSocket connected host=beszel.tudominio.com

Con eso, el NUC ya reportaba al Hub sin abrir puertos.

Resultado

El panel muestra las dos máquinas con CPU, memoria, disco, carga media, red y, en el NUC, la temperatura (la VM de Oracle no expone sensores):

Panel de Beszel con los dos sistemas

Y la vista de contenedores reúne los de ambos sistemas en una sola tabla:

Contenedores de ambos sistemas en Beszel

Desde el panel puedes entrar a cada sistema y cada contenedor para ver las gráficas históricas.

Seguridad

Montar el socket de Docker merece una explicación aparte.

:ro en el socket de Docker no es aislamiento

La configuración oficial de Beszel monta el socket de Docker con :ro (solo lectura), y es fácil pensar que eso hace el acceso inofensivo. No es así. La opción :ro solo afecta al archivo del socket en el sistema de archivos del contenedor. Un proceso que pueda conectarse al socket sigue pudiendo enviar peticiones a la API de Docker, incluidas las que crean o modifican contenedores, porque la API no distingue entre peticiones de lectura y de escritura según cómo se montó el socket.

En la práctica, quien controla el Agent tiene un acceso muy amplio al host. Mantengo el montaje porque es el que usa la configuración oficial y lo necesito para ver los contenedores, pero lo trato como un riesgo conocido. Si tu modelo de amenaza lo exige, existen proxies de socket que filtran qué llamadas a la API se permiten. Evaluar uno es parte de mis próximos pasos.

Que el Hub no quede expuesto

Dos decisiones reducen la superficie expuesta:

  • El puerto 8090 del Hub está publicado solo en 172.17.0.1, no en la IP pública.
  • El Agent local escucha en un socket Unix y no en TCP, así que 45876 no se abre en la VM.

Comprobaciones que conviene hacer en la VM y desde fuera:

# En la VM: 8090 debe escuchar solo en 172.17.0.1 y 45876 no debe aparecer
ss -tlnp | grep -E '8090|45876'

# Desde otra red (por ejemplo, tu equipo): debe fallar o agotar el tiempo
nc -zv -w 5 <IP_PÚBLICA_DE_LA_VM> 8090

Revisa también las reglas de red de tu proveedor (en Oracle Cloud, la Security List o los Network Security Groups) para confirmar que 8090 y 45876 no están abiertos al exterior.

Aprendizajes

Estos fueron los puntos en los que tuve que ajustar algo o que no eran evidentes:

  1. Crear el archivo en la carpeta correcta. Hice mkdir pero no cd, y vim docker-compose.yml creó el archivo en el directorio padre. Parece trivial, pero es un clásico.
  2. Orden de instalación. Hay que levantar primero el Hub, agregar el sistema en la interfaz para obtener la KEY y el TOKEN, y solo entonces arrancar el Agent.
  3. 172.17.0.1 en lugar de localhost. Mi Nginx Proxy Manager llega a los servicios a través de la gateway de Docker. Por eso publiqué el puerto del Hub en esa dirección y no en 127.0.0.1, que NPM no vería desde su contenedor.
  4. Websockets Support en NPM. Sin esa opción, los agentes con HUB_URL no conectan.
  5. APP_URL con tu dominio. No dejes http://localhost:8090 en una instalación publicada.
  6. SELinux en Enforcing. Agregué :z a los volúmenes del Hub y del Agent local por precaución. No lo probé sin esa opción, así que no puedo asegurar que fuera necesaria. Si tu Agent da permission denied sobre el socket de Docker, revisa getenforce y ls -Z /var/run/docker.sock antes de tocar políticas.
  7. El puerto 45876 no siempre importa. Con HUB_URL (WebSocket), no necesitas abrirlo en la máquina remota. Solo es relevante en el modo SSH, donde el Hub inicia la conexión.

Próximos pasos

Esto lo dejo para un próximo artículo:

  • Alertas y notificaciones: umbrales de CPU, memoria, disco y estado de los sistemas, con un canal de notificación.
  • Monitorear el RAID del NUC: montar una carpeta del volumen dentro de /extra-filesystems para ver su espacio.
  • Respaldo del Hub: usar la función de backups integrada o incluir beszel_data en mi rutina de respaldo.
  • Endurecer el acceso al socket de Docker: evaluar un proxy de socket que limite las llamadas permitidas. La propia documentación de Beszel lo sugiere mediante la variable DOCKER_HOST.
  • Proteger el acceso al panel: hoy el login del Hub es público. Opciones: MFA por correo (MFA_OTP, requiere SMTP), OAuth, o una regla de WAF / Cloudflare Access que deje libre /api/beszel/agent-connect para que el NUC siga conectando.
  • Revisar la vista de detalles de contenedores: el panel permite ver el inspect y los logs de cada contenedor (variable CONTAINER_DETAILS, activa por defecto). Quiero comprobar qué expone y decidir si la desactivo.

Conclusión

Para un laboratorio pequeño, Beszel resuelve lo que yo necesitaba con muy pocas piezas: un Hub, un Agent por máquina y un panel con histórico. La instalación fue directa y lo único que requirió atención fue la integración con mi proxy inverso y la conexión del NUC.

Prometheus + Grafana siguen siendo la opción cuando necesitas métricas propias de aplicaciones, consultas avanzadas o logs y trazas. Pero si lo que quieres es ver cómo están tus servidores y contenedores sin construir una plataforma de observabilidad, Beszel es un buen punto de partida.

Referencias