Most self-hosting failures are silent. This guide sets up one watcher that tells your phone when an app stops, then gives you a short set of commands to find out why.
How servers fail without a sound
Nothing pops up when a server has a problem. You find out when an app does not load. The usual causes are dull:
- A full disk. Databases and apps stop writing. Some crash, some keep running and lose data.
- An expired certificate. The app works, but browsers show a scary warning and refuse to continue. A certificate is the file that lets a site use HTTPS. Most setups renew it automatically, and that renewal can fail without notice.
- An out-of-memory kill. When the machine runs out of RAM, the Linux kernel stops a process to save the rest. The app vanishes. Docker often starts it again, so it looks fine for a while.
- A restart loop. A container starts, crashes, and starts again. The status says "running" for a few seconds each time.
You cannot watch for these by looking. You need something that checks for you and sends a message.
The default watcher: Uptime Kuma
For a novice, the simplest choice is Uptime Kuma. It is a small web app that checks your URLs on a schedule and alerts you when one fails. Its README lists monitor types including HTTP(s), TCP, keyword on a page, Ping, DNS record, Push and Docker containers. It also lists 90+ notification services, among them Telegram, Discord, Gotify, Slack, Pushover and email.
This is the project's compose file, with one change: the port is bound to 127.0.0.1, so only the machine itself can reach it. See Docker Compose basics if you have not used one:
services:
uptime-kuma:
image: louislam/uptime-kuma:2
restart: unless-stopped
volumes:
- ./data:/app/data
ports:
- "127.0.0.1:3001:3001"
Run docker compose up -d. Open it through an SSH tunnel (ssh -L 3001:127.0.0.1:3001 you@watcher, then http://127.0.0.1:3001) and create the admin account. Keep that page private. Before you share an app explains how to expose an app safely, and Choose your front door explains the options.
Put it on a different machine
A monitor on the same server it watches dies with that server. If the disk fills or the machine loses power, the watcher goes quiet at the same moment as your apps. Silence then looks like "no news".
So run Uptime Kuma somewhere else. Two good places:
- A cheap VPS. The Home server or VPS guide covers the trade-off. Uptime Kuma is small, so the smallest plan is enough. Check the current price on your provider's page.
- A second machine at home, such as a Raspberry Pi, ideally on a different power plug and network cable than the main server.
If your apps are only reachable on a private network, the watcher must join that network too. What is Tailscale? shows how.
Add an outside check for the watcher
Now one more question: who watches the watcher? A private Uptime Kuma page cannot be checked from outside, so turn the check around. Healthchecks.io gives you a URL and raises an alert as soon as a ping does not arrive on time. As of October 2026 its free plan covers 20 checks. On the watcher machine, add a cron line (see crontab -e below) that pings it only while Uptime Kuma answers:
*/5 * * * * curl -fsS -o /dev/null http://127.0.0.1:3001 && curl -fsS -o /dev/null https://hc-ping.com/your-uuid-here
If you made the Kuma page public, a hosted checker such as UptimeRobot can poll it instead.
Send alerts to your phone
Email works, but you will not see it for hours, and mail from a new server often lands in spam. Before you share an app covers why email is hard to set up well. A chat or push message reaches you in seconds.
Pick one channel in Uptime Kuma under Settings, then Notifications. Telegram, Discord and ntfy all have phone apps. Each is on Uptime Kuma's list of supported services. Use the "Test" button on the notification form. Do this before you need it.
Tick "Default enabled" so every new monitor uses it.
What to watch
Start with three things.
- Each app's URL. Add an HTTP(s) monitor for every app you run. This catches crashes, restart loops and a dead proxy in one check. Use the keyword type if an app shows an error page with a 200 status.
- Certificate expiry. On an HTTPS monitor, turn on "Certificate Expiry Notification". The number of days of warning is set in Settings under "TLS Certificate Expiry". The app's own interface text says: "HTTPS Monitors trigger notification when TLS certificate expires in:" followed by your chosen days. A warning two weeks ahead gives you time to fix a failed renewal.
- Disk space. Uptime Kuma has no disk monitor of its own. Use a Push monitor instead. A Push monitor gives you a URL. Your server calls that URL on a schedule, and Uptime Kuma raises an alert when the calls stop or report
status=down. The source code acceptsstatus=down.
Create the Push monitor, copy its URL (it looks like https://kuma.example.com/api/push/<token>?status=up&msg=OK&ping=), and set the heartbeat interval to 10 minutes. Then save this script on the server as /usr/local/bin/disk-check.sh:
#!/bin/sh
USED=$(df --output=pcent / | tail -1 | tr -dc '0-9')
BASE="https://kuma.example.com/api/push/your-token-here"
if [ "$USED" -ge 85 ]; then
curl -fsS "$BASE?status=down&msg=disk-${USED}pct"
else
curl -fsS "$BASE?status=up&msg=disk-${USED}pct"
fi
Make it runnable, then run it every 5 minutes with cron. If curl or crontab is missing, run sudo apt install -y curl cron first.
sudo chmod +x /usr/local/bin/disk-check.sh
crontab -e
Add this line:
*/5 * * * * /usr/local/bin/disk-check.sh
If the whole server dies, the pushes stop and you get an alert too. The 85 is a rule of thumb. Pick a number that leaves you days to react.
Stop Docker logs from filling the disk
This is the most common quiet disk filler. By default, Docker keeps every container's output in a json-file log. The Docker docs say max-size defaults to -1 (unlimited), and max-file only works when max-size is set. A chatty app can write gigabytes.
Set a limit for all containers in /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
This keeps at most three files of 10 MB per container. Values in log-opts must be strings, so keep the quotes. If daemon.json already has content, merge these keys in, and keep the file valid JSON. Then restart Docker:
sudo systemctl restart docker
The change applies only to newly created containers. Recreate each stack once. Files in volumes and bind mounts stay, but check where your data lives first:
docker compose up -d --force-recreate
A first-aid kit
When an alert arrives, look before you touch anything. Run these from the folder with your compose.yaml.
Is the app running?
docker compose ps -a
docker compose logs --tail 100 <service>
ps -a also lists stopped containers. --tail 100 shows the last 100 lines. A status such as "Restarting" means a restart loop. The last log lines usually name the cause. Add -f to follow the log live.
Is the disk full?
df -h
sudo du -xh / --max-depth=1 2>/dev/null | sort -h | tail
docker system df
df -h shows free space per disk. du shows which top-level folder is large, and you can repeat it inside that folder. docker system df shows how much space images, containers and volumes use. Add -v for detail.
Is the memory gone?
free -h
sudo journalctl -k | grep -i -E 'out of memory|oom'
docker inspect --format '{{.State.OOMKilled}}' <container>
free -h shows RAM and swap. The journalctl -k command reads kernel messages, where an out-of-memory kill is logged. On some systems sudo dmesg -T shows the same lines. The Docker API says the OOMKilled field is true if a process in the container was killed for lack of memory since its last start. The fix is more RAM, more swap, or fewer apps. Many apps, one server helps you size this.
Is the certificate valid? Open the site in a browser and read the warning. Check your proxy's logs next.
Do not reinstall to fix a problem
A reinstall feels clean. It is also how data gets lost. Containers are disposable, but the volumes and folders that hold your data are not. Reading logs destroys nothing. Wiping and rebuilding can. Take a copy first, as Backups you can restore explains, and update only with a plan.
Optional: read your access logs
GoAccess turns a web server's access log into a report of visitors, pages and errors. It reads log files, and supports formats including Combined, Caddy and Traefik. That means your reverse proxy must write an access log first. Many do not by default. Treat this as a second step, after alerts work.
Triage order
When something is wrong, go in this order:
- Is the server reachable? If not, use your provider's console.
df -h. A full disk causes many odd faults.free -hand the OOM check.docker compose ps -afor stopped or restarting containers.docker compose logs --tail 100 <service>for the reason.- The certificate date, if only HTTPS fails.
- Fix the cause. Do not wipe the data.
If a fault costs you data, Backups you can restore shows how to get it back.