Beszel: The Lightweight Server Monitor
One binary, one dial, and nothing you didn't ask for

Contents
Most monitoring software is designed by people solving a much larger problem than yours. This is not a criticism — Prometheus exists because someone at SoundCloud had thousands of ephemeral containers and a genuine need — but the consequence is that the small case gets served by an enormous tool wearing a small hat. You want to know whether the NAS is running out of disk. You get a query language, a rule evaluator, a service-discovery subsystem and a 3 GB resident process.
Beszel is a young project that took the opposite approach: work out the smallest thing that is still genuinely useful, build that, and stop. It appeared this year, it is a Go binary and a Svelte front-end sitting on PocketBase, and the whole hub uses about 40 MB of RAM while the agent on each monitored host uses about 15. It gives you CPU, memory, disk, network, temperature and per-container stats, with alerts, behind a login, with no configuration file anywhere.
I have been running it for a couple of months alongside the grown-up stack, and I have opinions.
The architecture, which is the interesting part
Beszel has two pieces.
The hub is the web UI and the database. It runs on one machine, stores everything in a single SQLite file via PocketBase, and serves the dashboard.
The agent runs on every machine you want to watch. It reads the local system, and — here is the design decision that makes the whole thing click — it listens as an SSH server on a high port. The hub connects out to each agent over SSH, authenticated by a keypair, and pulls the current stats.
That is an unusual choice and a good one. There is no bearer token to leak, no HTTP API to accidentally expose, no shared secret in a config file. The agent will only talk to something holding the private key that matches the public key it was given at startup. If you have ever exposed a metrics endpoint to your whole LAN because binding to localhost broke the scrape, you will appreciate the difference. The transport is SSH, which is a protocol with thirty years of scrutiny behind it, doing exactly what it is good at.
The cost of pull-over-SSH is the same cost Prometheus pays for pull: the hub must be able to reach the agent. Machines behind NAT elsewhere need a tunnel or a mesh VPN, and Beszel offers no push mode to work around it.
Standing it up
The hub, in compose:
| |
That is the whole hub. One image, one volume, one port. Bind it to localhost and put your existing reverse proxy in front — Beszel has its own login and it is fine, but a second authentication layer costs nothing and you already have one.
Open it, create the first user (the first account to register becomes admin, so do this immediately rather than at the weekend), and click “Add system”. The UI hands you a copy-pasteable block containing a generated public key. That is the entire onboarding flow.
The agent on each host:
| |
Or as a plain binary with systemd, which is what I run on the machines that are not container hosts:
| |
The KEY value there is a placeholder; yours comes from the hub UI. The systemd hardening directives are mine rather than the project’s, and they cost nothing — the agent needs to read /proc and /sys and nothing else on disk.
FILESYSTEM matters more than it looks. Without it the agent guesses at the root filesystem and, on a box with a dozen container overlay mounts and a ZFS pool, it guesses wrong roughly half the time and charts a device you do not care about. Give it the device name explicitly, and list any others in EXTRA_FS. This is the only configuration in Beszel that I would call mandatory.
Total elapsed time from docker compose up on the hub to a working fleet view across five machines: under fifteen minutes, and most of that was me ssh-ing around pasting keys.
What you get, and what you very much do not
The dashboard is a grid of systems, each showing CPU, memory, disk usage and network throughput as sparklines with current values. Click one and you get full charts, per-filesystem breakdowns, temperature sensors if lm-sensors finds any, and — the part I use most — a per-container table with CPU and memory per container, sortable.
That container table is the single feature that keeps Beszel on my hub. docker stats gives me the same numbers for one host, for right now, in a terminal I have to keep open. Beszel gives me the same numbers for eleven hosts with an hour of history and a sort-by-memory column. That is a genuinely different tool.
Now the list of what is absent, which is longer and equally important:
- No query language. No PromQL, no ad-hoc maths, no derived metrics. You get the charts the developer drew.
- No long retention. Beszel keeps fine-grained records for around an hour, then progressively coarser averages out to roughly a month. There is no year of history, and no way to ask for one.
- No log aggregation. For logs you want something like Loki, which does the job without the Elasticsearch tax.
- No SMART detail. It will show you a drive’s temperature. It will not tell you about reallocated sectors, which is what Scrutiny is for.
- No arbitrary exporters. If a service exposes metrics, Beszel cannot ingest them. It watches the host and the containers, full stop.
- No dashboards you design. There is no panel editor. This is a feature.
Beszel is a young project moving quickly, so some of that list will shrink. Some of it is the design and should stay.
Alerts
Per-system, in the UI, you set thresholds for status, CPU, memory, disk, bandwidth and temperature, each with a value and a duration. Status alerts fire when an agent stops answering.
Notification delivery is Shoutrrr, which means the same URL syntax a dozen other Go projects use, and which means ntfy works with a one-line URL:
| |
Set the durations honestly. A CPU alert at 80% for 1 minute will fire every time a backup runs, every time a container image builds, and every time you transcode anything. Mine are all at 10 minutes minimum, and the disk one is the only threshold I have ever been glad of. The general principle applies here as everywhere: an alert that cries wolf is worse than no alert, because you will mute the channel and then believe you are covered.
The alerts are simple in the specific sense that they are per-system thresholds with no expressions. You cannot write “alert if disk will be full within 30 days based on the trend”, which is the alert I actually want and which Prometheus gives me in one line of predict_linear. Beszel’s answer is that you will see it on the chart. Which is true, if you look.
Where it sits against the alternatives
The honest comparison is against three things.
Against Uptime Kuma: these are complementary and I run both. Kuma answers “is it responding” from outside, per service. Beszel answers “how is the host” from inside, per machine. Neither replaces the other, and together they cost about 100 MB.
Against Netdata: Netdata gives you a thousand times more data at per-second resolution with auto-detected collectors for everything you run. It also gives you a dashboard so dense that finding the chart you want takes real effort, an agent that needs SYS_ADMIN and the host PID namespace, and 300 MB per node. Beszel is 15 MB per node and shows you six things. If your question is “which machine is the sad one”, Beszel answers faster.
Against Prometheus and Grafana: no contest on capability, and no contest on cost either, in the opposite direction. The metrics stack is hours of setup, a real config surface and a genuine skill investment, in exchange for arbitrary questions over a year of history. Beszel is fifteen minutes and answers six questions about the last hour.
The interesting thing is that these are complements more often than substitutes. My Prometheus watches the things I have thought about. Beszel watches all eleven boxes in one glance, and it is the tab I actually open.
The PocketBase escape hatch
Beszel is built on PocketBase, and the project does not hide it. That has a practical consequence worth knowing: everything Beszel collects sits in ordinary SQLite tables behind a REST API you can reach.
The admin UI lives at /_/ and shows you the raw collections — systems, system_stats, container_stats, alerts, users. If the dashboard will not show you something, you can query for it:
| |
This is how you get a number out of Beszel and into something else — a script, a dashboard, a nightly report. It is not an integration story anyone would design on purpose, and it works.
The same substrate is why backup is trivial. One SQLite file holds users, systems, keys and every metric. Copy it, or better, use SQLite’s own online backup so you get a consistent snapshot of a live database rather than a torn one:
| |
Given the retention ceiling, losing it costs you a month of coarse averages. That is a very small thing to lose, which is itself part of the argument for the tool: nothing here is precious, so nothing here needs a recovery plan.
Troubleshooting
The system shows “down” but the machine is fine. The hub cannot reach port 45876 on the agent. Check the host firewall and check network_mode: host on the agent container — bridged networking here means the agent reports the container’s stats rather than the host’s, when it works at all.
Disk usage charts the wrong device. Set FILESYSTEM to the device name (sda2), with no /dev/ prefix. df -h tells you which one you meant.
Container stats are missing. Docker socket not mounted, or the agent user is not in the docker group on a binary install. Mounting the socket into any container is a real privilege grant — the socket is a root-equivalent API — so decide whether you want that on every host or only on the ones where the container table earns it.
Temperature shows nothing. Install lm-sensors and run sensors-detect on the host. In the container image you also need /sys visibility, which network_mode: host alone does not give you; add --privileged or accept no temperatures. I accept no temperatures.
The hub loses history after a redeploy. The volume is not mounted at /beszel_data. Everything — users, systems, keys, all the metrics — lives in one SQLite file in there. Which also means backing Beszel up is a single file copy, and that is worth appreciating.
Charts have a gap after a hub restart. Expected. The hub polls; if it is down, nobody collects. There is no agent-side buffer. For a monitoring tool this is a real limitation and the reason I would never let Beszel be the only thing watching something that matters.
The honest verdict
Beszel is a young project by essentially one developer, on a fast release cadence, with a small feature surface and no long-term data model. That combination should make you cautious about anything you would be upset to lose. I would not build an on-call rotation on it today.
For what it is, though, it is close to perfect. The SSH-keyed agent is a genuinely elegant piece of design that sidesteps an entire class of exposure problem. The resource footprint means installing it on a Pi that is already doing three other jobs costs nothing you will notice. And the fifteen-minute setup means it clears the bar that matters most for homelab software: it is easier to install than to keep putting off.
Who should run it: anyone with three to twenty machines who wants one page showing all of them, and anyone whose real alternative is nothing at all because the Prometheus setup has been pending since spring. Who should skip it: anyone who needs history, arbitrary queries or service-level metrics — you want the full stack, and adding Beszel on top is precisely the kind of pleasant-but-unnecessary accretion that turns a homelab into a second job.
I still run the big stack. I still open Beszel first.




