How to Self-Host Healthchecks: Cron Job Monitoring 2026
·OSSAlt Team
healthcheckscronmonitoringself-hostingdockerdevops2026
TL;DR
Healthchecks (BSD 3-Clause, ~8K GitHub stars, Python/Django) monitors scheduled tasks by expecting periodic pings. When your nightly backup script or weekly cleanup job fails to check in, Healthchecks sends an alert. Unlike uptime monitoring (which watches if a URL is up), Healthchecks watches for what didn't happen — the silent failures that would otherwise go unnoticed for weeks. Healthchecks.io's hosted service limits you to 20 checks on the free plan. Self-hosted is unlimited.
Key Takeaways
- Healthchecks: BSD 3-Clause, ~8K stars, Python — "dead man's switch" for cron jobs
- Push model: Jobs ping a URL when they complete; Healthchecks alerts if no ping arrives
- Grace period: Configurable window — a job running 5 min late won't trigger a false alert
- Schedules: Simple (every N minutes/hours/days) or cron expression
- Integrations: Email, Slack, Discord, PagerDuty, ntfy, Telegram, 50+ more
- Teams: Multiple team members per project, audit log
Part 1: Docker Setup
# docker-compose.yml
services:
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: hc
POSTGRES_USER: hc
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
volumes:
- db_data:/var/lib/postgresql/data
healthchecks:
image: healthchecks/healthchecks:latest
container_name: healthchecks
restart: unless-stopped
ports:
- "8000:8000"
environment:
SECRET_KEY: "${SECRET_KEY}"
ALLOWED_HOSTS: "hc.yourdomain.com"
DEFAULT_FROM_EMAIL: "hc@yourdomain.com"
EMAIL_HOST: "mail.yourdomain.com"
EMAIL_PORT: 587
EMAIL_USE_TLS: "True"
EMAIL_HOST_USER: "hc@yourdomain.com"
EMAIL_HOST_PASSWORD: "${EMAIL_PASSWORD}"
DB: postgres
DB_HOST: db
DB_USER: hc
DB_PASSWORD: "${POSTGRES_PASSWORD}"
DB_NAME: hc
SITE_ROOT: "https://hc.yourdomain.com"
SITE_NAME: "Healthchecks"
REGISTRATION_OPEN: "False" # Disable public registration
depends_on:
- db
volumes:
db_data:
# Create initial superuser:
docker exec -it healthchecks python manage.py createsuperuser
docker compose up -d
Part 2: HTTPS with Caddy
hc.yourdomain.com {
reverse_proxy localhost:8000
}
Part 3: Create Your First Check
Simple schedule
- + New Check
- Name:
Nightly Database Backup - Schedule:
Simple→ every1 day - Grace time:
1 hour(allow 1h late before alerting) - Save
You get a unique URL like:
https://hc.yourdomain.com/ping/UNIQUE-UUID
Cron schedule
For jobs with specific run times:
- Schedule:
Cron - Cron expression:
0 2 * * *(daily at 2:00 AM) - Timezone:
America/Los_Angeles - Grace time:
30 minutes
Part 4: Integrate with Cron Jobs
Basic ping on success
# /etc/cron.d/backup:
0 2 * * * root /opt/scripts/backup.sh && curl -fsS -m 10 "https://hc.yourdomain.com/ping/YOUR-UUID" >/dev/null
# Breakdown:
# && → only ping if backup.sh succeeds (exit code 0)
# -fsS → fail silently on HTTP errors
# -m 10 → timeout after 10 seconds
# >/dev/null → suppress output
Ping with start signal (detect long-running jobs)
# Signal start:
curl -fsS -m 10 "https://hc.yourdomain.com/ping/YOUR-UUID/start"
# Run the job:
/opt/scripts/slow-job.sh
# Signal success:
curl -fsS -m 10 "https://hc.yourdomain.com/ping/YOUR-UUID"
Healthchecks measures the duration between start and success pings.
Ping with failure signal
#!/bin/bash
# /opt/scripts/backup-with-monitoring.sh
UUID="YOUR-UUID"
HC_URL="https://hc.yourdomain.com/ping/${UUID}"
# Signal start:
curl -fsS -m 10 "${HC_URL}/start"
# Run backup:
if /opt/scripts/backup.sh; then
# Success:
curl -fsS -m 10 "${HC_URL}"
else
# Explicit failure signal:
curl -fsS -m 10 "${HC_URL}/fail"
fi
Ping with log output
# Send last 10KB of job output with the ping:
/opt/scripts/job.sh 2>&1 | tail -c 10000 | curl -fsS -m 10 \
--data-binary @- "https://hc.yourdomain.com/ping/YOUR-UUID"
Part 5: Notification Channels
- Settings → Notifications → + Add email integration
- Email address
- All alerts for your project go here
Slack
Type: Slack
Webhook URL: https://hooks.slack.com/services/...
Telegram
Type: Telegram
Bot Token: from @BotFather
Chat ID: your chat ID
ntfy (self-hosted push)
Type: ntfy
Topic URL: https://ntfy.yourdomain.com/cron-alerts
Priority: High
PagerDuty
Type: PagerDuty
Routing Key: your-pagerduty-integration-key
Part 6: Check Management
Check status meanings
| Status | Meaning |
|---|---|
| New | Never pinged (newly created) |
| Up | Last ping within schedule + grace |
| Late | Past schedule + grace, not yet alerted |
| Down | Alerted — job missed its window |
| Paused | Temporarily paused (maintenance) |
Pause during maintenance
# Pause a check via API:
curl -X POST "https://hc.yourdomain.com/api/v3/checks/UUID/pause" \
-H "X-Api-Key: your-api-key"
# Resume:
# POST to /ping/UUID resumes automatically
Part 7: REST API
API_KEY="your-api-key"
BASE="https://hc.yourdomain.com/api/v3"
# List all checks:
curl "$BASE/checks/" \
-H "X-Api-Key: $API_KEY" | jq '.[].name'
# Get check status:
curl "$BASE/checks/UUID" \
-H "X-Api-Key: $API_KEY" | jq '.status'
# Create a check programmatically:
curl -X POST "$BASE/checks/" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Weekly Report",
"schedule": "0 9 * * 1",
"tz": "America/Los_Angeles",
"grace": 3600,
"channels": "*"
}' | jq '.ping_url'
# Get check's ping log:
curl "$BASE/checks/UUID/pings/" \
-H "X-Api-Key: $API_KEY" | jq '.[0:5]'
Part 8: Common Use Cases
Backup monitoring
# Database backup:
0 2 * * * root pg_dump -U postgres mydb | gzip > /backups/mydb-$(date +%Y%m%d).sql.gz \
&& curl -fsS "https://hc.yourdomain.com/ping/DB-BACKUP-UUID"
# File backup (restic):
0 3 * * * root restic backup /important/data \
&& curl -fsS "https://hc.yourdomain.com/ping/RESTIC-UUID"
Certificate renewal
# Certbot auto-renewal:
0 0 */2 * * root certbot renew --quiet \
&& curl -fsS "https://hc.yourdomain.com/ping/CERT-UUID"
Data sync jobs
# Nightly sync:
0 4 * * * root /opt/scripts/sync-data.py \
&& curl -fsS "https://hc.yourdomain.com/ping/SYNC-UUID" \
|| curl -fsS "https://hc.yourdomain.com/ping/SYNC-UUID/fail"
Maintenance
# Update:
docker compose pull
docker compose up -d
# Backup:
docker exec healthchecks-db-1 pg_dump -U hc hc \
| gzip > healthchecks-db-$(date +%Y%m%d).sql.gz
# Logs:
docker compose logs -f healthchecks
# Prune old pings (keep 6 months):
docker exec healthchecks python manage.py prunepings
See also: Uptime Kuma — for monitoring websites and services
See all open source monitoring tools at OSSAlt.com/categories/devops.