Build1 publisher2 min readPublished
Healthchecks turns the missing backup ping into the alarm
Self-hosted Healthchecks v4.4 expects every cron job to check in after a successful run, and it alerts when the ping is late by more than the grace time. Getting the container up takes three settings the guides get wrong.
The Engineer · Build desk

What happened
- A dev.to tutorial from Serverkueche deploys Healthchecks v4.4 behind Traefik with Docker Compose and SQLite, so each recurring job, Restic backups included, pings its own URL after a successful run.
- The alert logic is inverted: a check goes down when its expected ping fails to arrive inside the configured period plus grace time, so a cron entry someone deleted still raises an alarm.
- The official image does not read SUPERUSER_EMAIL or SUPERUSER_PASSWORD, and its startup hook only runs manage.py migrate, so the admin account must be made with manage.py createsuperuser.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Grace time becomes a per-job setting someone has to defend, because the same number fixes how long a dead backup stays invisible and how much run-time slack a healthy job gets before it pages.
- cost Coverage is manual work. Every script has to be edited to call its own ping URL, and any cron entry added later stays unmonitored until someone wires one in.
- exposure A ping confirms the script reached the ping call. A job that completed and wrote an unusable archive still shows green until someone tests a restore.
- capability Absence gets a timestamp, so the failure surfaces at a deadline the operator picked instead of at the moment the data is needed.
Detection latency is period plus grace. You choose both. For a daily backup the tutorial's example sets period to 1 day and grace time to 1 hour [6]. The check therefore goes down 25 hours after the last successful ping [1]. Against the failure the write-up opens with, a nightly backup dead for three weeks before anyone noticed [1], that turns 504 hours of silence into 25 [2].
Grace time settles a second question at the same time. The ping fires at the end of the job, after a successful run [4], so a run that takes longer than usual pings later than usual. Set the grace window narrower than that variation and a healthy job wakes someone up [3]. Set it a day wide and the latency you were trying to remove is back.
The write-up draws the line this way: "Uptime Kuma tells you when a service is up. Healthchecks tells you when a job didn't run." [2] The server knows a request arrived at a time [5]; it does not know whether the archive that request refers to can be restored [4]. Testing the restore is a separate job with its own ping URL.
The deployment notes are where the guide gets specific. With SQLite, DB_NAME must be /data/hc.sqlite; leave the path off and Healthchecks creates the database somewhere non-writable and fails to start with "unable to open database file" [9]. SITE_NAME has to stay pure ASCII, because one umlaut makes current Python raise UnicodeEncodeError: surrogates not allowed and the page answers HTTP 500 [10]. The author wrote that "this exact bug hit me while testing this tutorial" [11].
Then the login. Many guides list SUPERUSER_EMAIL and SUPERUSER_PASSWORD as environment variables. The official image does not read them; its startup hook only runs manage.py migrate, which the author reports testing on v4.3 and v4.4 [13]. You create the account yourself, with manage.py createsuperuser inside the running container [14]. The image also ships its own health check, and Traefik routes only once the container reports healthy, about 20 to 30 seconds [12].
Healthchecks is a Django application, and the tutorial runs it on SQLite, which it calls "perfectly sufficient for a typical self-hosting scale" [8]. The tutorial does not say how many checks or pings per minute that covers. For that judgement to carry to your setup, your setup has to look like the one described: a single container behind Traefik, one SQLite file at /data/hc.sqlite, and a handful of recurring jobs checking in once a day [7].
What to watch
- Whether a later Healthchecks image reads SUPERUSER_EMAIL and SUPERUSER_PASSWORD, which would remove the manual createsuperuser step.
- Whether the SITE_NAME UnicodeEncodeError is fixed upstream or survives in images built against newer Python.
- Whether the ping call is gated on the job's exit status, so a failed Restic run cannot check in and report success.