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

SettingEnvironment VariableDefaultNotes
Site URLSITE_ROOThttp://localhost:8000Full URL including protocol
RegistrationREGISTRATION_OPENTrueSet to False after creating your account
Ping body limitPING_BODY_LIMIT10000Max bytes stored per ping body
uWSGI workersUWSGI_PROCESSES4Reduce to 2 on low-RAM systems
SMTP inboundSMTPD_PORTdisabledSet 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

ResourceValue
RAM (app)80–120 MB idle, 200–300 MB under load
RAM (database)50–100 MB (PostgreSQL)
CPUVery 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.