Beszel¶
Beszel is a lightweight server monitor: CPU, memory, network, disk usage and I/O, temperatures, per-container stats and S.M.A.R.T. per disk, with history and threshold alerts. A hub (web UI + database) collects from an agent on every server.
| Component | Runs on |
|---|---|
beszel-hub |
control-host, in the monitoring stack next to Uptime Kuma and Dozzle |
beszel-agent |
every server, including control-host itself |
Alerts (e.g. disk usage, CPU, temperature thresholds) go to ntfy alerts as the
write-only beszel user. Configure that under the hub's notification settings with an
ntfy URL that carries the token.
Hub¶
services:
beszel-hub:
image: henrygd/beszel:0.21.0
container_name: beszel-hub
restart: unless-stopped
ports:
- "8090:8090"
volumes:
- /mnt/appdata/beszel/hub-data:/beszel_data
# The Beszel binary ships a health subcommand built for this
healthcheck:
test: ["CMD", "/beszel", "health", "--url", "http://localhost:8090"]
interval: 120s
start_period: 10s
timeout: 5s
The hub sits behind the reverse proxy at https://beszel.<YOUR_LOCAL_DOMAIN>. Add a
system in the UI and it shows you the KEY and TOKEN for that agent.
Pre-1.0 software
Beszel is still 0.x, where a minor bump (0.20 → 0.21) can be breaking. Hub and agents
are pinned to the same version and updated together in one grouped update PR, which I
review instead of automerging.
Agent¶
Every agent uses the same shape: pinned tag, unless-stopped, the /agent health
healthcheck, disks passed by serial for SMART, and the real data mounts as extra
filesystems. This is the NAS version (most disks); the other servers only pass their
root NVMe and their appdata SSD.
services:
beszel-agent:
image: henrygd/beszel-agent:0.21.0
container_name: beszel-agent
restart: unless-stopped
network_mode: host
# SMART: every disk passed by serial, so a reordered /dev/sdX can't swap them.
devices:
- /dev/nvme0:/dev/nvme0 # root NVMe (controller device)
- /dev/disk/by-id/ata-<MODEL>_<SERIAL_D1>:/dev/sda # pool data d1
- /dev/disk/by-id/ata-<MODEL>_<SERIAL_D2>:/dev/sdb # pool data d2
- /dev/disk/by-id/ata-<MODEL>_<SERIAL_SSD>:/dev/sdc # appdata SSD
- /dev/disk/by-id/ata-<MODEL>_<SERIAL_NVR>:/dev/sdd # NVR recordings
- /dev/disk/by-id/ata-<MODEL>_<SERIAL_P>:/dev/sde # SnapRAID parity
cap_add:
- SYS_RAWIO # required for S.M.A.R.T. data
- SYS_ADMIN # required for NVMe S.M.A.R.T. data
volumes:
- ./beszel_agent_data:/var/lib/beszel-agent
- /var/run/docker.sock:/var/run/docker.sock:ro
# Extra disk panels: real mountpoints, name = <diskstats partition>__<label>.
# Root is the agent's primary filesystem already.
- /mnt/storage/:/extra-filesystems/storage__Pool:ro
- /mnt/appdata/:/extra-filesystems/sdc1__Appdata:ro
- /mnt/hdd/<SERIAL_D1>:/extra-filesystems/sda1__Data 1:ro
- /mnt/hdd/<SERIAL_D2>:/extra-filesystems/sdb1__Data 2:ro
- /mnt/hdd/<SERIAL_P>:/extra-filesystems/sde1__Parity:ro
- /mnt/nvr:/extra-filesystems/sdd1__NVR:ro
environment:
LISTEN: 45876
KEY: ${BESZEL_KEY}
HUB_URL: https://beszel.<YOUR_LOCAL_DOMAIN>
TOKEN: ${BESZEL_TOKEN}
CONTAINER_DETAILS: true
healthcheck:
test: ['CMD', '/agent', 'health']
interval: 120s
start_period: 10s
timeout: 5s
# Both shown by the hub when you add the system
BESZEL_KEY=ssh-ed25519 AAAA...
BESZEL_TOKEN=<token from the hub>
LISTEN is the current name of the agent port variable; PORT is the deprecated
older name and still works. (Two of my agents still use PORT.) CONTAINER_DETAILS is
documented as a hub setting (it controls whether container inspect/logs are viewable
in the UI), so on an agent it has no documented effect.
Why it's built like this¶
- Disks by serial (
/dev/disk/by-id/...), not/dev/sdX. Kernel device letters can change between boots. With/dev/sdb:/dev/sdba reorder silently swaps which physical disk the "parity" panel is showing. The by-id path maps a specific serial to a fixed name inside the container. When I rebuilt this mapping, the old one had two data disks labelled the wrong way round and had left out the parity disk entirely. Check labels againstlsblk -o NAME,SERIAL,MOUNTPOINTand yoursnapraid.conf. - NVMe: pass the controller (
/dev/nvme0), not the namespace (/dev/nvme0n1), and addSYS_ADMIN. SATA SMART needsSYS_RAWIO. - Extra filesystems must be the real mountpoints. Mounting an empty directory that
happens to live on the root filesystem gives you a second panel showing the root disk.
That happened on two hosts, where
/mnt/sdawas just a leftover empty dir. The folder name<device>__<label>tells Beszel which diskstats device to use for I/O, and the label is what the UI shows. /agent health. The agent image's binary is/agent. A healthcheck copied from the hub (/beszel health) points at a file that doesn't exist, and that agent sat "unhealthy" for weeks without anyone noticing.network_mode: hostso the agent sees the host's real network interfaces and the hub can reach it on45876.
WebSocket vs SSH
With HUB_URL + TOKEN the agent connects out to the hub over WebSocket. If it can't
(for example the hub uses a private CA the agent image doesn't trust), the hub falls
back to connecting in over SSH on port 45876, which works fine. To use the WebSocket
path with a private CA, mount the CA bundle into the agent and point SSL_CERT_FILE
at it.
SMART vs smartd¶
Beszel shows SMART attributes and history. It is not what runs self-tests or pages
me about a failing disk. smartd on each host does that and sends to ntfy, see
Host maintenance.
Beszel is for "show me the trend", smartd for "wake me up".