Skip to content

Roles: beszel_hub + beszel_agent

Lightweight, self-hosted server monitoring — covers CPU, memory, disk, network, temperature, Docker container stats, ZFS ARC, and (with SMART enabled) per-disk health. Single Discord channel for alerts.

Hosts:

  • beszel_hub — dedicated LXC (CT 106, 192.168.1.247)
  • beszel_agent — applied to proxfold (host OS, SMART enabled), arrstack (VM), plex (CT 100), vintage (CT 201), control (CT 104), pbs (CT 105), edge (CT 107), n8n (VM 108), forgejo (CT 109). Pattern: every per-host playbook ends with beszel_agent as a trailing role.

Phase 5B executed — 2026-04-24

Hub live as CT 106, agents deployed and registered. Install delegated to upstream scripts; role keeps a local cached copy at /root/install-beszel-{hub,agent}.sh for auditability and version bumps. Alert rules are UI-owned (not codified in Ansible) per the Phase 5 scope reset decision. Initial fleet was 6 agents; subsequent host additions (edge 5D, n8n 5C, forgejo 6A.1) joined under the same role.

Why Beszel, not Netdata / Prometheus / Grafana

  • ~10 MB agent RAM vs 200-500 MB for Netdata — matters on small LXCs like CT 104
  • SQLite history is sufficient for a 6-host homelab; no time-series DB to operate
  • Built-in alerting with Discord webhook out of the box — no separate Alertmanager
  • No dashboard engineering — designed for "click a host and see it" rather than building panels
  • Grafana/Prometheus can be bolted on later if long retention or cross-host queries become a real requirement (they aren't today)

Architecture

flowchart LR
    subgraph hub["beszel_hub · CT 192.168.1.247"]
        H[hub binary + SQLite]
    end
    subgraph hosts["beszel_agent — one per host"]
        A1[proxfold<br/>host OS<br/>SMART enabled]
        A2[arrstack]
        A3[plex CT100]
        A4[vintage CT201]
        A5[control CT104]
        A6[pbs CT105]
        A7[edge CT107]
        A8[n8n VM108]
        A9[forgejo CT109]
    end
    H -- SSH-signed scrape --> A1 & A2 & A3 & A4 & A5 & A6 & A7 & A8 & A9
    H -- webhook --> D["#homelab-ops Discord"]

Hub connects outbound to each registered agent at <host>:45876. Agent validates the hub's signature against its cached public key (vaulted as vault_beszel_hub_pubkey, baked into the agent's systemd unit as an Environment=KEY=... line by the installer). All metrics land in the hub's SQLite store. Alerts fire from the hub UI, one Discord channel for everything.

Install approach

Both roles delegate install to upstream scripts at get.beszel.dev and get.beszel.dev/hub. The scripts are POSIX-compliant, create the beszel system user (with disk group membership on agents), fetch the latest binary, and register the systemd unit. The role caches the installer at /root/install-beszel-{hub,agent}.sh so the exact version that ran is auditable, and an operator can re-run it manually for a version bump.

Custom systemd hardening, if ever needed, goes via drop-in files under /etc/systemd/system/beszel-{hub,agent}.service.d/, not template rewrites.

beszel_hub — tasks

Task Tag
Install prerequisites (curl, ca-certificates) beszel, install
Stat the cached installer; fetch if missing beszel, install
Stat the hub binary; run installer if missing beszel, install
Ensure beszel-hub service is running beszel, service
First-run reminder (admin bootstrap + pubkey capture) beszel, install

beszel_agent — tasks

Task Tag
Install prerequisites (curl, ca-certificates) beszel, install
Install smartmontools (when beszel_agent_enable_smart: true) beszel, install
Stat the cached installer; fetch if missing beszel, install
Stat the agent binary; run installer with -k <pubkey> -p <port> beszel, install
Ensure systemd drop-in directory (when SMART or extra filesystems are configured) beszel, config
Render SMART access drop-in (when beszel_agent_enable_smart: true) beszel, config
Render extra-filesystems drop-in (when beszel_agent_extra_filesystems is non-empty) beszel, config
Ensure beszel-agent service is running beszel, service

Key variables

Hub

Variable Source Value
beszel_hub_installer_url defaults https://get.beszel.dev/hub
beszel_hub_installer_path defaults /root/install-beszel-hub.sh
beszel_hub_home defaults /opt/beszel
beszel_hub_listen_port defaults 8090 (plain HTTP — TLS terminates at the nginx reverse proxy)
beszel_hub_external_host defaults beszel.rampancy.cloud (reverse-proxy target, not configured by this role)
vault_discord_webhook_homelab_ops vault shared with ZED + PVE notifications

Agent

Variable Source Value
beszel_agent_installer_url defaults https://get.beszel.dev
beszel_agent_installer_path defaults /root/install-beszel-agent.sh
beszel_agent_bin_path defaults /opt/beszel-agent/beszel-agent
beszel_agent_listen_port defaults 45876
beszel_agent_hub_pubkey_var defaults vault_beszel_hub_pubkey
beszel_agent_enable_smart host_vars true on proxfold only
beszel_agent_extra_filesystems host_vars list of additional mount points / devices to surface in the hub UI (default []); proxfold sets ["/stash__stash"]
vault_beszel_hub_pubkey vault hub's public key; derived post-install via ssh-keygen -y -f <priv>

Gotchas captured during execution

  • Installer bakes env vars directly into the systemd unit (Environment=PORT=..., Environment=KEY=...), not into a separate env file. To rotate the hub pubkey, re-run the cached installer with a new -k. The older stub that rendered /etc/default/beszel-agent via template was pointing at a path the installer doesn't use; dropped in the as-executed role.
  • Hub pubkey has no separate .pub file. Beszel only writes the private key to beszel_data/id_ed25519. Derive the public key with ssh-keygen -y -f /opt/beszel/beszel_data/id_ed25519 and stash into vault.
  • Hub listens on plain HTTP by design. TLS terminates at the reverse-proxy layer; https://<ip>:8090 will not respond. Use http://<ip>:8090 for LAN access until nginx is wired up.
  • nesting=1 is required on the hub LXC (same Debian-13-systemd-257 gotcha as PBS) — set at pct create time via --features nesting=1.
  • Agent must be registered in the hub UI (Add System → name/host/port); no auto-discovery. Registration is fast (~5s) but has to be done once per host after the agent role runs.
  • get_url check-mode hostility — the cached-installer fetch task is stat-gated. Without it, daily drift reports changed=1 on this role against every host.

SMART on Proxmox hosts

The upstream installer auto-adds the beszel user to the disk group on all hosts, and the role's beszel_agent_enable_smart flag installs smartmontools (required for actual SMART reads). proxfold is the only host with physical disks worth scraping.

Two extra things are needed beyond the upstream defaults to actually get SMART data on a PVE host with a MegaRAID/PERC controller (discovered 2026-04-25 while trying to surface SMART for proxfold's 8 disks behind the H730):

  1. CAP_SYS_RAWIO — smartctl's -d sat path uses SCSI-passthrough ioctls for ATA SMART. The disk group permits read() but not the ioctls.
  2. DeviceAllow=block-sd rw — the upstream systemd unit sets DeviceAllow= for nvidia devices only, which cgroup-blocks /dev/sd* at the kernel level. Without this, smartctl fails with exit status 2 ("device open failed") even with CAP_SYS_RAWIO and disk-group membership.

The role drops both into /etc/systemd/system/beszel-agent.service.d/smart.conf when beszel_agent_enable_smart: true. Refs: henrygd/beszel#1652, henrygd/beszel#1545.

Surfacing additional filesystems

By default the Beszel agent only reports the root filesystem's capacity. Beszel does not natively monitor ZFS pool health; pool mounts have to be added explicitly alongside any other non-root mounts that matter.

Set beszel_agent_extra_filesystems in host_vars to a list of mount points, device names, or labelled mount points (/path__Label syntax). The role renders them into /etc/systemd/system/beszel-agent.service.d/extra-filesystems.conf as a comma-separated Environment=EXTRA_FILESYSTEMS=... drop-in. Format reference: Beszel — additional disks.

proxfold (2026-04-25):

beszel_agent_extra_filesystems:
  - "/stash__stash"

Surfaces the 6-wide raidz1 stash pool so its capacity feeds the Beszel Disk alert. Pool name on its own (EXTRA_FILESYSTEMS=stash) is not valid — ZFS doesn't map to a single block device; use the mount point. Pool state (ONLINE/DEGRADED/FAULTED) and scrub/resilver events remain covered by ZED, not by Beszel — see henrygd/beszel#1541.