SSD Nodes Learn 8GB RAM — $66/Jahr
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-08-01

Docker Compose Healthchecks richtig schreiben

Erfahren Sie, wie Docker Compose Healthchecks bewertet, warum depends_on allein nichts Nutzbares abwartet und wie Readiness-Checks für Postgres und Ihre App funktionieren.

Was ein Docker Compose-Healthcheck tatsächlich tut

Ein Docker-Compose-Healthcheck ist ein einzelner Befehl, den Docker im Container nach einem Zeitplan ausführt. Docker liest Ihre Logs nicht, überwacht Ihren Port nicht und prüft Ihre Prozessliste nicht. Der Befehl wird ausgeführt, der Exit-Code wird gelesen, und Docker speichert einen einzelnen Status für den Container: starting, healthy oder unhealthy. Der Exit-Code 0 bedeutet gesund. Jeder andere Exit-Code bedeutet nicht gesund. Der Exit-Code 2 ist für Docker reserviert. Geben Sie ihn daher niemals absichtlich zurück.

Das ist der gesamte Mechanismus. Fast jedes Problem mit Healthchecks ist dasselbe Problem: Der von Ihnen geschriebene Befehl beantwortet eine andere Frage als die, die Sie beantworten wollten. Dieser Leitfaden setzt voraus, dass Sie bereits wissen, wie Sie eine Compose-Datei auf einem VPS schreiben, und setzt an dem Punkt an, an dem der Stack in der falschen Reihenfolge gestartet wird.

services:
  api:
    image: ghcr.io/example/api:1.4.0
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s

Der Wert test hat zwei nützliche Formen. Eine Liste, die mit CMD beginnt, führt den Befehl direkt und ohne Shell aus. Daher funktionieren Pipes, && und die Variablenexpansion nicht. Eine Liste, die mit CMD-SHELL beginnt, übergibt den Rest als eine Zeichenfolge an /bin/sh -c im Container. Das benötigen Sie, sobald der Check Shell-Syntax verwendet. Eine einfache Zeichenfolge wird als CMD-SHELL behandelt. Eine Liste, die genau aus ["NONE"] besteht, entfernt einen Healthcheck, den das Image über seine Dockerfile vorgibt.

Der Check läuft im Container. Daher muss jede darin genannte Binärdatei in diesem Image vorhanden sein. Prüfen Sie das zuerst. Ein schlankes Image ohne curl erzeugt sonst einen Container, der dauerhaft den Status nicht gesund hat. Der Grund dafür erscheint nicht im Anwendungslog. Testen Sie den Check manuell:

docker compose exec api curl --version

Eine fehlende Binärdatei führt zu OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. Auf Alpine basierende Images enthalten stattdessen normalerweise BusyBox wget. Der Check wird dann zu ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].

So wirken interval, retries und start_period zusammen

Fünf Einstellungen steuern das Timing. Ihre Standardwerte stammen aus Docker Engine, nicht aus Compose.

  • interval: Zeit zwischen zwei Prüfungen, sobald der Container seine Startphase beendet hat. Standardwert: 30s.
  • timeout: Maximale Dauer eines Prüflaufs, bevor Docker ihn beendet und diesen Lauf als Fehler zählt. Standardwert: 30s.
  • retries: Anzahl aufeinanderfolgender Fehler, bevor der Status auf unhealthy wechselt. Standardwert: 3.
  • start_period: Kulanzzeit nach dem Start des Containers. Standardwert: 0s.
  • start_interval: Häufigkeit der Prüfungen während der Startphase. Standardwert: 5s. Dafür ist Docker Engine 25.0 oder neuer erforderlich.

Die entscheidende Regel lautet: Während der Startphase zählt eine fehlschlagende Prüfung nicht für retries, und der Container bleibt in starting. Sobald die Prüfung erstmals erfolgreich ist, wechselt der Container zu healthy, und die Startphase endet sofort, auch wenn der größte Teil ihrer Zeit noch nicht abgelaufen ist. Wenn die Startphase endet, während die Prüfung weiterhin fehlschlägt, beginnt der normale Zähler. Der Container benötigt dann retries aufeinanderfolgende Fehler, bevor er als unhealthy markiert wird.

Die maximale Zeit vom Start des Containers bis zu unhealthy beträgt daher start_period plus retries multipliziert mit interval plus timeout. Mit den Werten aus der obigen Datei sind das 30 plus 5 mal 13, also 95 Sekunden. Notieren Sie diese Zahl, bevor Sie ein Bereitstellungs-Timeout festlegen. Ein Rollout, der nach 60 Sekunden abbricht, wird nie beobachten, dass dieser Container einen endgültigen Status erreicht.

Der häufigste Fehler besteht darin, retries zu erhöhen, um einen langsamen Start abzudecken. Das funktioniert zunächst, führt aber dauerhaft zu einem Nachteil: Ein Dienst, der zum Starten 8 Wiederholungen benötigte, toleriert in der Produktion nun 8 aufeinanderfolgende Fehler, bevor etwas darauf reagiert. Verwenden Sie stattdessen start_period, da diese Einstellung nur vor dem ersten erfolgreichen Prüflauf gilt.

Warum depends_on allein nichts garantiert

Die Kurzform von depends_on ist die häufigste Ursache für Verwirrung.

  api:
    depends_on:
      - db

Das bedeutet nur: Starten Sie den Container db vor dem Container api. Compose wartet, bis der Container erstellt und gestartet wurde. Es wartet nicht, bis PostgreSQL seine Initialisierung beim ersten Start abgeschlossen hat. Es wartet auch nicht, bis Port 5432 Verbindungen akzeptiert. Ihre Anwendung startet etwa eine Sekunde später, verbindet sich mit einem Port, an dem noch nichts lauscht, und wird beendet. Im Log sehen Sie Connection refused oder FATAL: the database system is starting up, wenn der Server bereits läuft, aber noch wiederhergestellt wird.

Die Langform ist das, was in der Praxis benötigt wird:

  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      migrate:
        condition: service_completed_successfully

condition hat drei Werte. service_started entspricht der Kurzform. service_healthy hält den abhängigen Service zurück, bis die Abhängigkeit den Status „healthy“ meldet. Das ist nur sinnvoll, wenn die Abhängigkeit einen healthcheck definiert, entweder in der Compose-Datei oder in ihrem Image. service_completed_successfully wartet bei einem Container mit einmaliger Ausführung, etwa einer Datenbankmigration, bis dieser mit Status 0 beendet wird.

Neben condition gibt es zwei weitere Felder. restart: true weist Compose an, diesen Service neu zu starten, nachdem der abhängige Service aktualisiert wurde. required: false stuft eine fehlende Abhängigkeit von einem Fehler zu einer Warnung herab.

Nun zur Einschränkung, die häufig übersehen wird. Diese Bedingungen werden ausgewertet, wenn der Stack gestartet wird. Sie legen die Startreihenfolge fest, sind aber keine Überwachungsregel. Wenn die Datenbank um drei Uhr morgens neu startet, werden service_healthy nicht erneut ausgewertet und Ihre Anwendung wird nicht erneut gestartet, um die Bedingung wieder zu erfüllen. Ihr Anwendungscode muss die Verbindung weiterhin selbstständig wiederherstellen. docker compose up --no-deps api umgeht diesen Mechanismus absichtlich. Das gilt auch, wenn Sie einen Container direkt mit docker start starten.

Schreiben Sie eine Prüfung, die die Bereitschaft testet, nicht nur die Existenz eines Prozesses

Eine Prüfung wie pgrep nginx weist nach, dass ein Eintrag in der Prozesstabelle vorhanden ist. Sie sagt nichts darüber aus, ob der Dienst eine Anfrage beantworten kann. Eine Webanwendung kann ihren Listening-Socket noch lange offen halten, nachdem ihr Datenbankpool ausgefallen ist. Die Prozessprüfung bleibt während des gesamten Ausfalls erfolgreich.

Lassen Sie den Container die Aufgabe ausführen, für die er vorgesehen ist:

  • Fordern Sie bei einem HTTP-Dienst einen echten Endpunkt an. curl -fsS beendet sich bei jedem Status von 400 oder höher mit einem Fehler, weil -f verwendet wird. Daher gilt auch ein 500-Fehler einer defekten Anwendung als fehlgeschlagene Prüfung.
  • Verwenden Sie für PostgreSQL pg_isready. Das Programm beendet sich mit 0, wenn der Server Verbindungen annimmt, mit 1, wenn er sie ablehnt, mit 2, wenn er überhaupt nicht antwortet, und mit 3, wenn die übergebenen Parameter falsch waren.
  • Verwenden Sie für Redis redis-cli ping. Das Programm gibt PONG aus und beendet sich mit 0.
  • Für MariaDB enthält das offizielle Image ein healthcheck.sh-Skript. healthcheck.sh --connect --innodb_initialized ist die von den Maintainer dokumentierte Form.

pg_isready enthält eine wichtige Besonderheit. Beim allerersten Start mit einem leeren Datenverzeichnis führt das offizielle postgres-Image seine Initialisierung gegen einen temporären Server aus, der nur am Unix-Socket lauscht. pg_isready ohne Hostargument verwendet diesen Socket. Daher kann der Befehl „Verbindungen werden angenommen“ melden, während TCP-Port 5432 für Ihre Anwendung noch geschlossen ist. Richten Sie die Prüfung ausdrücklich auf TCP. Damit verschwindet das Problem, weil der temporäre Server dort nicht antwortet.

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

Die doppelten Dollarzeichen sind kein Tippfehler. Compose expandiert $VAR selbst beim Lesen der Datei. Dadurch würde ein Wert aus der Umgebung Ihres Hosts in die Prüfung übernommen. $$ maskiert es zu einem einzelnen $, sodass die Shell im Container den Wert anhand der eigenen Container-Umgebung expandiert.

Eine PostgreSQL- und App-Stack, der in der richtigen Reihenfolge startet

services:
  db:
    image: postgres:17.5
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s
    restart: unless-stopped

  api:
    image: ghcr.io/example/api:1.4.0
    environment:
      DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

volumes:
  pgdata:

Starten Sie ihn und beobachten Sie, wie sich die Zustände ändern:

docker compose up -d
docker compose ps

Die Spalte STATUS enthält den Gesundheitszustand in eckigen Klammern. Ein gesundes Paar zeigt in beiden Zeilen Up 41 seconds (healthy) an. Während die Datenbank noch initialisiert wird, zeigt db den Wert Up 4 seconds (health: starting) an. api fehlt in der Liste, weil Compose den Dienst noch nicht erstellt hat.

Um festzustellen, warum eine Prüfung erfolgreich war oder fehlgeschlagen ist, lesen Sie das Gesundheitsprotokoll:

docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"

Docker speichert die letzten Ergebnisse. Jedes Ergebnis enthält eine Startzeit, eine Endzeit, einen ExitCode und die Output des Befehls. Die gespeicherte Ausgabe wird gekürzt. Eine Prüfung, die den Inhalt einer großen Seite ausgibt, erzeugt daher einen unbrauchbaren Protokolleintrag. Halten Sie Prüfungen kurz und ausgabearm.

Was Docker tut, wenn ein Container den Status „unhealthy“ erhält

Nichts. Das ist die Antwort, die die meisten Menschen überrascht.

Docker Engine auf einem einzelnen Host startet einen Container mit dem Status „unhealthy“ nicht neu. Die restart: unless-stopped-Richtlinie reagiert darauf, dass der Hauptprozess beendet wird. Ein Container mit dem Status „unhealthy“ wurde jedoch nicht beendet. Er kann eine Woche lang den Status unhealthy behalten, während Compose ihn unverändert lässt. Der Swarm-Modus ersetzt fehlerhafte Tasks. Ein einfacher Compose-Stack auf einem Server tut das jedoch nicht.

Damit bleiben zwei sinnvolle Optionen. Lassen Sie den Prozess sich beenden, sobald er erkennt, dass er fehlerhaft ist. Dann kann die Neustartrichtlinie reagieren. Oder überwachen Sie den Status von außen und lösen Sie bei diesem Status einen Alarm aus. Wenn Sie einen Uptime-Kuma-Monitor auf denselben Endpunkt richten, den auch der Healthcheck aufruft, wird eine fehlerhafte Abhängigkeit an beiden Stellen angezeigt. Sie erfahren davon dann über den Monitor und nicht erst durch einen Benutzer. Wenn der Netzwerkverkehr die Anwendung über einen Traefik-Reverse-Proxy erreicht, beachten Sie, dass die eigene Sicht des Proxy auf ein Backend unabhängig vom Docker-Health-Status ist. Der eine Mechanismus ersetzt daher nicht den anderen.

Fehlersuche bei einem Check, der nie den Status „healthy“ erreicht

Führen Sie den exakten Befehl selbst im selben Container aus und prüfen Sie den Exit-Code:

docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"

exit=0 an dieser Stelle, während der Container weiterhin den Status „unhealthy“ meldet, bedeutet, dass sich Ihre Compose-test von Ihrer gerade eingegebenen Konfiguration unterscheidet. Meist wurde CMD verwendet, obwohl Shell-Syntax erforderlich war.

Für die meisten übrigen Fälle sind zwei Fehler verantwortlich. Der erste ist der falsche Port. Der Healthcheck läuft innerhalb des Containers. Deshalb muss er den Container-Port verwenden, niemals den veröffentlichten Host-Port. Mit ports: - "8080:3000" lauscht die Anwendung an Port 3000. Eine Prüfung gegen http://localhost:8080 schlägt dann dauerhaft fehl, obwohl die Website im Browser problemlos funktioniert. Der zweite ist der falsche Host. Innerhalb des Checks bezeichnet localhost denselben Container. Das ist richtig, wenn der Container sich selbst prüft, aber falsch, wenn ein anderer Container geprüft werden soll. In diesem Fall benötigen Sie den Servicenamen, zum Beispiel db.

Ein letzter Fall verdient eine eigene Bezeichnung: Der Healthcheck ist erfolgreich, während Benutzer Fehler sehen. Das geschieht, wenn der Endpunkt einen statischen Status 200 zurückgibt, ohne etwas Reales zu prüfen. Ein Readiness-Endpunkt, der niemals die Datenbank abfragt, kann nicht feststellen, dass die Datenbank ausgefallen ist. Lassen Sie ihn eine kostengünstige, echte Abfrage ausführen.

FAQ

Warum kann meine Anwendung weiterhin keine Verbindung herstellen, obwohl depends_on meldet, dass die Datenbank fehlerfrei ist?

Weil condition: service_healthy einmal ausgewertet wird, wenn der Stack gestartet wird. Danach überwacht es nichts mehr. Wenn der Datenbankcontainer später neu gestartet wird, startet Compose Ihre Anwendung nicht erneut, um die Bedingung wieder zu erfüllen. Daher benötigt Ihr Anwendungscode eine eigene Logik für Wiederverbindungen und erneute Versuche. Die Bedingung hat außerdem keine Wirkung, wenn Sie einen einzelnen Container mit docker start oder docker compose up --no-deps starten.

Benötige ich einen healthcheck, wenn das Image bereits einen definiert?

In der Regel nicht. Das Überschreiben ist oft ein Rückschritt, weil der Maintainer des Images weiß, was Bereitschaft für diese Software bedeutet. Fügen Sie nur dann einen eigenen hinzu, wenn die Prüfung des Images für Ihre Umgebung nicht geeignet ist, zum Beispiel wenn sie einen Port prüft, den Sie geändert haben. Um einen healthcheck des Images zu deaktivieren, setzen Sie beim Service test: ["NONE"] oder disable: true.

Sollte der healthcheck curl oder wget verwenden?

Verwenden Sie das Programm, das bereits im Image vorhanden ist, und prüfen Sie dies mit docker compose exec <service> curl --version, bevor Sie sich darauf verlassen. Viele Debian-basierte Images enthalten keines von beiden. Alpine-basierte Images enthalten BusyBox wget. Fügen Sie einem Image nicht nur für einen healthcheck ein Paket hinzu, wenn die Software ihren eigenen Client bereitstellt, etwa pg_isready oder redis-cli.

Wird ein fehlerhafter Container automatisch neu gestartet?

Nicht durch Docker Engine auf einem einzelnen Host. Restart-Richtlinien reagieren darauf, dass der Prozess beendet wird, nicht auf den Health-Status. Daher bleibt ein fehlerhafter Container aktiv und defekt, bis etwas anderes eingreift. Lassen Sie entweder den Prozess beenden, sobald er den Fehler erkennt, oder führen Sie einen externen Monitor aus, der bei diesem Status alarmiert.

Wie lang sollte start_period sein?

Lang genug für den langsamsten von Ihnen gemessenen gültigen ersten Start, zuzüglich eines Puffers. Messen Sie die Zeit mit docker compose up bei einem leeren Volume, da der erste Start einer Datenbank deutlich länger dauert als alle folgenden Starts. Ein zu langer Startzeitraum verzögert nur das erste unhealthy-Ergebnis. Zu viele Wiederholungen schwächen die Prüfung während der gesamten Lebensdauer des Containers. Das ist der schwerwiegendere Fehler.

#docker-compose#healthcheck#depends-on#docker#reliability