Healthchecks
Healthchecks watches for silence. Each job gets a unique URL and pings it when it runs; when an expected ping does not arrive within its period and grace time, Healthchecks alerts you. It catches what the other monitors cannot: a backup that stopped running, a timer disabled weeks ago, a host or a monitor that died. One container, with a SQLite database.
Last run on a host 8 Nov 2026
Pinned at v5.0.3 (released 27 Oct 2026) · the newest upstream release
In the upstream images: 0 CRITICAL · 9 HIGH with a published fix — per image
To try it: Docker, nothing else. See Try it locally.
To deploy it behind the proxy: Traefik running, a domain, and SMTP credentials if alerts should go out by email.
Try it locally
Section titled “Try it locally”On your own machine, with no proxy, domain or certificate. From monitoring/healthchecks/:
cp .env.local.example .env.localecho "HC_SECRET_KEY=$(openssl rand -hex 32)" >> .env.localmkdir -p volumes/local/datasudo chown -R 999:999 volumes/local/datadocker compose -f docker-compose.local.yml --env-file .env.local up -ddocker compose -f docker-compose.local.yml --env-file .env.local \ exec app python /opt/healthchecks/manage.py createsuperuserThe last command asks for an email address and a password. Then open
http://localhost:8000 and log in. Stop it again with the same command and
down.
Installation
Section titled “Installation”From monitoring/healthchecks/:
-
Copy the environment file:
Terminal window cp .env.example .envVariable What it is APP_TRAEFIK_HOSTthe hostname of the interface and the ping URLs HC_SITE_NAMEthe name in the interface and in email subjects HC_EMAIL_FROM,HC_SMTP_*the relay alerts are sent through HC_LOCAL_PORTthe loopback port for jobs on this host — 8001by default -
Generate the Django secret key and put it into
HC_SECRET_KEY:Terminal window openssl rand -base64 60 | tr -d '\n='Healthchecks has no
_FILEform for this value, so it lives in.env. Changing it later ends every session. -
List
healthchecks-edgeinEDGE_NETWORKSincore/traefik/.env, then render and restart Traefik — see edge networks. -
Create the data directory for the user the application runs as:
Terminal window mkdir -p volumes/datasudo chown -R 999:999 volumes/data -
Start it:
Terminal window docker compose up -ddocker compose logs healthchecks-app --followWait for
spawned uWSGI worker. -
Create the first account. Registration is closed, so this account invites everyone else:
Terminal window docker compose exec healthchecks-app /opt/healthchecks/manage.py \ -
Open
https://<APP_TRAEFIK_HOST>and log in.
Verify
Section titled “Verify”docker compose ps # one service, healthycurl -fsSI https://<APP_TRAEFIK_HOST>/ # 200curl -fsSI https://<APP_TRAEFIK_HOST>/projects/ # 302 to the loginAdd a check in the interface, copy its ping URL and call it:
curl <ping-url>The check turns green with a timestamp. That proves the ping path, not the alarm: attach a notification channel, give the check a short period and grace, stop pinging, and wait for the down notification to arrive. The next ping brings the up notification.
Pings from the same host
Section titled “Pings from the same host”Jobs on the host that runs this stack — borgmatic, the watchdogs — ping the loopback port instead of the public name:
http://127.0.0.1:8001/ping/<uuid>Through Traefik, such a ping arrives from the gateway address of the edge
network, and a closed access policy refuses it. Adding that gateway to
TRUSTED_RANGES would admit every container on the network. The loopback port
avoids the proxy: it is published on 127.0.0.1 only, nothing off the host
reaches it, and the route keeps its closed policy. The interface shows ping URLs
under https://<APP_TRAEFIK_HOST>; replace that part with
http://127.0.0.1:8001, the UUID stays the same.
Going further
Section titled “Going further”Who can reach it
Section titled “Who can reach it”The stack ships access-vpn, with limit-standard and headers-standard, for
jobs on hosts in your VPN. Switching the whole route to access-public opens the
login page and the check list as well, because one service serves both. For jobs
on public servers, keep access-vpn and give the ping path its own router at a
higher priority:
- "traefik.http.routers.hc-ping.rule=Host(`${APP_TRAEFIK_HOST}`) && PathPrefix(`/ping/`)"- "traefik.http.routers.hc-ping.priority=100"- "traefik.http.routers.hc-ping.middlewares=${APP_TRAEFIK_HEADERS}@file,access-public@file,${APP_TRAEFIK_LIMIT}@file,${APP_TRAEFIK_AUTH}"- "traefik.http.routers.hc-ping.service=${COMPOSE_PROJECT_NAME}"A ping URL needs no authentication; its UUID is what keeps it private. Signed ping URLs, per check in the interface, tighten that.
Alerts
Section titled “Alerts”Email needs working SMTP. Without it, alerts go through the integrations you set
per check: webhooks, ntfy, Slack, Discord and others. Delivering to a private
address — ntfy on the same Docker network, for example —
needs HC_ALLOW_PRIVATE_IPS=True, which the stack ships. Set it to False if
every receiver is public.
Backup
Section titled “Backup”| State | volumes/data — SQLite at /data/hc.sqlite: checks, ping URLs, channels, history |
| Critical | The ping URLs. Every job holds one; new ones mean reconfiguring every job, borgmatic included |
| Quiescing | Dump rather than copy |
For borgmatic:
sqlite_databases: - name: healthchecks path: /srv/docker/monitoring/healthchecks/volumes/data/hc.sqliteRestore order: stop, restore, start, then confirm that an existing job’s ping still registers — the URL must be unchanged.
Updates
Section titled “Updates”Database migrations run when the container starts.
-
Read the release notes, especially the database schema and configuration changes.
-
Stop the stack and copy the database — the image ships no
sqlite3client:Terminal window docker compose stopsudo cp volumes/data/hc.sqlite hc-$(date +%Y%m%d).sqlite -
Raise
APP_TAGin.env, then:Terminal window docker compose pull && docker compose up -ddocker compose logs healthchecks-app --tail 100 --follow -
Check that the interface loads and the existing checks are listed.
To roll back, stop the stack, copy the saved file back to
volumes/data/hc.sqlite and set the previous APP_TAG.
Troubleshooting
Section titled “Troubleshooting”The container stays unhealthy with unable to open database file.
volumes/data is not owned by uid 999. sudo chown -R 999:999 volumes/data,
then start again.
Traefik answers 404. APP_TRAEFIK_HOST does not match the DNS name exactly,
or healthchecks-edge is missing from EDGE_NETWORKS.
A check shows down although the job runs. The job cannot reach this instance — a host behind NAT outside the VPN, or a same-host job pinging the public name instead of the loopback port. Look at the job’s own log first.
No alert reaches ntfy on the same host. HC_ALLOW_PRIVATE_IPS is False.
Repository files
Section titled “Repository files”monitoring/healthchecks/README.md— setup, same-host pings, security modelmonitoring/healthchecks/.env.examplemonitoring/healthchecks/UPSTREAM.md— upgrade checklist and rollback
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content CC BY-NC 4.0