Skip to main content

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

  1. + New Check
  2. Name: Nightly Database Backup
  3. Schedule: Simple → every 1 day
  4. Grace time: 1 hour (allow 1h late before alerting)
  5. Save

You get a unique URL like:

https://hc.yourdomain.com/ping/UNIQUE-UUID

Cron schedule

For jobs with specific run times:

  1. Schedule: Cron
  2. Cron expression: 0 2 * * * (daily at 2:00 AM)
  3. Timezone: America/Los_Angeles
  4. 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

Email

  1. Settings → Notifications → + Add email integration
  2. Email address
  3. 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

StatusMeaning
NewNever pinged (newly created)
UpLast ping within schedule + grace
LatePast schedule + grace, not yet alerted
DownAlerted — job missed its window
PausedTemporarily 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.

Comments