How to Self-Host Healthchecks with Docker
What Is Healthchecks?
Healthchecks monitors your cron jobs, scheduled tasks, and background services by expecting periodic “pings” at unique URLs. If a ping doesn’t arrive on schedule, Healthchecks sends alerts through 25+ notification channels — Slack, Telegram, PagerDuty, email, Discord, and more. It replaces cloud services like Cronitor and Dead Man’s Snitch with something you fully control. healthchecks.io
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 512 MB of free RAM (app + PostgreSQL)
- A domain name (recommended for remote access and email notifications)
Docker Compose Configuration
Create a docker-compose.yml file:
services:
db:
image: postgres:16-alpine
container_name: healthchecks-db
restart: unless-stopped
volumes:
- healthchecks-db:/var/lib/postgresql/data
environment:
POSTGRES_DB: hc
POSTGRES_PASSWORD: change-this-db-password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
healthchecks:
image: healthchecks/healthchecks:v4.0
container_name: healthchecks
restart: unless-stopped
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
env_file:
- .env
command: >
bash -c 'while !</dev/tcp/db/5432; do sleep 1; done;
uwsgi /opt/healthchecks/docker/uwsgi.ini'
volumes:
healthchecks-db:
Create a .env file alongside your compose file:
# --- Core settings (MUST change these) ---
SECRET_KEY=generate-a-random-50-character-string-here
ALLOWED_HOSTS=hc.example.com
SITE_ROOT=https://hc.example.com
SITE_NAME=Healthchecks
# --- Database ---
DB=postgres
DB_HOST=db
DB_NAME=hc
DB_USER=postgres
DB_PASSWORD=change-this-db-password
DB_PORT=5432
# --- Security ---
DEBUG=False
REGISTRATION_OPEN=True
# --- Email notifications (configure for alerts) ---
[email protected]
EMAIL_HOST=smtp.example.com
[email protected]
EMAIL_HOST_PASSWORD=your-smtp-password
EMAIL_PORT=587
EMAIL_USE_TLS=True
# --- Ping endpoint ---
PING_ENDPOINT=https://hc.example.com/ping/
# --- Optional integrations ---
PROMETHEUS_ENABLED=True
WEBHOOKS_ENABLED=True
SLACK_ENABLED=True
APPRISE_ENABLED=False
Start the stack:
docker compose up -d
Initial Setup
After starting the containers, create a superuser account:
docker compose exec healthchecks /opt/healthchecks/manage.py createsuperuser
Enter your email, username, and password when prompted. Open http://your-server:8000 (or your configured domain) and log in.
Once logged in, create your first project, then add checks. Each check generates a unique ping URL like https://hc.example.com/ping/a1b2c3d4-.... Add these URLs to the end of your cron jobs:
# Example: ping after a successful backup
0 3 * * * /usr/local/bin/backup.sh && curl -fsS --retry 5 https://hc.example.com/ping/YOUR-UUID-HERE
Configuration
| Setting | Environment Variable | Default | Notes |
|---|---|---|---|
| Site URL | SITE_ROOT | http://localhost:8000 | Full URL including protocol |
| Registration | REGISTRATION_OPEN | True | Set to False after creating your account |
| Ping body limit | PING_BODY_LIMIT | 10000 | Max bytes stored per ping body |
| uWSGI workers | UWSGI_PROCESSES | 4 | Reduce to 2 on low-RAM systems |
| SMTP inbound | SMTPD_PORT | disabled | Set to 2525 for ping-by-email feature |
Notification integrations are configured per-project in the web UI, not via environment variables. Supported channels include Slack, Telegram, Discord, PagerDuty, OpsGenie, VictorOps, Pushover, Matrix, Mattermost, Rocket.Chat, Zulip, Microsoft Teams, Signal, webhooks, and email.
Advanced Configuration
Prometheus Metrics
Healthchecks exposes a Prometheus-compatible metrics endpoint at /projects/<project-uuid>/metrics/<api-key>. With PROMETHEUS_ENABLED=True (default), add a scrape target in your Prometheus config:
scrape_configs:
- job_name: healthchecks
scheme: https
static_configs:
- targets: ["hc.example.com"]
metrics_path: /projects/YOUR-PROJECT-UUID/metrics/YOUR-API-KEY
Ping by Email
Set SMTPD_PORT=2525 in your .env and expose port 2525 in your compose file. Each check gets an email address — sending any email to it counts as a ping. Useful for services that can send email but not HTTP requests.
File-Based Secrets
For Docker Swarm or enhanced security, use SECRET_KEY_FILE, DB_PASSWORD_FILE, and EMAIL_HOST_PASSWORD_FILE instead of their plain-text counterparts. Point them at mounted secret files.
Reverse Proxy
Place Healthchecks behind a TLS-terminating reverse proxy. Nginx Proxy Manager example — proxy to healthchecks:8000, enable WebSocket support, and set SITE_ROOT in .env to your public HTTPS URL. See Reverse Proxy Setup for detailed configuration.
Backup
Back up the PostgreSQL database:
docker compose exec db pg_dump -U postgres hc > healthchecks-backup.sql
The database contains all check definitions, ping history, and project settings. The application container itself is stateless. See Backup Strategy for automated approaches.
Troubleshooting
Healthchecks web UI shows “Bad Request (400)”
Symptom: Accessing the web UI returns a 400 error.
Fix: ALLOWED_HOSTS in .env must include the exact hostname you’re using to access the UI. Multiple hosts: ALLOWED_HOSTS=hc.example.com,localhost.
Pings not being received
Symptom: Checks show “Never” for last ping despite running curl.
Fix: Verify PING_ENDPOINT matches your actual public URL. Check that the reverse proxy forwards requests to port 8000. Test with: curl -v https://hc.example.com/ping/YOUR-UUID.
Email notifications not sending
Symptom: Checks go down but no email alerts arrive.
Fix: Verify SMTP settings. Test with Django’s email test: docker compose exec healthchecks python -c "from django.core.mail import send_mail; send_mail('Test', 'Body', None, ['[email protected]'])". Check DEBUG=False is set — debug mode suppresses emails.
Container exits immediately
Symptom: The healthchecks container starts and stops in a loop.
Fix: Check docker compose logs healthchecks. The most common cause is SECRET_KEY still set to the default ---. Generate a proper random key.
Database connection refused
Symptom: Logs show “connection refused” to PostgreSQL.
Fix: Ensure DB_HOST=db matches the service name in your compose file. The depends_on with service_healthy condition should prevent startup races, but verify the db container is healthy: docker compose ps.
Resource Requirements
| Resource | Value |
|---|---|
| RAM (app) | 80–120 MB idle, 200–300 MB under load |
| RAM (database) | 50–100 MB (PostgreSQL) |
| CPU | Very low — lightweight Django app |
| Disk | ~100 MB for image, database grows slowly with ping history |
Healthchecks runs as non-root (hc user) inside the container. The built-in Docker healthcheck probes /api/v3/status/ every 60 seconds.
Verdict
Healthchecks is the best self-hosted solution for monitoring cron jobs and scheduled tasks. It does one thing exceptionally well — alert you when expected pings stop arriving. The 25+ notification integrations, built-in Prometheus endpoint, and clean web UI make it production-ready. The only thing it doesn’t do is uptime monitoring (checking if services are up) — for that, pair it with Uptime Kuma or Gatus.
If you’re running any scheduled tasks — backups, database maintenance, report generation, data syncs — Healthchecks should be monitoring all of them.
FAQ
How is Healthchecks different from Uptime Kuma?
Healthchecks monitors cron jobs and scheduled tasks by receiving pings. Uptime Kuma monitors services by sending HTTP requests to check if they’re up. They’re complementary — Healthchecks tells you when a job doesn’t run; Uptime Kuma tells you when a service goes down.
Can I use SQLite instead of PostgreSQL?
Yes. Set DB=sqlite and remove the db service from your compose. The SQLite file is stored in /data inside the container — mount this directory to persist it. PostgreSQL is recommended for production because it handles concurrent pings better.
What happens if the ping URL leaks?
Anyone with the URL can send pings, which could mask failures. Use read-only API keys for monitoring dashboards and keep ping URLs private. You can regenerate a check’s ping URL from the web UI if compromised.
Does Healthchecks store ping request bodies?
Yes, up to PING_BODY_LIMIT bytes (default 10 KB). This is useful for debugging — your cron job can pipe its output to the ping. Example: my-script.sh 2>&1 | curl --data-binary @- https://hc.example.com/ping/UUID.
Related
Get self-hosting tips in your inbox
New guides, comparisons, and setup tutorials — delivered weekly. No spam.