Healthchecks: Cron-Jobs & Backups überwachen (Dead-Man-Switch)
Merkt, wenn ein Backup NICHT läuft: Healthchecks überwacht Cron-Jobs per Dead-Man-Switch und alarmiert bei Ausfall – selbst gehostet hinter Traefik.
Inhaltsverzeichnis
Die gefährlichsten Ausfälle sind die stillen: Das nächtliche Backup läuft seit drei Wochen nicht mehr – und niemand merkt es, bis man die Daten wirklich braucht. Uptime Kuma sagt dir, wenn ein Dienst da ist. Healthchecks sagt dir, wenn ein Job nicht gelaufen ist. Genau das ist der Unterschied, der Daten rettet.
Was bauen wir?
Einen selbst gehosteten Healthchecks-Server (v4.4) hinter Traefik, der als Dead-Man-Switch funktioniert: Jeder überwachte Job „meldet sich“ nach erfolgreichem Lauf mit einem kurzen HTTP-Ping. Bleibt dieser Ping aus (weil der Job abgestürzt ist, der Server aus war oder der Cron-Eintrag fehlt), schlägt Healthchecks Alarm. Am Ende überwachst du damit deine Restic-Backups, Datenbank-Dumps und jeden anderen wiederkehrenden Job – und wirst benachrichtigt, bevor das Fehlen auffällt.
Voraussetzungen
- Ein Server mit laufendem Traefik und Docker Compose
- Eine Subdomain, die auf den Server zeigt – im Folgenden
DEINE_DOMAIN - Wiederkehrende Jobs, die es zu überwachen lohnt (z. B. deine Restic-Backups)
VPS 1000 G12.5
4 vCore · 8 GB RAM · 128 GB SSD
ab 14,50 €/Monat
Healthchecks ist genügsam und läuft gut neben deinem übrigen Stack.
💶 5 € Gutschein für netcup-Neukunden: (nicht für Domains und VPS Lite)
Schritt für Schritt
Schritt 1: Das Prinzip verstehen – ein umgedrehtes Monitoring
Klassisches Monitoring fragt aktiv: „Antwortet der Dienst?“ Healthchecks dreht das um: Der Job meldet sich beim Server. Jeder Check hat eine eindeutige Ping-URL. Nach erfolgreichem Lauf ruft der Job diese URL auf. Healthchecks erwartet den Ping in einem festgelegten Zeitfenster (Period) plus einer Toleranz (Grace Time). Kommt der Ping nicht rechtzeitig, gilt der Check als „down“ und Healthchecks alarmiert. Das ist der Dead-Man-Switch: Nicht das Vorhandensein eines Signals löst Alarm aus, sondern sein Ausbleiben.
Schritt 2: Compose-Datei anlegen
Healthchecks ist eine Django-Anwendung; wir betreiben sie mit SQLite – für den typischen Selfhosting-Umfang völlig ausreichend. Lege das Projekt an:
mkdir -p /opt/healthchecks/data && cd /opt/healthchecks
chown -R 1000:1000 dataDie compose.yaml – ersetze DEINE_DOMAIN und erzeuge einen eigenen SECRET_KEY (openssl rand -hex 32):
services:
healthchecks:
image: healthchecks/healthchecks:v4.4
restart: unless-stopped
user: "1000:1000"
volumes:
- ./data:/data
environment:
SITE_ROOT: https://DEINE_DOMAIN
SITE_NAME: Serverkueche Healthchecks
ALLOWED_HOSTS: DEINE_DOMAIN
CSRF_TRUSTED_ORIGINS: https://DEINE_DOMAIN
DEBUG: "False"
DB: sqlite
DB_NAME: /data/hc.sqlite
SECRET_KEY: "DEIN_ZUFAELLIGER_SCHLUESSEL"
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.hc.rule=Host(`DEINE_DOMAIN`)"
- "traefik.http.routers.hc.entrypoints=websecure"
- "traefik.http.routers.hc.tls.certresolver=le"
- "traefik.http.services.hc.loadbalancer.server.port=8000"
networks:
proxy:
external: trueZwei Fallen: DB_NAME und keine Umlaute in SITE_NAME
DB_NAME: /data/hc.sqlite Pflicht – ohne den Pfad legt Healthchecks die Datenbank an einer nicht beschreibbaren Stelle an und startet mit „unable to open database file“ nicht. Und: Schreibe SITE_NAME rein in ASCII (kein „ü“, „ö“ …). Ein Umlaut in dieser Umgebungsvariable führt bei aktuellem Python zu einem UnicodeEncodeError: surrogates not allowed – die Seite quittiert dann mit HTTP 500 (genau das ist mir beim Testen dieses Tutorials passiert).Schritt 3: Starten und anmelden
docker compose up -dDas Image bringt einen Healthcheck mit; Traefik leitet erst weiter, wenn der Container healthy ist (etwa 20–30 Sekunden). Prüfe:
docker compose psNAME IMAGE STATUS
healthchecks-healthchecks-1 healthchecks/healthchecks:v4.4 Up (healthy)Ein Konto bringt Healthchecks nicht von selbst mit – das Image legt beim Start nur die Datenbank an. Leg deinen Zugang deshalb einmalig selbst an (Passwort ersetzen):
docker compose exec healthchecks python manage.py createsuperuser \
--email admin@DEINE_DOMAIN --password DEIN_STARKES_PASSWORTSuperuser created successfully.SUPERUSER_EMAIL wirkt nicht
SUPERUSER_EMAIL und SUPERUSER_PASSWORD als Umgebungsvariablen
in der Compose-Datei. Das offizielle Image liest sie nicht – sein Start-Hook führt
ausschließlich manage.py migrate aus. Wer sich darauf verlässt, steht vor einer Anmeldeseite,
an der kein Konto existiert (getestet mit v4.3 und v4.4). Der Weg über createsuperuser oben
ist der verlässliche.Öffne jetzt https://DEINE_DOMAIN/ und melde dich mit diesen Zugangsdaten an:

Schritt 4: Ein Projekt und den ersten Check anlegen
Nach dem Login legst du über New Project… ein Projekt an (z. B. „Serverküche“) und darin über Add Check deinen ersten Check. Gib ihm einen sprechenden Namen, Tags und einen Zeitplan – Period = erwarteter Abstand zwischen zwei Läufen (für ein tägliches Backup: 1 Tag), Grace Time = wie lange Healthchecks nach dem Fälligkeitszeitpunkt noch wartet, bevor es Alarm schlägt (z. B. 1 Stunde). Die Übersicht zeigt alle Checks mit Status, Ping-URL und letztem Ping:

Jeder Check bekommt eine eigene Ping-URL der Form https://DEINE_DOMAIN/ping/<UUID>. Ein Klick auf den Check öffnet die Detailseite mit Anleitung, Verlauf und Status:

Schritt 5: Einen Job den Ping senden lassen
Jetzt der Kern. Deinen Job lässt du nach erfolgreichem Lauf die Ping-URL aufrufen. Das einfachste Beispiel – am Ende deines Skripts:
curl -fsS -m 10 --retry 5 https://DEINE_DOMAIN/ping/DEINE_CHECK_UUID-fsS macht curl leise, aber meldet Fehler; -m 10 bricht nach 10 Sekunden ab; --retry 5 fängt kurze Netzwerkaussetzer ab. Noch besser: den Exit-Code des Jobs mitmelden, damit ein fehlgeschlagener Lauf sofort als Fehler erscheint statt als „kein Ping“:
#!/bin/bash
URL="https://DEINE_DOMAIN/ping/DEINE_CHECK_UUID"
# ... hier läuft dein eigentlicher Job ...
restic backup /wichtige/daten
# Exit-Code an Healthchecks melden (0 = ok, sonst Fehler)
curl -fsS -m 10 --retry 5 "$URL/$?"Schritt 6: Restic-Backups überwachen
Das ist der Paradefall. Wenn deine Restic-Backups per systemd-Timer laufen, ergänzt du den Ping am Ende des Backup-Skripts. Läuft das Backup nicht (Timer deaktiviert, Server aus, Skript abgestürzt), bleibt der Ping aus – und nach Ablauf der Grace Time alarmiert Healthchecks. So erfährst du von einem toten Backup nach Stunden, nicht erst beim Datenverlust.
Start- und Fehlersignale nutzen
.../ping/UUID/start vor dem Job misst zusätzlich die Laufzeit, ein Ping auf .../ping/UUID/fail meldet aktiv einen Fehlschlag. So siehst du nicht nur ob, sondern auch wie lange ein Job lief – nützlich, um schleichend langsamer werdende Backups zu erkennen.Schritt 7: Benachrichtigungen einrichten
Ein Alarm nützt nur, wenn er dich erreicht. Unter Integrations verbindest du Kanäle: E-Mail (dafür SMTP-Umgebungsvariablen setzen), ntfy, Telegram, Webhooks und viele mehr. Für den Selfhosting-Stack ist ntfy die naheliegende Wahl – Push aufs Handy, ohne fremden Dienst. Richte mindestens einen Kanal ein und weise ihn deinen Checks zu, sonst bleibt der „down“-Status stumm.
Wenn es nicht funktioniert
Container startet nicht: „unable to open database file“. DB_NAME: /data/hc.sqlite fehlt oder der data-Ordner gehört nicht UID 1000. Pfad setzen und chown -R 1000:1000 /opt/healthchecks/data (siehe Warnung in Schritt 2).
Die Seite antwortet mit HTTP 500. Häufigste Ursache: ein Umlaut in SITE_NAME (oder einer anderen Text-Umgebungsvariable). Auf reines ASCII umstellen und neu starten. Zur Diagnose vorübergehend DEBUG: "True" setzen – aber danach wieder auf False.
Login schlägt fehl / CSRF-Fehler beim Absenden. CSRF_TRUSTED_ORIGINS: https://DEINE_DOMAIN muss gesetzt sein – Django lehnt sonst POST-Anfragen hinter dem Reverse Proxy ab. Und ALLOWED_HOSTS muss exakt deine Domain enthalten.
Der Check wird nicht „grün“, obwohl der Job läuft. Prüfe, ob der curl-Ping wirklich ausgeführt wird und die richtige UUID trifft: curl -v https://DEINE_DOMAIN/ping/UUID sollte OK zurückgeben. Auf der Detailseite siehst du im Protokoll, ob und von welcher IP Pings ankommen.
Ich bekomme keine Benachrichtigung bei „down“. Es ist kein Integrations-Kanal zugewiesen, oder (bei E-Mail) fehlen die SMTP-Einstellungen. Unter „Integrations“ einen Kanal einrichten und dem Check zuweisen.
Wartung & Backups
- Updates. Setze den Image-Tag (
healthchecks/healthchecks:v4.4) gelegentlich auf die aktuelle Version unddocker compose up -d; die Datenbank migriert beim Start automatisch. Den Rest übernimmt dein normaler Update-Prozess. - Backup. Der gesamte Zustand liegt in der SQLite-Datei unter
data/– ins Restic-Backup aufnehmen. Kleiner, aber feiner Nebeneffekt: Healthchecks überwacht dann das Backup, das es selbst mitsichert – schließe den Kreis, indem der Backup-Job auch einen Healthchecks-Check pingt. - Wer überwacht den Wächter? Healthchecks selbst sollte laufen, wenn es alarmieren soll. Ergänze es deshalb in Uptime Kuma als HTTP-Monitor – so decken sich die beiden Werkzeuge gegenseitig ab: Kuma prüft, dass Healthchecks erreichbar ist, Healthchecks prüft, dass deine Jobs gelaufen sind.
Feedback per E-Mail: feedback@serverkueche.de
Das könnte dir auch schmecken

Homepage: das Dashboard für alle deine selbstgehosteten Dienste
Ein aufgeräumtes Start-Dashboard für deinen Server: Homepage verlinkt alle Dienste, zeigt Systemlast und live den …

ntfy: Push-Benachrichtigungen vom eigenen Server aufs Handy
ntfy mit Docker & Traefik selbst hosten: Push-Nachrichten aufs Handy per einfachem curl – ideal für Backup-Meldungen, …

Immich selbst hosten: dein privates Foto-Backup hinter Traefik
Immich mit Docker hinter Traefik aufsetzen: die selbst gehostete Alternative zu Google Fotos – mit automatischem …