Kaputten Docker-Compose-Stack debuggen
Eine feste Reihenfolge fuer einen Docker-Compose-Dienst, der nicht startet: docker compose ps, logs --tail, der Exit-Code und der Healthcheck-Status, in dieser Reihenfolge, bevor Sie irgendetwas neu starten, plus wie Sie einen echten Crash-Loop von einem langsamen ersten Boot unterscheiden.

docker compose ps zeigte jeden Container als "running" an. Die App selbst war nicht erreichbar — Verbindung abgelehnt, keine Fehlerseite, nichts. Der Reflex war, den Stack neu zu starten und zu sehen, ob sich das Problem von selbst löst. Ein Neustart zuerst hätte genau den einen Beweis vernichtet, der eigentlich erklärt hätte, was falsch lief.
TL;DR
- Niemals einen kaputten Dienst neu starten, bevor die Logs gelesen wurden — ein Neustart löscht genau die Beweise, die den Absturz erklären.
- Die Reihenfolge ist fest:
docker compose psfür den Zustand,logs --tailfür die Historie, der Exit-Code für die Fehlerkategorie, dann derhealthcheck-Status, falls definiert. logs -fist für einen Fehler, der gerade passiert;logs --tail=Nist für einen Dienst, der bereits abgestürzt ist und Historie braucht, ohne das Terminal zu fluten.- Exit-Code 137 bedeutet fast immer, dass der Prozess getötet wurde — meist durch den OOM-Killer des Kernels, nicht durch einen Bug in der App selbst.
- Ein Crash-Loop und ein langsamer erster Boot sehen in den ersten zehn Sekunden identisch aus; der Restart-Zähler in der nächsten Minute unterscheidet die beiden.
KEY-STAT: 137 — Der Exit-Code, der bedeutet, dass ein Container getötet wurde, nicht dass er von selbst abgestürzt ist — prüfen Sie docker inspect auf OOMKilled: true, bevor Sie einen Bug in der App vermuten
Die Reihenfolge beim Debuggen, bevor Sie irgendetwas anfassen
docker compose ps— bestätigt den tatsächlichen Zustand und den Restart-Zähler. Ein als "running" gelisteter Container kann trotzdem seinen eigenen Healthcheck nicht bestehen; ein hoher Restart-Zähler bedeutet, dass Compose bereits versucht hat, das Problem für Sie zu beheben, und gescheitert ist.docker compose logs --tail=100 <Dienst>— lesen Sie, was der Container tatsächlich gesagt hat, bevor er starb, ohne die gesamte Historie durchzuscrollen.- Der Exit-Code —
docker compose ps -aoderdocker inspect <container> --format='{{.State.ExitCode}}'grenzt den Fehler auf eine Kategorie ein, bevor Sie auch nur eine weitere Zeile lesen. - Der Healthcheck-Status, falls der Dienst einen definiert —
docker inspect --format='{{json .State.Health}}' <container>zeigt die letzten mehreren Prüfergebnisse, inklusive der tatsächlichen Ausgabe des Prüfbefehls.
In dieser Reihenfolge vorzugehen, bevor irgendetwas neu gestartet wird, dauert unter zwei Minuten und grenzt das Problem fast immer auf eine von wenigen Kategorien ein — einen Konfigurationsfehler, ein Ressourcenlimit oder eine Abhängigkeit, die noch nicht bereit ist.
Warnung
Einen abgestürzten Container neu zu starten, bevor die Logs gelesen wurden, ist der mit Abstand häufigste Fehler in diesem ganzen Prozess. Der Neustart löscht stdout/stderr des abgestürzten Prozesses — genau die Beweise, die erklären würden, warum er starb — und ersetzt sie durch ein frisches, aussageloses "startet gerade"-Log.
logs -f vs. logs --tail: zwei unterschiedliche Aufgaben
docker compose logs -f <Dienst> folgt der Ausgabe live — das richtige Werkzeug, während ein Fehler gerade aktiv passiert, um einen Dienst in Echtzeit beim Hochfahren zu beobachten. docker compose logs --tail=100 <Dienst> (oder jede beliebige Zahl N) holt die letzten N Zeilen der Historie ohne zu folgen — genau das, was ein bereits abgestürzter und neu gestarteter Dienst tatsächlich braucht: Der Absturz ist schon passiert, es gibt also nichts mehr zu verfolgen, nur Historie zu lesen. -f bei einem bereits toten Container zu nutzen zeigt nur Stille; --tail bei einem laufenden Boot zu nutzen bedeutet, alles Kommende zu verpassen.
Was der Exit-Code tatsächlich eingrenzt
| Exit-Code | Bedeutung | Typische Ursache |
|---|---|---|
0 | Sauberer Exit | Der Prozess hat sich selbst beendet — erwartet bei einmaligen Jobs, unerwartet bei einem dauerhaft laufenden Dienst |
1 | Allgemeiner Anwendungsfehler | Ein Konfigurationsfehler, eine fehlende Umgebungsvariable oder eine unbehandelte Exception in der App selbst |
137 | SIGKILL (128 + 9) | Fast immer der OOM-Killer des Kernels, oder Docker stoppt einen nicht reagierenden Container zwangsweise |
143 | SIGTERM (128 + 128) | Der Prozess wurde gebeten, sich sauber zu beenden — normal bei einem docker compose stop oder down, unerwartet mitten im Betrieb |
docker inspect <container> --format='{{.State.OOMKilled}}' beantwortet die 137-Frage direkt — true bedeutet, dass der Kernel den Prozess wegen zu hohen Speicherverbrauchs getötet hat, was ein Ressourcenlimit-Problem ist, kein Bug, der im Code der App zu suchen wäre.
Einen fehlschlagenden Healthcheck-Block lesen
Ein Dienst mit einem healthcheck:-Block meldet einen von drei Zuständen: starting (noch innerhalb der start_period, Fehler zählen noch nicht), healthy, oder unhealthy (der Prüfbefehl selbst ist mehrfach hintereinander fehlgeschlagen). docker inspect --format='{{json .State.Health}}' <container> gibt die letzten fünf Ergebnisse zurück, inklusive der tatsächlichen stdout-Ausgabe des Prüfbefehls — oft der schnellste Weg herauszufinden, dass der Healthcheck-Befehl den falschen Port oder Pfad testet, nicht dass die App selbst tatsächlich kaputt ist.
Hinweis
Ein Container, der in unhealthy feststeckt, bedeutet nicht zwangsläufig, dass die App kaputt ist. Genauso oft testet ein healthcheck:-Block den falschen Endpunkt, den falschen Port, oder läuft, bevor die App tatsächlich mit dem Lauschen begonnen hat — lesen Sie die eigene Ausgabe des Checks, bevor Sie den Anwendungscode verdächtigen.
Crash-Loop oder langsamer erster Boot? Der Restart-Zähler verrät es
Ein Datenbank-Container in diesem Homelab hing in restarting fest, was genau wie ein Crash-Loop aussah — bis eine Prüfung von docker compose ps eine Minute später zeigte, dass sich der Restart-Zähler seit der ersten Prüfung nicht bewegt hatte. Das war das eigentliche Signal: Ein echter Crash-Loop erhöht seinen Restart-Zähler alle paar Sekunden, während Compose wiederholt versucht und scheitert, während ein langsamer erster Boot einmal (oder gar nicht) neu startet und dann einfach eine Weile innerhalb dieses einen Versuchs braucht. Die betroffene Datenbank führte bei ihrem ersten Start eine initiale Schema-Migration durch — eine einmalige, mehrere Minuten dauernde Verzögerung, die in den ersten zehn Sekunden genau wie ein Crash-Loop aussieht und eine Minute später überhaupt nicht mehr.
Die Lösung ist in beide Richtungen unterschiedlich: Ein echter Crash-Loop bedeutet, dass der zugrunde liegende Fehler behoben werden muss, bevor der Container jemals stabil laufen wird — sei es ein Netzwerkproblem, wie im Netzwerk-Guide beschrieben, oder eine schlechte Restart-Policy, die schlecht mit einem echten Fehler zusammenspielt, wie im Restart-Policies-Guide beschrieben. Ein langsamer erster Boot braucht nur Geduld und eine start_period beim Healthcheck, die lang genug ist, um ihn nicht als unhealthy zu markieren, bevor er tatsächlich bereit ist.
Entscheidungstabelle
| Symptom | Zuerst prüfen | Wahrscheinliche Ursache |
|---|---|---|
| Container als "running" gelistet, App aber nicht erreichbar | healthcheck-Status via docker inspect | Healthcheck testet falschen Port/Pfad, oder App lauscht noch nicht |
| Restart-Zähler steigt alle paar Sekunden | logs --tail für den Absturzgrund, dann der Exit-Code | Echter Crash-Loop — Konfigurationsfehler, fehlende Abhängigkeit, oder Exit 137 (OOM) |
| Restart-Zähler flach, Container "restarting" für Minuten | Ob dies der allererste Start ist | Langsamer erster Boot — Migration, Index-Aufbau, oder großer Datensatz-Ladevorgang |
| Exit-Code 137 | docker inspect auf OOMKilled: true | Speicherlimit zu niedrig für die Arbeitslast, nicht zwangsläufig ein App-Bug |
Exit-Code 143 außerhalb eines bewussten stop/down | Ob etwas anderes einen Stop ausgelöst hat | Etwas Externes hat SIGTERM gesendet — Orchestrierungs-Skript oder gesundheitsbasierten Auto-Restart prüfen |
Das Fazit
Lesen, bevor Sie neu starten. docker compose ps für den Zustand, logs --tail für die Historie, der Exit-Code für die Kategorie, und die eigene Ausgabe des Healthchecks, falls definiert — in dieser Reihenfolge, jedes Mal, egal welcher der sieben Dienste in einem Stack derjenige ist, der kaputt ist. Sobald ein Stack wieder stabil läuft, ist das saubere Herunter- und Wiederhochfahren eine eigene Entscheidung, die im Guide Down vs. Stop vs. Kill behandelt wird.
Häufige Fragen
Was bedeutet Exit-Code 137 bei Docker?
128 + 9 — der Prozess hat SIGKILL erhalten. Das bedeutet fast immer, dass der OOM-Killer des Kernels den Container wegen zu hohen Speicherverbrauchs beendet hat; bestätigen Sie das mit docker inspect <container> --format='{{.State.OOMKilled}}', bevor Sie einen Anwendungsfehler vermuten.
Wie prüfe ich, warum ein Docker-Compose-Container ständig neu startet?
Prüfen Sie zuerst docker compose ps auf den Restart-Zähler — steigt er alle paar Sekunden, lesen Sie docker compose logs --tail=100 <Dienst> für den tatsächlichen Fehler, bevor der nächste Neustart ihn löscht, und prüfen Sie dann den Exit-Code, um die Kategorie einzugrenzen.
Was ist der Unterschied zwischen docker compose logs und docker logs?
docker compose logs <Dienst> liest anhand des im Compose-File definierten Dienstnamens und kann die Ausgabe jedes Dienstes im Stack gleichzeitig zeigen; docker logs <container> braucht den tatsächlichen Container-Namen oder die ID. Für einen von Compose verwalteten Stack ist docker compose logs fast immer die praktischere der beiden Varianten.
Wie erkenne ich, ob ein Docker-Healthcheck fehlschlägt?
docker inspect --format='{{json .State.Health}}' <container> zeigt den aktuellen Status (starting, healthy, oder unhealthy) und die letzten mehreren Prüfergebnisse, inklusive der tatsächlichen Ausgabe des Prüfbefehls — lesen Sie diese Ausgabe, bevor Sie die Anwendung selbst verdächtigen.
Warum zeigt mein Container als laufend an, obwohl die App nicht reagiert?
Prüfen Sie zuerst den Healthcheck-Status, falls einer definiert ist — ein Container kann im Zustand running sein, während sein Healthcheck unhealthy meldet, oft weil der Check den falschen Port oder Pfad testet, oder die App noch nicht fertig gestartet ist.
Mehr aus Self-Hosting & Privatsphäre

Wo environment:, env_file: und secrets: in Docker Compose ein Credential jeweils offenlegen, warum secrets: keinen Swarm braucht, und das .env + .gitignore-Muster, das dieses Homelab tatsaechlich verwendet.

Was docker compose up, --build und build --no-cache jeweils wirklich neu bauen, die Volume-Falle, die einen frisch gebauten Container trotzdem veraltet aussehen laesst, und echte Rebuild-Zeiten aus diesem Homelab.

Ich habe docker compose stop ausgefuehrt und erwartet, dass der ganze Stack verschwindet. Container, Netzwerk und Volumes blieben trotzdem bestehen -- was stop, down, down -v und kill wirklich abbauen, und wann welcher Befehl der richtige ist.
Bleiben Sie auf dem Laufenden
Erhalten Sie die neuesten Artikel direkt in Ihr Postfach. Kein Spam, jederzeit abbestellbar.
Docker Compose Secrets im Vergleich zu .env
Wo environment:, env_file: und secrets: in Docker Compose ein Credential jeweils offenlegen, warum secrets: keinen Swarm braucht, und das .env + .gitignore-Muster, das dieses Homelab tatsaechlich verwendet.
Weiterlesen