Journal-IndexDockup / Feldnotiz
Note / health-check-failing-deployment

Health Check schlägt fehl, aber die App funktioniert

Wenn ein Health Check beim Deployment fehlschlägt, die App lokal aber problemlos läuft, sind meist fünf Ursachen verantwortlich. Prüfe Bind-Adresse, Pfad, Port, Timing und Abhängigkeiten in der Reihenfolge, in der du das Problem am schnellsten findest.

Es gibt eine ganz besondere Form des Feststeckens: Ein fehlschlagender Health Check beim Deployment blockiert jedes Release, obwohl die Anwendung nach allen erreichbaren Maßstäben völlig in Ordnung ist. Sie läuft lokal. Sie läuft lokal in Docker. Die Logs zeigen, dass sie Verbindungen entgegennimmt. Und die Plattform meldet einen Fehler nach dem anderen, manchmal zwanzig am Stück, ohne dass jemals eine Anfrage in deinem Access-Log auftaucht.

Dieses letzte Detail ist entscheidend und grenzt die möglichen Ursachen sofort ein. Wenn deine Anwendung die Anfrage nie protokolliert hat, hat der Check deine Anwendung nicht erreicht — dann kann auch nichts in deinem Anwendungscode die Ursache erklären.

Hier sind die fünf Ursachen, geordnet nach der Reihenfolge, in der du das Problem am schnellsten findest.

1. Du bindest an localhost

Das ist die mit Abstand häufigste Ursache und erklärt das Symptom „Es kommt überhaupt kein Traffic an“ exakt.

Innerhalb eines Containers bezeichnet 127.0.0.1 das Loopback-Interface dieses Containers selbst. Ein Health Check, der von außerhalb des Containers kommt, kann es nicht erreichen. Der Prozess lauscht, deine Logs bestätigen das, und der Socket ist von keinem relevanten Ort aus erreichbar.

// Unreachable from outside the container
app.listen(3000, '127.0.0.1')

// Correct
app.listen(3000, '0.0.0.0')

Frameworks verwenden unterschiedliche Standardwerte, und bei einigen wurde der Standardwert zwischen Major-Versionen geändert. Prüfe, woran dein Framework tatsächlich bindet, statt dich auf deine Erinnerung zu verlassen.

# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api

Wenn die Listening-Adresse 127.0.0.1:3000 statt 0.0.0.0:3000 lautet, hast du die Ursache gefunden — alles andere auf dieser Liste ist dann irrelevant.

2. Der von der Plattform geprüfte Port ist nicht der Port, auf dem du den Service bereitstellst

Hier sind zwei Ports beteiligt, die leicht verwechselt werden: der Port, auf dem dein Prozess innerhalb des Containers lauscht, und der Port, an den die Plattform weiterleitet. Wenn deine App PORT aus der Umgebung liest, du aber an irgendeiner Stelle in einem Dockerfile 3000 fest codiert hast, können diese beiden Werte unbemerkt voneinander abweichen.

Das zuverlässige Muster sieht so aus:

const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')

Lege den Port des Service anschließend einmalig in der Plattform fest und verwalte die Nummer nicht mehr an zwei Stellen.

3. Der Pfad liefert etwas anderes als einen Erfolgsstatus

Ein Health-Check-Pfad wird exakt abgeglichen, und überraschend viele Fehler sind tatsächlich Redirects. Wenn deine App /healthz zu /healthz/ umleitet oder HTTPS per 301 erzwingt, schlägt ein Checker, der nur 2xx als Erfolg akzeptiert, jedes Mal fehl, während ein Browser dem Redirect folgt und dir eine funktionierende Seite anzeigt.

Drei konkrete Stolperfallen:

  • Redirects wegen nachgestelltem Schrägstrich. /healthz/healthz/ ist ein 301.
  • Erzwungenes HTTPS. Der interne Check kommt normalerweise über unverschlüsseltes HTTP auf dem Loopback-Interface an. Eine bedingungslose HTTPS-Weiterleitung lässt ihn fehlschlagen.
  • Auth-Middleware. Ein globaler Authentifizierungs-Guard, der vor dem Routing ausgeführt wird, liefert auch für den Health-Pfad 401 zurück.

Schließe den Health-Pfad ausdrücklich von Authentifizierung und der Erzwingung von HTTPS aus. Das ist die eine Route, die langweilig sein sollte.

4. Der Check ist schneller als dein Cold Start

Wenn der Check einige Male fehlschlägt und anschließend erfolgreich ist oder beim Deployment fehlschlägt, bei einem erneuten Versuch aber funktioniert, handelt es sich um ein Timing- und kein Konfigurationsproblem.

Das benötigte Zeitbudget ist nicht ein einzelner Versuch, sondern Intervall × Wiederholungen. Eine Anwendung, die zwölf Sekunden braucht, um eine Verbindung zu ihrer Datenbank herzustellen und einen Cache aufzuwärmen, benötigt ein Gesamtbudget von mehr als zwölf Sekunden. Andernfalls schlägt jedes Release fehl und du deaktivierst irgendwann die Prüfung — damit entfernst du die einzige Barriere zwischen einem fehlerhaften Build und deinen Benutzern.

dockup info my-project/my-api --json | grep -A6 healthCheck

Setze den Timeout höher als deinen langsamsten legitimen Einzelversuch und wähle die Anzahl der Wiederholungen so, dass Intervall × Wiederholungen deinen langsamsten legitimen Start deutlich übersteigt. Miss die Startzeit, statt sie zu schätzen — die Logs enthalten Zeitstempel.

5. Die Anwendung ist tatsächlich noch nicht bereit

Das ist der letzte Fall und genau der Grund, warum es den Check gibt: Deine App wurde gestartet, konnte eine Abhängigkeit nicht erreichen und versucht es erneut. Sie ist nicht abgestürzt, also wird nichts neu gestartet. Sie kann keine Anfragen bedienen, daher schlägt der Check fehl. Das System funktioniert genau wie vorgesehen und teilt dir mit, dass dieses Release keinen Traffic erhalten sollte.

Der Unterschied zu den anderen vier Ursachen besteht darin, dass deine Anwendung die Anfrage protokolliert und mit einem Status ungleich 2xx antwortet. Wenn die Anfrage in deinen Logs auftaucht, sind die Ursachen 1 bis 3 ausgeschlossen.

Die Diagnose-Reihenfolge, die Zeit spart

# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow

# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api

# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api

# 4. What is the gate configured to expect?
dockup info my-project/my-api --json

Schritt 3 löst die meisten dieser Fälle. Ein curl aus dem Container heraus entfernt alle Netzwerkvariablen auf einmal: Liefert er dort 200 und schlägt die Prüfung auf der Plattform trotzdem fehl, liegt das Problem bei der Adresse oder dem Port, nicht bei der App. Liefert er 301 oder 401, hast du die Ursache gefunden, ohne die Plattform überhaupt anzufassen.

Warum es sich lohnt, die Prüfung beizubehalten

Nach dem vierten fehlgeschlagenen Deployment ist es verlockend, den Health Check zu deaktivieren und das Release endlich auszurollen. Es lohnt sich, daran zu denken, was du damit abschaltest.

Auf Dockup ist das Health Gate die Instanz, die ein fehlerhaftes Release von deinen Benutzern fernhält. Die neue Version wird erstellt und gestartet, während die aktuelle Version weiterhin Traffic bedient. Erst wenn die neue Version antwortet, wird der Traffic umgeleitet. Deaktivierst du das Gate, aktivierst du wieder den Fehlerfall, bei dem ein Container, der zwar startet, aber nicht funktioniert, einen fehlerfreien Container ersetzt.

Ein Check, der vier Releases hintereinander fehlschlägt, ist ärgerlich. Ein Check, der bedingungslos erfolgreich ist, wird das eine Deployment, auf das es ankommt, nicht stoppen.

Häufig gestellte Fragen

Warum schlägt der Health Check fehl, obwohl die App lokal funktioniert? Fast immer, weil der Container an 127.0.0.1 statt an 0.0.0.0 bindet. Lokal stellst du die Verbindung über dasselbe Loopback-Interface her; von außerhalb des Containers ist diese Adresse nicht erreichbar.

Sollte der Health-Endpunkt eine Authentifizierung erfordern? Nein. Schließe ihn von der globalen Auth-Middleware aus, sonst erhält der Checker 401 und das Deployment schlägt fehl, obwohl die App funktioniert.

Welchen Timeout sollte ich verwenden? Er sollte länger sein als dein langsamster legitimer Einzelversuch, und die Wiederholungen sollten deinen langsamsten legitimen Cold Start abdecken. Ermittle die Startzeit aus deinen Logs, statt sie zu schätzen.

Ist es sicher, den Health Check zu deaktivieren, um ein Release freizugeben? Das Release wird dadurch freigegeben, aber gleichzeitig entfernst du den Schutz, der verhindert, dass eine fehlerhafte Version Traffic erhält. Behebe stattdessen den Check — in den meisten Fällen liegt die Ursache bei der Bind-Adresse oder einem Redirect und ist in wenigen Minuten behoben.