How to Know When a Cron Job Didn't Run (Self-Hosted Healthchecks)

The backup script stops running — the drive unmounted, cron got confused after a time change, the script errored on line 3 — and you find out months later. Uptime Kuma (tutorial 35) can't see it; nothing is "down". A dead-man's-switch monitor can: each job pings a URL when it finishes, and if the ping doesn't arrive on time, you get a message. Healthchecks is the self-hosted version of healthchecks.io, and the ping is one curl.
Quick reference
| Task | What |
|---|---|
| Folder | ~/docker/healthchecks/compose.yaml |
| Web | http://server-ip:8000 |
| Add a check | Add Check → name, period, grace → copy the ping URL |
| Ping at end of a job | curl -fsS -m 10 --retry 3 https://hc.example.com/ping/UUID |
| Report a failure | .../ping/UUID/fail |
| Alerts | Integrations → ntfy, Telegram, email, Discord, Slack |
1. The compose file
mkdir -p ~/docker/healthchecks && cd ~/docker/healthchecks
nano compose.yaml
services:
healthchecks:
image: lscr.io/linuxserver/healthchecks:latest
container_name: healthchecks
ports:
- "8000:8000"
environment:
PUID: 1000
PGID: 1000
TZ: Europe/London
SITE_ROOT: https://hc.example.com
SITE_NAME: Home Checks
SUPERUSER_EMAIL: john@example.com
SUPERUSER_PASSWORD: ${ADMIN_PASSWORD}
SECRET_KEY: ${SECRET_KEY}
REGISTRATION_OPEN: "False"
APPRISE_ENABLED: "True"
volumes:
- ./config:/config
restart: unless-stopped
.env with a long SECRET_KEY and ADMIN_PASSWORD. SITE_ROOT is the address the ping URLs will use — the LAN address is fine if jobs only run at home.
docker compose up -d
sudo ufw allow from 192.168.1.0/24 to any port 8000
2. Add a check
Log in at http://192.168.1.50:8000. Add Check:
- Name: Nightly backup
- Period: 1 day (how often it should ping)
- Grace time: 2 hours (how late is "late")
Copy the ping URL: http://192.168.1.50:8000/ping/3a9b.... For jobs on a cron schedule, choose Cron instead of simple and paste the schedule — Healthchecks then knows exactly when to expect each ping.
3. Ping from the job
At the end of the backup script (tutorial 23 / 55):
curl -fsS -m 10 --retry 3 -o /dev/null https://hc.example.com/ping/3a9b...
Or wrap the whole job so success and failure are both reported, with the output attached:
#!/bin/bash
URL=https://hc.example.com/ping/3a9b...
curl -fsS -m 10 --retry 3 -o /dev/null $URL/start
if /usr/local/bin/backup.sh > /tmp/backup.out 2>&1; then
curl -fsS -m 10 --retry 3 --data-binary @/tmp/backup.out $URL
else
curl -fsS -m 10 --retry 3 --data-binary @/tmp/backup.out $URL/fail
fi
/start lets it measure duration; /fail alerts immediately rather than waiting for the grace period; the body shows up in the check's log so you read the error without SSH.
The one-line cron form:
0 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://hc.example.com/ping/3a9b...
4. What to watch
Anything that should happen regularly:
- the nightly backup, the rclone upload, the restic prune
- SnapRAID sync (tutorial 99), ZFS scrub (97), SMART long tests (56)
- certbot renewal (ping from a --deploy-hook)
- DuckDNS update (48) — tutorial 48's script plus a curl
- Watchtower runs (WATCHTOWER_NOTIFICATION_URL can't ping, but a Sunday cron check with the same schedule can)
- a Pi in the shed that should check in hourly
5. Alerts
Integrations → Add: ntfy (tutorial 93) with your topic and token, Telegram, email via SMTP, or Apprise for anything else. Each check can use different integrations; the default is all of them. You'll get "Nightly backup is DOWN" when a ping is late, and "is now UP" when it returns.
Dashboard
Status badges per check and a read-only status page (/status/) make a nice Homepage widget (tutorial 94 supports Healthchecks directly) — a row of green dots for every job on the server.
Troubleshooting
Ping URL shows localhost. SITE_ROOT is wrong; set it to the address jobs will reach.
Check always "late" by an hour. Time zone mismatch between the server, the container's TZ and the cron schedule you pasted.
curl fails with certificate errors from a Pi. Through the https proxy the Pi needs up-to-date CA certs (sudo apt install ca-certificates), or use the LAN http address.
Too many alerts after a reboot. Grace time too short for jobs that run on boot; or mark checks Paused during maintenance.
Wrapping up
One container, a check per job, a curl at the end of each script. The silent failures — the backup that quietly stopped in March — become a message within hours, and the status page shows every scheduled job on the server as a green dot you can glance at.
