Install via docker compose
# docker-compose.yml
services:
healthchecks:
image: healthchecks/healthchecks:latest
container_name: healthchecks
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000"
volumes:
- ./data:/data
environment:
DEBUG: "False"
SECRET_KEY: ${SECRET_KEY}
ALLOWED_HOSTS: "ping.example.com"
DEFAULT_FROM_EMAIL: "healthchecks@example.com"
EMAIL_HOST: smtp.example.com
EMAIL_PORT: 587
EMAIL_HOST_USER: healthchecks@example.com
EMAIL_HOST_PASSWORD: ${SMTP_PASSWORD}
EMAIL_USE_TLS: "True"
REGISTRATION_OPEN: "False" # disable after creating your account
SITE_ROOT: https://ping.example.com
SITE_NAME: "Healthchecks"
DB: sqlite # or postgres for >a few users
DB_NAME: /data/hc.sqlite
docker compose up -d
docker compose logs -f healthchecks
For first-run signup, set REGISTRATION_OPEN: "True", create your account, then flip it back to "False" and restart.
Reverse proxy
# Caddy
ping.example.com {
reverse_proxy 127.0.0.1:8000
}
Healthchecks needs to be HTTPS-reachable from every host that will ping it.
Create your first check
In the UI, click "+ Add Check":
- Name: "Nightly DB backup"
- Schedule: choose Simple with period "1 day" + grace "1 hour" (allow up to 1 day + 1h between pings before alerting). Or choose Cron and paste the exact cron expression for an irregular schedule.
- Tags: optional grouping ("prod", "backup", "weekly").
The check page shows its unique ping URL: https://ping.example.com/<uuid>.
Ping from the job
At the end of a successful job, hit the ping URL:
# Bash cron entry
15 3 * * * /usr/local/bin/db-backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null \
https://ping.example.com/<uuid>
# Or as a systemd ExecStartPost (see /tutorials/systemd-timers-cron-replacement.html)
[Service]
ExecStart=/usr/local/bin/db-backup.sh
ExecStartPost=/usr/bin/curl -fsS -m 10 --retry 5 -o /dev/null \
https://ping.example.com/<uuid>
The && means the ping only fires on success (exit 0). If the job fails, no ping fires; Healthchecks notices the missing ping within the grace window and alerts.
Ping success / failure / start / log
# Plain success
curl https://ping.example.com/<uuid>
# Explicit failure ping (skips waiting for the timeout)
curl https://ping.example.com/<uuid>/fail
# Mark a long-running job as started, then success
curl https://ping.example.com/<uuid>/start
./run-job.sh
curl https://ping.example.com/<uuid>
# Include log output as the ping body (shown in the UI)
./run-job.sh 2>&1 | curl --data-binary @- https://ping.example.com/<uuid>
Wrap any command with runitor
runitor wraps any command, sending start + success/failure pings around it automatically:
runitor -uuid <uuid> -api-url https://ping.example.com -- /usr/local/bin/db-backup.sh
One binary, drop-in around any cron command. Handles start, success, failure, stdout/stderr capture, exit-code reporting.
Notification channels
Integrations → configure once per channel; per-check, attach the channels that should alert:
- Email (already-configured SMTP)
- Slack / Discord / Microsoft Teams webhooks
- PagerDuty / OpsGenie / VictorOps
- Pushover / Pushbullet / Gotify
- SMS via Twilio
- Generic webhook (POST to any URL)
- ntfy.sh / self-hosted ntfy
- Matrix (see that tutorial)
- Signal (via signal-cli)
The webhook channel + n8n (see that tutorial) covers any custom integration.
Status pages
Per project, optionally publish a public status page that surfaces the checks: green/yellow/red status, uptime history. Useful for transparency with stakeholders ("yes, the nightly export ran"). Auth-required projects don't expose this; explicit opt-in.
API for programmatic management
The Healthchecks Management API lets you create / update / delete checks programmatically:
curl -X POST https://ping.example.com/api/v3/checks/ \
-H "X-Api-Key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Nightly backup — web-01",
"schedule": "0 3 * * *",
"grace": 3600,
"tags": "prod backup",
"channels": "*"
}'
Useful for Ansible / Terraform / config-management that creates a check per host or per service automatically.
Backups for Healthchecks itself
The SQLite database under ./data/hc.sqlite is everything. A nightly restic on the directory (see that tutorial) covers it. For larger setups, switch to Postgres and back it up separately.
Where this catches things
- The script that broke six months ago because a referenced binary moved — cron silently fails; Healthchecks notices.
- The home internet that goes down and takes the backup VPN with it; the cloud-side health check stops getting pings.
- The host that ran out of disk and stopped writing to the database the backup expected to read.
- The certbot renewal that hasn't fired in 80 days because the .timer was disabled by accident.
The passive nature is the key insight: "if you don't hear from me, alert." Active monitoring (Beszel; see that tutorial) catches different failures; the two complement each other. Pair both, and silent-failure-bugs lose most of their hiding places.