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 withbeszel_agentas 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-agentvia template was pointing at a path the installer doesn't use; dropped in the as-executed role. - Hub pubkey has no separate
.pubfile. Beszel only writes the private key tobeszel_data/id_ed25519. Derive the public key withssh-keygen -y -f /opt/beszel/beszel_data/id_ed25519and stash into vault. - Hub listens on plain HTTP by design. TLS terminates at the reverse-proxy layer;
https://<ip>:8090will not respond. Usehttp://<ip>:8090for LAN access until nginx is wired up. nesting=1is required on the hub LXC (same Debian-13-systemd-257 gotcha as PBS) — set atpct createtime 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_urlcheck-mode hostility — the cached-installer fetch task is stat-gated. Without it, daily drift reportschanged=1on 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):
CAP_SYS_RAWIO— smartctl's-d satpath uses SCSI-passthrough ioctls for ATA SMART. Thediskgroup permitsread()but not the ioctls.DeviceAllow=block-sd rw— the upstream systemd unit setsDeviceAllow=for nvidia devices only, which cgroup-blocks/dev/sd*at the kernel level. Without this, smartctl fails withexit 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):
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.
Related¶
- Roadmap — Phase 5B
- Role: zfs — ZED webhook complements Beszel for zpool-level events
- Role: proxmox — PVE 9 notification target complements Beszel for backup + replication events