All posts

Beszel on a VPS: hub, agent, and the graph

Beszel 0.21.1: hub on 127.0.0.1, agent with KEY+TOKEN, one green system and CPU graph, Caddy. Idle 19.8 MiB hub+agent.

Written by NodeRings Inc.

NodeRings Inc. writes these notes. The company runs the marketplace and the agent that stays on the provider’s side of the hypervisor.

Beszel 0.21.1 held 19.8 MiB idle on this lab host after one green system (vps) and a quiet UI with CPU, memory, and disk graphs already drawing. That sum is cgroup memory.current for the hub and the agent, each the median of twenty samples taken 30 seconds apart. The hub was 11.7 MiB and the agent 8.1 MiB. docker stats printed 11.3 MiB and 8.1 MiB (19.4 MiB together). CPU percent sat at 0. Port 8091 listened on 127.0.0.1 only. A curl to the guest's LAN address on 8091 was refused.

Beszel is a lightweight server monitoring hub plus per-host agents. The hub stores history and draws CPU, memory, disk, and network graphs; agents ship metrics with a token/key pair. It aims at the gap between “SSH and htop” and a full Prometheus stack: one UI, small binaries, Docker-friendly install. A single VPS can run hub and agent together; more machines get an agent each pointed at the hub URL.

The getting started guide ships a Compose file that publishes 8090:8090 and pairs a local agent over a unix socket. Competing guides leave that publish on every interface and stop once the first-user form loads. Pin 0.21.1 by digest, bind the hub to loopback, create the admin, and put KEY and TOKEN on the agent. Wait for a green row and a CPU chart with points. Put Caddy on 443.

Readinghubagentsum
cgroup memory.current11.7 MiB8.1 MiB19.8 MiB
docker stats11.3 MiB8.1 MiB19.4 MiB
CPU percent000
Host listener127.0.0.1:8091unpublished (unix socket)-
Skip thisWhat this run did
8090:8090 on every interfacePublished 127.0.0.1:8091:8090
Agent without KEY or TOKENCopied both from Add System / fingerprint
HUB_URL=http://localhost:8090 from a second Compose networkShared beszel_socket + HUB_URL=http://127.0.0.1:8091
Stopping at a grey systemOpened system detail until the CPU chart had samples
Opening 8091 on the panel firewallLeft 8091 closed; Caddy on 80/443
Bar chart. Idle cgroup memory for Beszel hub 11.7 MiB, agent 8.1 MiB, sum 19.8 MiB.
Idle after one green system and a filled CPU graph. More agents and longer retention sit above this.

How do you install Beszel hub and agent on Docker?

This lab pinned henrygd/beszel:0.21.1 (digest sha256:8514a69d9bbbcb74f371a3205561ee231fc0289739cea64e8131f341d695b770) and henrygd/beszel-agent:0.21.1 (digest sha256:cb6868012964d7a68debaa2c84bb72285e5f08531e81f506bd4c493bf052e41c). Host port 8090 was already taken, so the hub published 8091 on loopback. If 8090 is free on your VPS, map 127.0.0.1:8090:8090 instead and keep APP_URL in sync.

mkdir -p ~/beszel/{beszel_data,beszel_socket,beszel_agent_data} && cd ~/beszel

Start with the hub only. Fill KEY and TOKEN after you create the system in the UI.

services:
  beszel:
    image: henrygd/beszel:0.21.1@sha256:8514a69d9bbbcb74f371a3205561ee231fc0289739cea64e8131f341d695b770
    container_name: beszel
    restart: unless-stopped
    environment:
      APP_URL: http://127.0.0.1:8091
    ports:
      - "127.0.0.1:8091:8090"
    volumes:
      - ./beszel_data:/beszel_data
      - ./beszel_socket:/beszel_socket
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  beszel-agent:
    image: henrygd/beszel-agent:0.21.1@sha256:cb6868012964d7a68debaa2c84bb72285e5f08531e81f506bd4c493bf052e41c
    container_name: beszel-agent
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./beszel_agent_data:/var/lib/beszel-agent
      - ./beszel_socket:/beszel_socket
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      LISTEN: /beszel_socket/beszel.sock
      HUB_URL: http://127.0.0.1:8091
      TOKEN: "<token>"
      KEY: "<public-key>"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

LISTEN on the unix socket matches the official same-host recipe. With host networking, the agent dials the hub at HUB_URL on the host loopback. Mounting docker.sock fills the Docker CPU and memory cards on the system page. Log rotation matches the Compose checklist.

docker compose up -d beszel
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8091/

You want 200. The first visit opens the create-account form.

Compose for Beszel 0.21.1 hub on 127.0.0.1:8091:8090 and agent with socket LISTEN, KEY, TOKEN.
Hub on loopback. Agent on host network with a shared socket volume.

How do you add systems and read the graphs?

From the hub UI, Add System, copy the install env (KEY, TOKEN, HUB_URL) into the agent compose or binary service on that host. HUB_URL is the base origin only (https://beszel.example.com); the agent appends /api/beszel/agent-connect itself, so a path suffix returns 404 (Beszel discussion #1774, agent install). Same-host agents should use the shared unix socket (LISTEN=/beszel_socket/beszel.sock) because localhost between containers is the wrong network namespace. Remote agents need host networking for NIC stats, docker.sock:ro for container metrics, and a token that has not leaked into a public gist. Private CA hubs set CA_CERT_FILE (env docs).

Alerts and retention depend on the Beszel version you pin; set notification channels once a graph is green. Hub data lives on its volume: back that up if historical graphs matter. Agents are disposable; the hub is the source of truth. For log diving use Dozzle; Beszel is metrics, not stdout.

Why must 8091 stay on loopback?

The hub is a PocketBase app with your metrics history and the first-user form. Anyone who can reach it can try that form before you finish setup.

ss -H -lntup | grep 8091

On this host that printed 127.0.0.1:8091. Curl to 192.168.1.150:8091 failed with connection refused. Leave 8091 closed in the panel firewall. Open 80 and 443 for Caddy. Docker published ports often bypass UFW inside the guest, so the loopback bind is what kept the LAN address quiet.

ss showing LISTEN on 127.0.0.1:8091 only, and curl to the LAN address refused.
Host view of the hub publish. Inside the container the process still binds 8090.

How do you create the admin and add a system?

Open http://127.0.0.1:8091/ (or an SSH tunnel to that port). Create the account with an email and a long password. Crop the password from screenshots. Both a regular hub user and a PocketBase superuser are created on that first run; password resets for /_/ use the superuser CLI, which is separate from the hub login.

Click Add System. For this same-host layout:

  1. Set Host / IP to /beszel_socket/beszel.sock.
  2. Copy the public KEY and the TOKEN into the agent environment.
  3. Save, then bring the agent up:
docker compose up -d

The agent log should show a WebSocket connect to the hub. The systems table flips the row to green (up) and starts filling CPU, memory, disk, and load. Open the system page and wait until the CPU chart has points.

A universal token under Settings → Tokens works the same way for several agents. Remote agents keep LISTEN=45876 (or another port) and put the VPS address in Host / IP instead of the socket path. On that host, open or close 45876 in the firewall.

Beszel All Systems table with vps green, CPU memory disk and load average filled, agent 0.21.1.
One system, green. Agent version matches the pinned tag.
Beszel system page for vps showing Up status, unix socket host, and CPU memory disk graphs.
System detail after the agent connected. Host path is the shared unix socket.

Where does Caddy go?

monitor.example.com {
	reverse_proxy 127.0.0.1:8091
}

Set APP_URL to that same https://monitor.example.com and recreate the hub so links and cookies match the public name. TLS setup is in the Caddy HTTPS post. Beszel's reverse-proxy notes cover forwarded headers when Caddy terminates TLS.

Caddyfile reverse proxy to 127.0.0.1:8091 for monitor.example.com.
Public HTTPS on 443. The hub stays on loopback HTTP.

How do you back up beszel_data?

Hub state lives under ./beszel_data. Stop the hub, archive the directory, start again:

docker stop beszel
tar -C ~/beszel -czf beszel_data.tgz beszel_data
docker start beszel

On this lab the archive was about 28 KB after one admin and one system. Point Restic at that tarball. Agent state under beszel_agent_data is smaller and easier to recreate from KEY and TOKEN.

Stop hub, tar beszel_data into beszel_data.tgz at 28.2K, start again.
Stopped copy of hub PocketBase data.

How do you grow from one agent to many?

Add each VPS as its own system with its own token. Keep the hub on the most stable host. If the hub is down, agents buffer or gap depending on version; treat hub uptime as part of monitoring itself. Disk graphs matter more than CPU for small hosts that fill /. Export or screenshot baselines after a quiet week so a regression is obvious.

FAQ

Why host networking on the agent? Host network is how the agent reads host interface counters. Drop it only if you accept weaker host network stats and map a port yourself.

Socket or port 45876? On the same host, use the shared unix socket as Host / IP. On a remote host, listen on 45876 (or another port) and point the hub at that address. With the socket path you can leave 45876 closed.

Why did localhost fail for HUB_URL? From a container on the Compose bridge, localhost is the container. Use the host loopback with host networking, a shared socket, or the hub's docker-network DNS name.

Beszel vs Uptime Kuma? Kuma watches HTTP checks and status pages (Uptime Kuma post). Beszel graphs the host and its containers. Many stacks run both.

Alerts? Point Beszel notifications at ntfy or another sink from the hub settings. This lab stopped at a green system and a filled graph.

Netdata or Grafana? Netdata and Grafana run heavier agents and scrapers. Beszel is the small hub and agent pair with a built-in UI. This lab's idle number is the comparison point on a quiet VPS.