Category
Monitoring and observability
Two rules decide whether monitoring helps or just adds another service to maintain: it must not run on the machine it watches, and it must cost less than that machine's actual workload.
Monitoring goes wrong in two structural ways, and neither is about picking the wrong tool.
The first is that it runs on the machine it watches. Fine until the day it matters, when the host wedges, the checker goes with it, and the absence of an alert reads exactly like everything being fine. A monitor on the box that died tells you nothing.
The second is that it costs more than the thing it watches. Per-second metrics with anomaly detection on a 2 GB Pi running three containers makes the observability stack the primary workload: a new thing that can break, watching two old things that mostly did not.
The default: Uptime Kuma, on a different machine#
Uptime Kuma is the default for "is my stuff up" if you want a UI rather than a config file. One container, no database service, 120 MB idle, notifications to almost anything, and a status page for the household. Version 2 changed the storage model: v1 wrote one heartbeat row per check forever, v2 keeps 24 hours of raw beats and rolls the rest into minute, hour and day tables.
Run it somewhere else: a Pi, an old thin client, the cheapest VPS you can find. The instance watching your server must not depend on your server.
The default breaks down in two places.
Your infrastructure lives in git and you want checks reviewed like code. Gatus, 25 MB, ops load 1, entirely YAML. Two defaults bite: storage defaults to memory, so an unconfigured Gatus shows history and forgets it on restart with no warning, and maximum-number-of-results defaults to 100, which at a 60 second interval is the last hour and a half.
You want resource numbers across several machines, not just up or down. Beszel: a 40 MB hub, an agent image around 4 MB, CPU, memory, disk, containers, ZFS, SMART and alerts. The 30 day retention windows are hard-coded rather than configurable, so seasonal comparisons need something else.
Alert on what you would get out of bed for#
Write the alert list before you install anything. For a home server it is four things: the host is unreachable, a disk is failing or nearly full, backups have not run in 48 hours, and a certificate expires within a week. That is what should reach your phone.
Everything else is a graph. CPU pinned during a library scan is not an incident, and memory at 85 percent is how Linux is supposed to behave. Alerting on those produces a channel you learn to ignore, which is worse than no channel because you believe you are covered.
When you actually want the heavy tier#
Netdata shows you more about a machine ten seconds after installation than anything else here, at about 150 MiB of RAM, 5 percent of a core and 4 GiB of disk. Fair on a server, bad on an SD card. The agent is GPLv3, but the dashboard UI is NCUL1 and pulled from a CDN.
Prometheus plus Grafana is for asking questions of the data rather than looking at it. Budget the discipline: 60 second scrapes, the TSDB on an SSD, retention set explicitly, and a check on prometheus_tsdb_head_series after every new exporter, because cardinality turns a 150 MB process into a multi-gigabyte one.
For logs, Dozzle is 15 MB and retains nothing by design. Its real lesson is that Docker's json-file driver has no size limit: set max-size and max-file in /etc/docker/daemon.json before a chatty container fills the root disk.
The mistakes people actually make#
- Putting Uptime Kuma's SQLite on the NAS. The README lists NFS as unsupported and CIFS behaves the same:
database is locked, then a database that will not open. Keep/app/datalocal. - Exposing port 19999. Netdata's dashboard is unauthenticated by design. Bind it to a private interface in
netdata.confand put an authenticating proxy in front. - Leaving Prometheus retention unset. It keeps 15 days and deletes the rest silently, which you discover when the graph you wanted stops dead.
- Bind-mounting a Grafana directory as root. The image runs as uid 472, so
-v ./grafana:/var/lib/grafanafails with permission denied and exits.chown -R 472:472, or use a named volume. - Pointing a Beszel agent at
localhost. With hub and agent as containers on one host,localhostresolves inside each namespace and the connection silently fails; the documented fix is a shared unix socket. Uptime Kuma behind a proxy that does not pass websocket upgrade headers fails the same quiet way: the page loads and then spins forever.
What to read next#
The minimum viable monitoring stack is the build sheet. For the tool decisions, Uptime Kuma vs Gatus, Beszel vs Netdata and Netdata vs Prometheus and Grafana. The alerts that matter most are about data, so pair this with Backups that actually restore and ZFS, btrfs, mdadm or one disk, then score yourself with Resilience scorecard. For delivery, ntfy vs Gotify.
Every monitoring tool we profile#
Sorted by operational load, lightest commitment first. Every row links to a full profile with the resource numbers, the upgrade traps and the restore path.
| Tool | Ops load | Idle RAM | Licence | Identity | Backup shape |
|---|---|---|---|---|---|
| Beszel | 1, Set and forget | 40 MB | MIT | Native OIDC | SQLite backup |
| Dozzle | 1, Set and forget | 15 MB | MIT | Trusted proxy header | File copy |
| Gatus | 1, Set and forget | 25 MB | Apache-2.0 | Native OIDC | SQLite backup |
| Grafana | 2, Light | 150 MB | AGPL-3.0-only | Native OIDC | SQLite backup |
| Netdata | 2, Light | 150 MB | GPL-3.0-or-later (agent); dashboard UI under NCUL1 | Auth proxy only | SQLite backup |
| Uptime Kuma | 2, Light | 120 MB | MIT | Local accounts only | SQLite backup |
| Prometheus | 3, Moderate | 150 MB | Apache-2.0 | Auth proxy only | Mixed |
| Scrutiny | 3, Moderate | 100 MB | MIT | Auth proxy only | SQLite backup |
Profiles#
Beszel
Small multi-server dashboard: CPU, memory, disk, containers, alerts, 30 days of history
ops 1/5 · 15 MBDozzle
Live Docker log viewer in the browser that stores nothing
ops 1/5 · 25 MBGatus
Health checks and a status page defined entirely in YAML, with no clickable state
ops 2/5 · 150 MBGrafana
Dashboards and alerting on top of data you already store, AGPLv3 since 2021
ops 2/5 · 150 MBNetdata
Per-second metrics for a whole machine with no configuration, and no login on port 19999
ops 2/5 · 120 MBUptime Kuma
Uptime checks, alerting and status pages in one container, no separate database
ops 3/5 · 150 MBPrometheus
Pull-based metrics database with 15 day default retention and an exporter for everything
ops 3/5 · 100 MBScrutiny
SMART data from every disk, tracked over time, scored against real failure rates
Head to head in this category#
- Beszel vs NetdataTiny and deliberately limited against comprehensive and heavy. Both numbers are published; here they are.
- Netdata vs Prometheus and GrafanaOne agent versus five containers. Most homelabs build the stack and never open the dashboards again.
- Uptime Kuma vs GatusChecks in a database with a UI, or checks in a file in git. Everything else follows from that.
Guides for this category#
- The minimum viable monitoring stackTier one is an off-box uptime check, disk alerting and one channel you read. Most homelabs stop there.
- An update strategy that does not lose dataNotify, do not auto-update. Pin tags, snapshot before upgrading, and know which projects cannot be rolled back.
Questions#
Where should the monitoring run?
Somewhere that can fail independently of what it watches. A checker on the same host reports perfectly right up to the moment the host dies, and then reports nothing, which is indistinguishable from silence. The cheapest correct answer for a homelab is a second small machine (a Pi, an old thin client, a low-cost VPS) running the uptime checks and holding the alert channel. Uptime Kuma is honest about this limit: one instance is one vantage point and it cannot tell you it went down itself. Pair it with an external ping or a second instance elsewhere.
What should actually send me an alert?
Only things you would get out of bed for, which for a home server is a very short list: the host is unreachable, a disk is failing or full, backups have not completed in 48 hours, and certificates expire within a week. Everything else is a graph you look at when something feels wrong. CPU at 90 percent during a media scan is normal; a paging rule for it teaches you to ignore the channel, and an ignored channel is worse than no channel because you believe you are covered.
How much RAM should monitoring use?
Less than the service it protects. Beszel's hub idles around 40 MB and its agent image is about 4 MB. Gatus is 25 MB, Dozzle 15 MB, Uptime Kuma 120 MB. Netdata publishes about 150 MiB of RAM, roughly 5 percent of a core and about 4 GiB of disk by default, which on a 2 GB Raspberry Pi watching three containers means the monitoring is the workload. Prometheus plus Grafana is 300 MB before you count exporters. Pick the tier that matches the machine, not the tier that matches the dashboard screenshots.
Uptime Kuma or Gatus?
Uptime Kuma if you want to add a check from your phone at 2am and see a status page without editing YAML. Gatus if your infrastructure already lives in a git repository and clicking through a UI to add a check feels like a step backwards: point GATUS_CONFIG_PATH at a directory and it deep merges every .yaml in it, so each service ships its check next to its compose file, reloaded on write. Gatus costs 25 MB against Uptime Kuma's 120 MB. Both do the job; this is a workflow preference, not a capability gap.
Do I need Prometheus and Grafana?
Only if you will actually query the data. Prometheus is a component, not a product: it stores numbers on a timeline, has no dashboards worth showing anyone, and defaults to 15 days of retention when neither storage.tsdb.retention.time nor .size is set. Grafana on its own is an empty shell with nothing to graph. Together they are the right answer when you need PromQL, alert rules over expressions, or history past a month. For "is it up and is the disk full", they are several hundred megabytes of infrastructure answering a question a 25 MB binary answers.
Will Prometheus wear out my SD card?
Yes, if you leave it on the boot media with default settings. The example config scrapes every 15 seconds, every scrape appends to a write-ahead log, the head block is written out every two hours and compaction rewrites blocks after that. That constant small-write pattern is exactly what kills SD cards and cheap USB drives, typically four to twelve months in. Set the global scrape interval to 60s for host metrics and put the TSDB on an SSD or NVMe. Storage itself is cheap: 1,000 active series at 15 second scrapes is roughly 10 MB a day compacted.
Do I need disk health monitoring?
You need to know a drive is dying before it does, but not necessarily with a dashboard. On a two-disk mini PC, smartd with an email alert does most of the job for nothing. Scrutiny earns its 100 MB and its InfluxDB dependency on a multi-bay NAS where trends across many drives matter. If you run it, note that NVMe needs --cap-add SYS_ADMIN on top of the usual SYS_RAWIO, and that leaving the collector on its daily default is deliberate: setting it to every 15 minutes produces nearly 55,000 datapoints per device.
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.