Wenn ein Container sich selbst immer wieder tötet
Ein Container, der in einer Endlosschleife startet, kurz läuft und dann wieder abstürzt, ist eines der frustrierendsten Alltagsprobleme im Docker-Betrieb, weil die Logs oft schneller verschwinden, als man sie lesen kann. Mit einem systematischen Vorgehen lässt sich die Ursache meist innerhalb weniger Minuten eingrenzen, statt wild an Konfigurationswerten herumzuraten.
Inhaltsverzeichnis
- 1. Was eine Restart-Loop technisch bedeutet
- 2. Schritt 1: Logs vor dem letzten Crash sichern
- 3. Schritt 2: Restart Policy temporär deaktivieren
- 4. Häufige Ursache 1: Fehlende oder falsche Umgebungsvariable
- 5. Häufige Ursache 2: Datenbank noch nicht bereit
- 6. Häufige Ursache 3: Ein zu aggressiver Healthcheck
- 7. Speicherprobleme: Wenn der Kernel den Container selbst tötet
- 8. Vorbeugen statt debuggen: Restart-Loops von vornherein vermeiden
- 9. Der systematische Debugging-Ablauf im Überblick
- 10. Zusammenfassung
- 11. FAQ
1. Was eine Restart-Loop technisch bedeutet
Eine Restart-Loop entsteht, wenn ein Container mit einer Restart Policy wie always, on-failure oder unless-stopped läuft, der Hauptprozess aber unmittelbar nach dem Start mit einem Fehler beendet wird. Docker interpretiert diese Restart Policy strikt und startet den Container gemäß der Konfiguration erneut, was ihn erneut mit demselben Fehler abstürzen lässt, wodurch der Zyklus sich unbegrenzt wiederholt.
Der Container-Status zeigt in diesem Zustand meist einen ständigen Wechsel zwischen Restarting und kurzzeitig Up, was sich mit docker ps gut beobachten lässt. Anders als bei Kubernetes gibt es in reinem Docker keinen expliziten CrashLoopBackOff-Status mit exponentiellem Backoff, Docker erhöht aber ab Docker Engine intern zunehmend die Wartezeit zwischen Neustartversuchen, um die Systemlast zu begrenzen.
# Restart-Loop anhand des Status erkennen
docker ps -a --filter "name=webshop_app"
# STATUS: Restarting (1) 3 seconds ago
# Anzahl der bisherigen Neustarts anzeigen
docker inspect webshop_app --format '{{.RestartCount}}'
2. Schritt 1: Logs vor dem letzten Crash sichern
Der wichtigste erste Schritt ist, die Logausgabe des Containers zu sichern, bevor ein weiterer Neustart die relevanten Fehlermeldungen im Terminal verdrängt. Der Standardbefehl docker logs zeigt zunächst nur die aktuelle Laufzeit, während die Option --since in Kombination mit einer Umleitung in eine Datei erlaubt, den relevanten Ausschnitt dauerhaft zu sichern, statt ihn im scrollenden Terminal zu verlieren.
Besonders wertvoll ist die Option docker logs --previous-ähnliches Verhalten über den Container-Namen selbst, da Docker die Logs der vorherigen Ausführung eines neu gestarteten Containers weiterhin unter derselben Container-ID vorhält, solange der Container nicht vollständig entfernt wurde. Ein einfacher Redirect in eine Datei verhindert zudem, dass die Ausgabe bei einem sehr schnellen Crash-Zyklus im Terminal-Buffer verloren geht.
# Alle bisherigen Logs mit Zeitstempeln sichern
docker logs -t webshop_app > /tmp/webshop_app_crash.log 2>&1
# Nur die letzten 200 Zeilen vor dem aktuellen Neustart
docker logs -t --tail 200 webshop_app
# Live mitschneiden waehrend des naechsten Crashs
docker logs -f webshop_app | tee /tmp/webshop_live.log
3. Schritt 2: Restart Policy temporär deaktivieren
Solange die Restart Policy aktiv bleibt, startet Docker den Container nach jedem Absturz automatisch neu, was das gezielte Debugging erschwert, weil der Container sich unter Umständen bereits wieder neu startet, während man noch den vorherigen Fehler analysiert. Der zuverlässigste nächste Schritt ist deshalb, die Restart Policy vorübergehend auf no zu setzen, damit der Container nach einem Absturz im gestoppten Zustand verharrt.
Mit einem gestoppten Container lässt sich anschließend gezielt der letzte Exit-Code auslesen, der oft bereits einen entscheidenden Hinweis liefert: Exit-Code 1 deutet meist auf einen allgemeinen Anwendungsfehler hin, Exit-Code 137 auf ein durch den Kernel erzwungenes SIGKILL, häufig ausgelöst durch ein Out-of-Memory-Ereignis, und Exit-Code 143 auf ein reguläres SIGTERM, das die Anwendung nicht innerhalb der Grace-Period sauber verarbeitet hat.
# Restart Policy vorübergehend deaktivieren
docker update --restart=no webshop_app
# Exit-Code der letzten Ausfuehrung pruefen
docker inspect webshop_app --format '{{.State.ExitCode}}'
# Nach der Analyse: Policy wiederherstellen
docker update --restart=unless-stopped webshop_app
4. Häufige Ursache 1: Fehlende oder falsche Umgebungsvariable
Eine der häufigsten Ursachen für Restart-Loops ist eine fehlende, leere oder falsch benannte Umgebungsvariable, auf die die Anwendung beim Start zwingend angewiesen ist, etwa eine Datenbank-Zugangsdaten-Variable oder ein API-Secret. Viele Anwendungsframeworks brechen den Start bewusst mit einem klaren Fehler ab, wenn eine Pflichtvariable fehlt, was sich als schnelle, wiederholte Absturzsequenz zeigt, sobald die Fehlermeldung nur im überfluteten Log untergeht.
Ein gezielter Vergleich der tatsächlich im Container gesetzten Umgebungsvariablen mit der Erwartungshaltung der Anwendung, etwa über eine .env.example-Datei, deckt solche Probleme meist innerhalb weniger Sekunden auf. Besonders tückisch sind dabei stille Tippfehler im Variablennamen, die von der Anwendung nicht als fehlend erkannt werden, sondern schlicht ignoriert werden und zu einem Fallback-Wert führen, der erst später zu einem Fehler führt.
# Tatsaechlich gesetzte Umgebungsvariablen im Container pruefen
docker exec webshop_app env | sort
# Mit der erwarteten Konfiguration vergleichen
docker exec webshop_app env | sort > /tmp/actual.env
sort .env.example > /tmp/expected.env
diff /tmp/expected.env /tmp/actual.env
5. Häufige Ursache 2: Datenbank noch nicht bereit
Ein weiteres klassisches Muster tritt bei Multi-Container-Setups auf, in denen eine Anwendung beim Start sofort eine Verbindung zu einer Datenbank herstellen möchte, die selbst noch initialisiert. Der Datenbank-Container meldet zwar bereits den Status Up, akzeptiert aber noch keine Verbindungen, weil interne Initialisierungsschritte wie das Anlegen von Systemtabellen noch laufen, was insbesondere bei MySQL und PostgreSQL beim allerersten Start mehrere Sekunden dauern kann.
Die Anwendung erhält daraufhin einen Verbindungsfehler und beendet sich, woraufhin die Restart Policy sie neu startet, oft schneller als die Datenbank ihre Initialisierung abschließt, was zu mehreren gescheiterten Versuchen in Folge führt. Die zuverlässige Lösung ist ein Healthcheck auf dem Datenbank-Container in Kombination mit depends_on und der Bedingung service_healthy in docker-compose, statt sich auf die reine Prozess-Laufzeit zu verlassen.
# docker-compose.yml: korrekte Abhaengigkeit auf Healthcheck
services:
db:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
app:
depends_on:
db:
condition: service_healthy
6. Häufige Ursache 3: Ein zu aggressiver Healthcheck
Paradoxerweise kann auch ein zu streng konfigurierter Healthcheck selbst eine Restart-Loop auslösen, wenn er in Kombination mit einem externen Orchestrator oder einem Restart-Skript so eingerichtet ist, dass ein Container bei einem fehlgeschlagenen Health-Status aktiv neu gestartet wird. Ein zu kurzes interval in Kombination mit einer Anwendung, die etwas länger als erwartet für den Start benötigt, etwa wegen eines Cache-Warmup, führt dazu, dass der Container als ungesund markiert wird, obwohl er sich noch in einer regulären Startphase befindet.
Die Option start_period im Healthcheck ist dafür genau das richtige Werkzeug: Sie definiert eine Anlaufzeit, in der fehlgeschlagene Healthchecks noch nicht als Fehler gezählt werden, sondern der Anwendung Zeit für ihre eigentliche Initialisierung gegeben wird. Wird diese Option vergessen oder zu knapp bemessen, wirkt ein an sich funktionierender Container fälschlicherweise dauerhaft ungesund und wird von übergeordneten Systemen unnötig neu gestartet.
# Healthcheck mit ausreichender Anlaufzeit
docker run -d \
--health-cmd="curl -f http://localhost/health || exit 1" \
--health-interval=10s \
--health-timeout=3s \
--health-retries=3 \
--health-start-period=45s \
webshop_app
7. Speicherprobleme: Wenn der Kernel den Container selbst tötet
Wenn ein Container mit einem festen Speicherlimit gestartet wurde und die Anwendung dieses Limit überschreitet, greift der Out-of-Memory-Killer des Linux-Kernels ein und beendet den Hauptprozess zwangsweise mit SIGKILL. Dieser Fall zeigt sich zuverlässig am Exit-Code 137 sowie an der Kernel-Warnung, die sich außerhalb des Containers im Host-System-Log findet, während der Container selbst dabei oft keine aussagekräftige Fehlermeldung mehr ausgeben kann, da der Prozess ohne Vorwarnung beendet wird.
Zur Bestätigung dieser Ursache lohnt sich ein Blick auf das Feld OOMKilled in der Docker-Inspect-Ausgabe, das explizit anzeigt, ob der letzte Absturz durch den Out-of-Memory-Killer ausgelöst wurde. Die Lösung liegt entweder in einer Erhöhung des Speicherlimits oder, häufiger sinnvoll, in der Analyse und Behebung eines tatsächlichen Speicherlecks in der Anwendung selbst.
# Pruefen, ob der letzte Absturz durch OOM verursacht wurde
docker inspect webshop_app --format '{{.State.OOMKilled}}'
# Aktuelles Speicherlimit und Nutzung vergleichen
docker inspect webshop_app --format '{{.HostConfig.Memory}}'
docker stats webshop_app --no-stream
8. Vorbeugen statt debuggen: Restart-Loops von vornherein vermeiden
Ein Großteil der hier beschriebenen Ursachen lässt sich bereits vor dem ersten Absturz entschärfen, indem Anwendungen so geschrieben werden, dass sie beim Fehlen einer Pflichtkonfiguration eine klare, sofort erkennbare Fehlermeldung ausgeben, statt kryptisch abzubrechen. Ebenso hilfreich ist ein eingebauter Retry-Mechanismus mit begrenzter Anzahl an Versuchen für externe Abhängigkeiten wie Datenbankverbindungen, statt beim ersten Verbindungsfehler sofort den gesamten Prozess zu beenden.
In Compose-Setups lohnt sich außerdem der bewusste Verzicht auf eine zu aggressive Restart Policy während der aktiven Entwicklungsphase, etwa durch die vorübergehende Nutzung von on-failure:3 statt always, damit ein fehlerhafter Container nach wenigen Versuchen stehen bleibt und Zeit für eine Analyse lässt, statt unbegrenzt weiterzulaufen und die Logs mit identischen Fehlermeldungen zu überfluten.
# Begrenzte Restart Policy waehrend der Entwicklung
docker run -d --restart=on-failure:3 webshop_app
# In docker-compose.yml aequivalent
# restart: on-failure:3
9. Der systematische Debugging-Ablauf im Überblick
Statt bei jeder Restart-Loop die Ursachen in zufälliger Reihenfolge zu prüfen, empfiehlt sich ein fester Ablauf: zuerst die Logs sichern, dann die Restart Policy deaktivieren, dann den Exit-Code auslesen, dann gezielt anhand des Exit-Codes zwischen Umgebungsvariablen, Abhängigkeiten, Healthcheck-Konfiguration und Speicherlimits unterscheiden. Dieser Ablauf lässt sich vollständig innerhalb weniger Minuten durchlaufen und deckt die überwiegende Mehrheit der in der Praxis auftretenden Fälle ab.
Die folgende Tabelle ordnet die häufigsten Exit-Codes und Symptome den jeweils wahrscheinlichsten Ursachen zu, um die Fehlersuche von der ersten Beobachtung an gezielt zu lenken, statt bei jedem neuen Vorfall wieder bei null zu beginnen.
| Symptom / Exit-Code | Wahrscheinlichste Ursache | Erster Prüfschritt | Typische Lösung |
|---|---|---|---|
| Exit-Code 1 | Allgemeiner Anwendungsfehler, z. B. fehlende Umgebungsvariable | docker logs -t --tail 200 | Konfiguration bzw. Umgebungsvariablen korrigieren |
| Exit-Code 137 | Out-of-Memory-Kill durch den Kernel | docker inspect --format '{{.State.OOMKilled}}' | Speicherlimit erhöhen oder Speicherleck beheben |
| Exit-Code 143 | Reguläres SIGTERM, kein sauberes Shutdown | Shutdown-Handling der Anwendung prüfen | Graceful-Shutdown-Logik implementieren |
| Sofortiger Absturz nach Datenbankverbindung | Datenbank noch nicht bereit | docker logs des Datenbank-Containers | Healthcheck plus depends_on: service_healthy |
| Container als unhealthy trotz laufender App | Zu aggressiver oder zu früher Healthcheck | docker inspect --format '{{.State.Health}}' | start_period im Healthcheck erhöhen |
Mironsoft
Container-Infrastruktur, CI-Pipelines und Deployment-Automatisierung
Docker-Setups, die im Team und in Produktion tragfähig bleiben?
Wir prüfen bestehende Dockerfiles und Compose-Stacks auf Sicherheitslücken, aufgeblähte Images und fragile Build-Pipelines und bauen daraus eine Container-Infrastruktur, die schnell baut, sicher läuft und im Team nachvollziehbar bleibt.
Dockerfile-Review
Multi-Stage-Builds, Layer-Caching und Image-Größe systematisch optimieren.
Security-Audit
Container-Isolation, Secrets-Handling und Image-Scanning gegen echte Angriffsflächen absichern.
CI/CD-Integration
Build-Pipelines, Registries und Deployment-Strategien für reproduzierbare Releases aufbauen.
10. Zusammenfassung
Restart-Loops debuggen: Das Wichtigste auf einen Blick
Erster Schritt
Logs immer sofort sichern, bevor der nächste automatische Neustart sie überschreibt.
Restart Policy
Temporär auf 'no' setzen, um den Container im gestoppten Zustand analysieren zu können.
Exit-Codes
137 deutet auf OOM-Kill, 1 meist auf Anwendungsfehler wie fehlende Umgebungsvariablen.
Häufigste Ursache
Fehlende Umgebungsvariablen und zu früh gestartete Anwendungen vor bereiten Abhängigkeiten.