← Blog

Healthchecks.io selbst hosten, um Cron-Jobs zu überwachen

Railyard

19. Juli 2026 · 10 Min. Lesezeit

Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig

Ein Cron-Job, der still aufhört zu laufen, kann dir nicht sagen, dass er aufgehört hat. Das Skript läuft einfach eine Nacht nicht, nichts crasht, nichts loggt einen Fehler, und drei Wochen später merkst du auf die harte Art, dass die Backups nicht mehr gelaufen sind, seit der Kernel des Servers aktualisiert wurde und der Cron-Daemon nie zurückkam. Healthchecks.io ist ein Dead-Man’s-Switch, genau für diesen Fall gebaut: Jeder deiner geplanten Jobs pingt eine eindeutige URL, wenn er fertig ist, und wenn ein Ping nicht im Zeitplan ankommt, den du Healthchecks mitgeteilt hast, alarmiert es dich per E-Mail, Slack, Webhook oder einer der vielen anderen unterstützten Integrationen. Diese Anleitung richtet eine selbst gehostete Instanz auf deinem eigenen Server ein, mit Postgres, HTTPS über Caddy und echter Alert-Mail, und zeigt danach den Ein-Klick-Weg, falls du das manuelle Setup überspringen willst.

Was du brauchst

  • Einen Server. Ein Hetzner CX23 (2 vCPU, 4 GB Arbeitsspeicher, 40 GB NVMe) für 5,99 €/Monat exkl. MwSt. reicht für Healthchecks, sein eigenes Postgres und einen Reverse Proxy, die zusammen auf einer Box laufen, für ein persönliches Setup oder die Cron-Jobs eines kleinen Teams.
  • Einen Domainnamen (oder eine Subdomain einer, die du bereits besitzt), mit Zugriff auf die DNS-Einstellungen.
  • Docker und das Docker-Compose-Plugin auf dem Server installiert (Schritt 2 deckt das ab).
  • Einen SMTP-Account oder Relay, über den du Mails verschicken kannst, denn Alert-Mails sind der ganze Sinn dieser Sache.
  • Ein Terminal und SSH-Zugriff auf den Server.

Schritt 1: Server erstellen und eine Domain darauf zeigen lassen

Starte einen Ubuntu 24.04 Server auf Hetzners CX23-Plan (2 vCPU, 4 GB Arbeitsspeicher, 40 GB NVMe, 5,99 €/Monat exkl. MwSt.). Merke dir die öffentliche IP-Adresse und füge deinen SSH-Key bei der Erstellung hinzu oder direkt danach, damit du nicht auf ein Passwort angewiesen bist.

Füge in den DNS-Einstellungen deiner Domain einen A-Record hinzu, der eine Subdomain, zum Beispiel checks.yourdomain.com, auf diese IP zeigt:

Type: A
Name: checks
Value: <your server's IP>
TTL: 300

Bestätige, dass die Auflösung funktioniert, bevor du weitermachst:

dig checks.yourdomain.com +short

Das sollte die IP deines Servers ausgeben, keine alte und nicht nichts. Während du dabei bist, schränke die Firewall auf das ein, was dieser Stack braucht:

ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Schritt 2: Docker installieren

Verbinde dich per SSH mit dem Server und installiere Docker Engine plus das Compose-Plugin:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Melde dich ab und wieder an, damit die Gruppenänderung wirksam wird, und prüfe dann, dass beide Teile vorhanden sind:

docker --version
docker compose version

Schritt 3: Projekt und seine Secrets anlegen

Erstelle ein Verzeichnis für den Stack:

mkdir -p ~/healthchecks && cd ~/healthchecks

Erzeuge einen zufälligen Secret Key und ein Datenbank-Passwort. Healthchecks nutzt SECRET_KEY, um Sessions zu signieren, also ist ein schwacher oder wiederverwendeter Wert ein echtes Problem, keine Formalität:

openssl rand -base64 32
openssl rand -base64 24

Erstelle .env mit den eigenen Konfigurationsschlüsseln des Projekts, ausgefüllt für deine Domain:

# .env
DOMAIN=checks.yourdomain.com
DB=postgres
DB_HOST=db
DB_PORT=5432
DB_NAME=healthchecks
DB_USER=postgres
DB_PASSWORD=<the password you generated>
ADMINS=you@yourdomain.com
DEFAULT_FROM_EMAIL=healthchecks@yourdomain.com
SITE_ROOT=https://checks.yourdomain.com
SITE_NAME=Healthchecks
PING_ENDPOINT=https://checks.yourdomain.com/ping/
REGISTRATION_OPEN=False
SECRET_KEY=<the key you generated>
GUNICORN_WORKERS=4
GUNICORN_PORT=8000

Ein paar davon sind wichtiger, als sie aussehen:

  • SITE_ROOT ist die vollständige HTTPS-URL deiner Instanz, nicht nur die blanke Domain. Healthchecks baut damit jeden Link, den es dir zeigt oder mailt, einschließlich der Ping-URLs selbst.
  • PING_ENDPOINT sollte SITE_ROOT mit angehängtem /ping/ entsprechen. Es ist eine separate Einstellung, weil manche Installationen Ping-Traffic von einem anderen, leichteren Pfad als die Haupt-UI bedienen; für dieses Einzelcontainer-Setup teilen sie sich dieselbe Domain.
  • REGISTRATION_OPEN=False schließt die öffentliche Registrierung auf deiner Instanz. Standardmäßig ist sie offen, was die eine Einstellung in dieser Datei ist, die du nicht vergessen willst, denn eine offene Registrierungsseite auf einer öffentlichen Domain ist eine Einladung für Fremde, Accounts auf deinem Server anzulegen.
  • DOMAIN ist eine zweite, blanke Domain-Form desselben Werts, genutzt vom Caddyfile in Schritt 5. Healthchecks selbst liest DOMAIN nie; es liest SITE_ROOT.

Du brauchst auch SMTP-Einstellungen, damit Healthchecks dir tatsächlich Alert-Mails schicken kann, denn ein Monitoring-Tool, das dir nicht mailen kann, wenn ein Job still wird, macht seinen Job nicht. Füge EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_PORT und EMAIL_USE_TLS zur selben .env-Datei hinzu, ausgerichtet auf welchen SMTP-Provider oder -Relay du auch schon nutzt. Die eigene Self-Hosted-Konfigurationsdoku des Projekts deckt jede mailbezogene Variable im Detail ab, falls dein Provider mehr als die Grundlagen braucht.

Setze Berechtigungen, damit nur dein User die Datei lesen kann:

chmod 600 .env

Schritt 4: Docker-Compose-Datei schreiben

Healthchecks’ eigene docker/docker-compose.yml aus dem GitHub-Repository des Projekts ist so geschrieben, dass sie das Image lokal aus docker/Dockerfile baut und Port 8000 direkt auf den Host veröffentlicht, was die richtige Form zum Betreiben auf deiner eigenen Maschine ist, aber nicht für einen Server, der dem Internet zugewandt ist. Dieses Setup passt sie auf drei Arten an: Es nutzt das vorgebaute healthchecks/healthchecks-Image statt aus dem Quellcode zu bauen, fixiert einen exakten Versions-Tag statt latest, damit ein unbeaufsichtigter Neustart dich nicht still auf ein neues Release verschiebt, und veröffentlicht 8000 überhaupt nicht auf den Host, da Caddy stattdessen davor steht.

Erstelle docker-compose.yml:

services:
  db:
    image: postgres:17-alpine
    restart: unless-stopped
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=${DB_NAME}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - healthchecks-net

  web:
    image: healthchecks/healthchecks:v5.0.1
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    networks:
      - healthchecks-net

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - DOMAIN=${DOMAIN}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - web
    networks:
      - healthchecks-net

networks:
  healthchecks-net:

volumes:
  db-data:
  caddy_data:
  caddy_config:

postgres:17-alpine ersetzt das schlichte postgres:16, das das eigene Beispiel des Projekts nutzt; beide funktionieren einwandfrei, 17 ist zum Zeitpunkt des Schreibens einfach die aktuellere Version. POSTGRES_USER wird bewusst nicht gesetzt, was Postgres standardmäßig auf einen postgres-User bringt, passend zu DB_USER=postgres in .env, ohne es zu wiederholen. Nur Caddy veröffentlicht Ports auf den Host; db und web sind nur im internen Docker-Netzwerk erreichbar.

Schritt 5: Caddyfile schreiben und alles starten

Erstelle Caddyfile im selben Verzeichnis:

{$DOMAIN} {
	reverse_proxy web:8000
}

Starte den Stack:

docker compose up -d

Beobachte, wie Caddy ein Zertifikat holt:

docker compose logs -f caddy

Du solltest eine Zeile über ein erhaltenes Zertifikat für deine Domain sehen, danach sollte https://checks.yourdomain.com mit einem gültigen Schloss und ohne Warnung laden.

Schritt 6: Erststart-Einrichtung und Registrierung schließen

Wenn du deine Domain zum ersten Mal besuchst, siehst du Healthchecks’ eigene Registrierungsseite. Es gibt hier keinen separaten CLI-Schritt, um einen Admin-Account zu erstellen; der erste Account, den du registrierst, ist der Account, mit dem du künftig Checks, Projekte und Integrationen verwaltest. Registriere ihn jetzt, logge dich ein und erstelle deinen ersten Check, um zu bestätigen, dass Pings ihn erreichen.

Sobald dieser Account existiert, stelle sicher, dass REGISTRATION_OPEN=False in .env gesetzt ist (ist es bereits, aus Schritt 3) und deploye den web-Dienst neu, damit die Einstellung wirksam wird, falls du etwas geändert hast:

docker compose up -d web

Von diesem Punkt an zeigt der Besuch der Registrierungsseite auf deiner Instanz eine Meldung über geschlossene Registrierung statt eines funktionierenden Formulars, sodass niemand sonst einen Account auf deinem Server anlegen kann.

Schritt 7: Datenbank und Secret Key sichern

Die Datenbank enthält jedes Projekt, jeden Check, jede Integration und jede Ping-Historie, die du hast. Ein täglicher Postgres-Dump, außerhalb des Servers aufbewahrt:

docker compose exec -T db pg_dump -U postgres healthchecks > "healthchecks-backup-$(date +%F).sql"

Pack das in einen Cron-Job (ironischerweise einen, den du von Healthchecks selbst überwachen lassen könntest) und kopiere das Ergebnis irgendwohin außerhalb dieses Servers, denn ein Backup, das nur auf dem Server lebt, den es eigentlich sichert, überlebt nicht den Ausfallmodus, der einen Server tatsächlich zerstört.

Sichere auch .env, irgendwo separat und zugriffsgeschützt aufbewahrt. Ihr SECRET_KEY signiert jede Session, die Healthchecks ausstellt; ihn zu verlieren bedeutet nur, dass sich alle neu einloggen müssen, was eine kleine Unannehmlichkeit ist, aber eine durchgesickerte Kopie davon ist ein Weg für jemand anderen, eine Session zu fälschen, also behandle ihn mit derselben Sorgfalt wie das Datenbank-Passwort.

Schritt 8: Saubere Upgrades

Prüfe die Release Notes im GitHub-Repository des Projekts zwischen dem Tag, den du gerade betreibst, und dem, zu dem du wechselst, und erhöhe dann die Version in docker-compose.yml:

image: healthchecks/healthchecks:<new-version>

Ziehe und starte nur diesen einen Dienst neu:

docker compose pull web
docker compose up -d web

Prüfe die Logs auf einen sauberen Start und einen erfolgreichen Migrationsschritt:

docker compose logs web

Vom vorgebauten Image wird erwartet, dass es alle anstehenden Datenbankmigrationen automatisch als Teil des eigenen Starts ausführt, also ist ein sauberer Logausgang ohne Migrationsfehler das, worauf du achten solltest; falls doch etwas fehlschlägt, ist dein Postgres-Backup aus Schritt 7 der Weg zurück zur vorherigen Version.

Der Shortcut: mit Railyard

Alles oben, eine Postgres-gestützte Healthchecks-Instanz mit generiertem Secret Key und schon verdrahtetem HTTPS, ist auch eine Ein-Klick-Vorlage:

https://app.railyard.run/deploy?template=healthchecks

Railyard baut sie direkt aus dem docker/Dockerfile im eigenen Repository des Projekts, stellt automatisch eine verwaltete Postgres-Datenbank dafür bereit und führt den eigenen Migrationsbefehl des Projekts als Release-Schritt vor jedem Deploy aus, dasselbe python manage.py migrate, das du sonst von Hand ausführen würdest. Es setzt auch ALLOWED_HOSTS=* und generiert einen echten SECRET_KEY für dich, also musst du kein Secret selbst erfinden und einfügen.

Falls du eher allgemein mit Railyard starten willst, bevor du dich konkret für Healthchecks entscheidest: der Quickstart deckt die Grundlagen ab, Deploy-Button erklärt das ?template=-Link-Format, falls du eins anderswo einbetten willst, und Datenbanken und Backups deckt ab, wie Backups generell über Vorlagen funktionieren, nicht nur bei dieser. Railyards Free-Plan deckt einen Server plus einen Datenbankserver und drei Apps ohne Kreditkarte ab, Pro ist 19 $/Monat pro Team, und Team ist 49 $/Monat pro Team.

Troubleshooting

Alert-Mails kommen nie an. Das sind fast immer fehlende oder falsche SMTP-Einstellungen, kein Healthchecks-Bug. Prüfe, dass EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_PORT und EMAIL_USE_TLS alle in .env gesetzt sind und dass der beschriebene Account tatsächlich Mails verschicken kann; viele Provider blockieren den ersten Versand von einem neuen Server oder einer neuen IP, bis du sie verifizierst. Ein Monitoring-Tool, dessen eigener Alert-Pfad kaputt ist, scheitert still genau an dem einen Job, für den es existiert, also schick dir selbst einen Test-Check-in-Fehler und bestätige, dass die Mail tatsächlich ankommt, bevor du dich für etwas Reales darauf verlässt.

In der UI angezeigte Ping-URLs zeigen auf die falsche Domain. SITE_ROOT stimmt nicht mit der echten HTTPS-Adresse überein, von der du die App bedienst. Korrigiere es in .env und deploye web neu; jede Ping-URL, die Healthchecks dir bereits gezeigt oder gemailt hat, wurde aus dem alten Wert gebaut und muss nach der Korrektur neu aus der UI kopiert werden.

Bad Request (400) beim Laden der Seite. ALLOWED_HOSTS enthält die Domain nicht, die du besuchst. Das Compose-Setup hier setzt es über die eigene Standardbehandlung des Projekts auf *, das taucht also normalerweise nur auf, wenn du dieses Verhalten geändert hast; prüfe, ob es zu deiner echten Domain passt, falls du das getan hast.

Postgres-Verbindung abgelehnt, oder der web-Container startet immer wieder neu. Prüfe zuerst docker compose logs db auf den tatsächlichen Fehler. Ein nicht übereinstimmendes Passwort zwischen DB_PASSWORD in .env und dem, womit Postgres initialisiert wurde, ist die häufigste Ursache, meist davon, .env zu bearbeiten, nachdem das db-data-Volume schon existiert; Postgres liest POSTGRES_PASSWORD nur beim allerersten Start mit leerem Datenverzeichnis, also braucht eine spätere Änderung entweder ein frisches Volume oder eine Passwortänderung in der Datenbank selbst.

HTTPS kommt nie, oder der Browser zeigt eine Zertifikatswarnung. Führe dig checks.yourdomain.com +short aus und bestätige, dass die IP dieses Servers zurückkommt. Let’s Encrypt braucht für den Validierungsschritt auch Port 80 aus dem Internet erreichbar, also prüfe, dass ufw status sowohl 80 als auch 443 offen zeigt, nicht nur 443.

FAQ

Brauche ich Postgres, oder funktioniert SQLite? Healthchecks unterstützt SQLite für kleine, wenig frequentierte Installationen, aber Postgres ist der bessere Standard, sobald mehr als eine oder zwei Personen davon abhängen, dass diese Instanz läuft, da es gleichzeitige Schreibvorgänge und größere Ping-Historien eleganter handhabt. Diese Anleitung nutzt deshalb von Anfang an Postgres.

Wie zeige ich einen Cron-Job tatsächlich auf diese Instanz, sobald sie läuft? Hänge einen curl-Aufruf ans Ende des Jobs selbst, nach dem Befehl, den du überwachst, falls er erfolgreich war:

your-backup-script.sh && curl -fsS -m 10 --retry 5 https://checks.yourdomain.com/ping/<uuid>

Jeder Check in der UI gibt dir seine eigene eindeutige Ping-URL zum Kopieren in den Job. Wenn das Skript fehlschlägt, bevor es diese Zeile erreicht, kommt kein Ping an, und genau das ist das Signal, auf das Healthchecks wartet.

Was ist der echte Unterschied zwischen diesem Setup und dem gehosteten healthchecks.io? Funktional sehr wenig; es ist in beiden Fällen dieselbe Open-Source-Anwendung. Die gehostete Version wird für dich gewartet und monatlich abgerechnet, sobald du über die kostenlose Stufe hinaus bist; eine selbst gehostete Instanz kostet nur den Server, auf dem sie läuft, ohne Limit bei der Anzahl der Checks, aber du bist derjenige, der sie gepatcht und gesichert hält.

Sollte REGISTRATION_OPEN wirklich geschlossen sein? Ja, sobald dein eigener Account existiert. Es gibt keinen Grund, warum eine selbst gehostete Monitoring-Instanz auf deiner eigenen Domain Registrierungen von jedem akzeptieren sollte, der die URL zufällig findet, und sie offen zu lassen ist die eine Einstellung in dieser ganzen Anleitung, die wirklich ein Fehler ist, wenn man sie überspringt.

Wie viel kostet der Betrieb? Der Server kostet 5,99 €/Monat exkl. MwSt. Healthchecks selbst ist freie, Open-Source-Software ohne Gebühr pro Check oder Integration; du zahlst für die Box, auf der es läuft, nicht für das, was darauf läuft.

Ein Cron-Job, der laut fehlschlägt, fällt leicht auf. Der, der still fehlschlägt, das Backup, das vor drei Wochen aufgehört hat, die Zertifikatserneuerung, die still aufgehört hat zu feuern, dafür ist das hier eigentlich gedacht, und jetzt hast du deine eigene Instanz, die genau darauf achtet.

Warum Leute sich dafür Railyard entscheiden

  • Dein eigenes Cloud-Konto. Deploye auf DigitalOcean, Hetzner, Vultr, AWS, OVHcloud oder jedem Linux-Server per SSH, direkt von deinem Anbieter abgerechnet, ohne Aufschlag.
  • Ein fester Preis pro Team. Free ist für immer kostenlos: ein Server plus ein Datenbankserver, drei Apps, keine Karte. Pro kostet 19 $ im Monat für bis zu fünf Server und unbegrenzte Apps. Team kostet 49 $ im Monat mit unbegrenzten Servern. Keine Kosten pro App, pro Deployment, pro Sitz oder pro Gigabyte.
  • Templates mit einem Klick. Deploye Open-Source-Apps, wie Datenbanken, Analytics und Dev-Tools, mit einem Klick auf deinem eigenen Server.
  • Verwaltete Datenbanken. Postgres, MySQL und Redis mit geplanten, wiederherstellungsgeprüften Backups und Point-in-Time-Recovery.
  • Push-to-Deploy. Deploye von GitHub, GitLab oder der CLI, mit automatischem HTTPS und Releases ohne Ausfallzeit.

Überspring das Server-Setup

Starte die verifizierte Healthchecks-Vorlage auf Railyard mit einem Klick und bekomme eine laufende Instanz mit Postgres und HTTPS, die schon verdrahtet sind.

Kostenloser Plan, keine Kreditkarte, und 14 Tage Pro für jedes neue Team. Ein fester Preis pro Team.