Most homelab monitoring starts as a dashboard. It is satisfying for about a week: CPU, memory, disk, network, all moving. Then you stop opening it, because nothing on it ever tells you to do anything.
A dashboard is something you check. An alert checks for you. It is a promise that something will interrupt you when a specific thing goes wrong. That promise is only worth making for a handful of failures, and only if the alert is quiet the rest of the time. This is the short list I would keep.
The monitor cannot live on the thing it watches
The first failure to plan for is the monitor itself. If the tool that watches your server runs on that server, its death is silent. The box goes down, the checks stop, and nothing is left to notice that the checks stopped.
Put the checker somewhere else. A small VPS is enough, or the one machine in the house that is not the media server. Uptime Kuma and Beszel both fit in a container and neither needs much. Beszel splits into a hub and an agent, so the hub can live on one host while agents report from the others.
Then give the monitor a heartbeat of its own. Beszel can post to an external endpoint on a schedule with HEARTBEAT_URL. The docs put the reason plainly: it lets you monitor the health of the Beszel instance and the systems it tracks without exposing the hub to the internet. The default gap is 60 seconds, and a service like Uptime Kuma or Healthchecks.io can watch for the missing post. If Beszel disappears, something outside notices. The checker needs a checker.
I would rather run two small monitors that watch each other than one perfect one that watches everything. The failure mode with one is the one you never hear about.
Service health: check the page that needs the database
The most common useless check is a request to the domain that returns 200 because the reverse proxy served something. A static homepage can be up while the application behind it is gone. Check a page that cannot render without the app and its database: the login form, a settings page, or a health endpoint that touches storage.
Uptime Kuma has a keyword monitor for HTTP responses. It searches the body for a word that only appears when the app is genuinely working, such as the label on the login button, rather than a word that appears in the site’s footer on every page. The match is case-sensitive, so type the label exactly as it renders. A JSON query monitor extracts a value from the response with an expression and compares it to an expected value as strings.
Retries are what keep this alert believable. Uptime Kuma describes the setting as the maximum retries before the service is marked as down and a notification is sent. With a 60 second heartbeat and two retries, a three minute outage is what wakes you. That is the right trade for a home server. One missed check is a dropped packet; three in a row is a service.
For things that cannot be polled from outside, use a push monitor instead. The job itself calls the monitor when it succeeds, and the monitor alerts when the call does not arrive.
Certificates: the failure with a date
Certificate renewal fails quietly. A DNS token expires, port 80 gets blocked, an ACME client stops running, and the first symptom is the certificate running out. This is one of the few outages you get to see coming, so it deserves an alert.
On an HTTPS monitor, Uptime Kuma has a certificate expiry notification checkbox. The day counts are set once under Settings, on the Notifications page, under TLS Certificate Expiry, and you can add more than one. The monitor notifies when the certificate has that many days left. I set two: fourteen days and three. One is an early warning; the other is a deadline. Two weeks is enough time to find the renewal fault on a normal evening; three days is the one that means do it now.
Beszel has the same idea in its network monitors. The Certificate Expiration alert triggers when an HTTPS certificate nears expiration, and its default is 14 days. The range goes from 1 to 365, so a certificate you renew once a year can have its own runway.
The alert does not care that the service is healthy. An expired certificate turns a working server into a browser warning, and API clients that do not offer a bypass simply fail. That is worth one notification two weeks early.
Disk: turn the percentage into time
A disk at 90 percent is not automatically a problem. On a 4 TB media array, 90 percent might be the normal resting state. On a 32 GB root partition, 90 percent might be tomorrow morning. The percentage is the wrong question. What matters is how fast it is moving.
Beszel’s disk alert triggers when usage of any disk exceeds a threshold, and the alert also takes a duration. Set the threshold where you still have time to act, and make the duration long enough to ignore a temporary spike from a log rotation or a container image pull. The number is a deadline.
I use 85 percent for ten minutes on the root filesystem and 95 percent for an hour on the media array. The database volume gets the root treatment, because a full disk under PostgreSQL or SQLite is not just a full disk.
The trap with Beszel is that an agent only reports the root filesystem and the extra filesystems it was told about. Additional disks have to be mounted into the container under /extra-filesystems, or declared with EXTRA_FILESYSTEMS for a binary agent. ZFS pools and datasets are the exception: Beszel finds those automatically. Otherwise the graph shows the root disk, you read it as the array, and the array fills up unwatched.
Then look at the slope once. If the disk gains one percent a month, 85 percent is years away. If it gains one percent a day, 85 percent is a fortnight. The alert is a deadline only after you know which of those you are looking at.
Backups: alert on silence
The backup alert worth having is not that the job ran. It is that a successful backup has not happened in longer than it should. A backup job can run, print an error and still exit zero. The absence of a success survives all of that.
Uptime Kuma’s push monitor does exactly this. The monitor generates a Push URL, and any request to that URL marks it up. A request with status=down marks it down immediately. If no request arrives inside the heartbeat window, the monitor raises “No heartbeat in the time window” and notifies.
Put the call at the end of the backup script, after the command that actually wrote the data:
set -eu
restic backup /srv/data
curl -fsS --retry 3 --max-time 10 \
"https://kuma.example.com/api/push/TOKEN?status=up&msg=backup-ok"
If the backup command exits non-zero, set -e stops the script before the curl, and the monitor goes down one window later. If you want the failure sooner, add a failure branch that calls the same URL with status=down. Both paths end with you finding out.
Set the heartbeat window to the longest gap you are willing to accept, not the interval you hope the job keeps. A daily backup with a 26-hour window tolerates a run up to two hours late. One missed night is a 48-hour gap, so it alarms. If you want to tolerate a missed night, set the window above 48 hours.
The second half of backup monitoring is the restore. A backup that has never been restored is a hope with a schedule. Backups that restore explains why a successful job is not the same as a usable copy, and the restore runbook walks the order: secrets first, then the data and the database dump, then DNS, then a check that proves the service is actually back. Read it before you need it, and run it once against a throwaway host.
What I leave on the dashboard
The dashboard is for looking, not for waking you. These are the numbers I would happily graph and never alert on:
CPU usage. A media server transcoding at 100 percent is doing its job. The service check will tell you when it stops doing it.
Memory usage. Linux fills memory with cache and gives it back under pressure. A high number is not an incident; an OOM kill usually shows up as a restart, and the service check catches that.
Container restarts. A restart policy doing its job is not an outage. A restart is not a crash. If the container restarts five times in an hour, the service check will show the downtime that matters.
Ping latency. Wi-Fi jitter is not a reason to be woken. If latency matters, alert on packet loss over an hour instead. Beszel’s network monitor loss alert defaults to five percent over the last hour, which is a real signal rather than a single slow ping.
Temperature. I do not alert on CPU temperature. Disk temperature and S.M.A.R.T. health are the exceptions, because a failing drive does not always give you a second warning.
Any disk above 80 percent. This is the alert that teaches people to ignore alerts. If the disk has years of headroom, the number is decoration.
The rule behind the list: if an alert can fire while the thing still works, it is a graph. Alerts are for conditions where you have to act.
Start with these two containers
Uptime Kuma is the black-box checker. It is one container. Its own install page gives one command:
docker run -d --restart=unless-stopped -p 3001:3001 \
-v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:2
Browse to port 3001, make the admin account, and add the monitors. The docs warn that the data directory needs real POSIX file locking, so map /app/data to a local volume and keep it off NFS. SQLite corruption is a poor reward for a monitor.
Beszel is the inside-the-host view: disk, failed services, containers, and the agent-side network probes. The hub is the first service in its documented compose file:
services:
beszel:
image: henrygd/beszel:latest
container_name: beszel
restart: unless-stopped
environment:
APP_URL: http://localhost:8090
ports:
- 8090:8090
volumes:
- ./beszel_data:/beszel_data
Then add the system in the web UI and copy the agent compose from the Add System dialog, which carries the key and token for that host. Change APP_URL to the address you will actually use. The hub can sit on the monitoring box; the agent goes on each machine whose disk and services you want to see.
If I had to keep one, it would be Uptime Kuma, because the push monitor is the alert I care about most. Beszel earns its place for the disk and failed-service alerts, which are awkward to reproduce from outside.
Test the alert, or you have a dashboard
Every alert on this list can be triggered on purpose. Stop the service and watch the phone. Point a monitor at a host that is not there. Let the backup job skip its curl once. Test each one once, before you trust it, because the first real outage is a bad time to discover that the notification channel was never connected.
The first test is also the one that tells you whether the alert is too loud. If one outage buzzes you three times for the same blip, the retries are wrong. If a failed backup waits two days, the window is too long.
Four alerts is about as many as I can keep believing. Every one you add is a bet that you would rather be interrupted than find out later. The quiet is the feature.

