in containerisierten Apps richtig umsetzen
docker stop schickt ein SIGTERM und wartet, doch viele Anwendungen reagieren gar nicht darauf und werden nach Ablauf der Frist hart gekillt. Wer versteht, wie Signal-Weiterleitung, PID 1 und Grace Period zusammenspielen, kann Verbindungsabbrüche und Datenverlust beim Deployment zuverlässig vermeiden.
Inhaltsverzeichnis
- 1. Was beim Stoppen eines Containers wirklich passiert
- 2. Warum PID 1 in Containern eine besondere Rolle spielt
- 3. Die Shell-Form-Falle: CMD und Signalweiterleitung
- 4. Graceful Shutdown in Node.js-Anwendungen implementieren
- 5. Graceful Shutdown in PHP-Anwendungen (PHP-FPM)
- 6. docker stop -t: Die Grace Period konfigurieren
- 7. docker stop vs. docker kill: ein wichtiger Unterschied
- 8. SIGKILL erkennen und vermeiden
- 9. Zusammenspiel in der Praxis: Ein vollständiger Ablauf
- 10. Zusammenfassung
- 11. FAQ
1. Was beim Stoppen eines Containers wirklich passiert
Wenn docker stop aufgerufen wird, sendet Docker zunächst ein SIGTERM-Signal an den Hauptprozess des Containers, also an PID 1 innerhalb des Container-Namespace. Dieses Signal ist eine höfliche Bitte: Bitte beende dich selbst und räume dabei sauber auf. Der Prozess bekommt eine gewisse Zeitspanne, die sogenannte Grace Period, um darauf zu reagieren. Reagiert er nicht rechtzeitig, schickt Docker ein SIGKILL, das den Prozess sofort und ohne jede Möglichkeit zum Aufräumen beendet.
Genau in diesem Ablauf liegt das Problem vieler containerisierter Anwendungen: Sie reagieren auf SIGTERM überhaupt nicht, weil entweder der Prozess selbst keinen Signal-Handler registriert, oder weil das Signal ihn nie erreicht. Die Folge ist immer dieselbe, unabhängig von der genauen Ursache: Der Container hängt die volle Grace Period lang scheinbar unbeeindruckt weiter, bis SIGKILL kommt und offene Datenbank-Transaktionen, laufende HTTP-Requests oder Datei-Schreibvorgänge abrupt abgebrochen werden.
2. Warum PID 1 in Containern eine besondere Rolle spielt
In einem normalen Linux-System ist PID 1 der Init-Prozess, etwa systemd, der speziell dafür gebaut ist, Signale korrekt zu verarbeiten und an Kindprozesse weiterzuleiten. Der Linux-Kernel behandelt PID 1 jedoch anders als jeden anderen Prozess: Für Signale ohne registrierten Handler wird kein Standardverhalten angewendet. Ein gewöhnlicher Prozess würde bei SIGTERM ohne eigenen Handler automatisch beendet, PID 1 hingegen ignoriert das Signal in diesem Fall stillschweigend, es sei denn, die Anwendung registriert explizit einen eigenen Handler dafür.
In einem Container ist der Hauptprozess praktisch immer PID 1, weil Docker keinen eigenen Init-Prozess vorschaltet, sofern man das nicht ausdrücklich konfiguriert. Startet man also direkt node server.js oder php-fpm als CMD im Dockerfile, läuft dieser Prozess als PID 1 und muss sich selbst um Signal-Handling kümmern. Viele Frameworks und Laufzeitumgebungen wurden aber nie dafür entworfen, als Init-Prozess zu laufen, und bringen deshalb von Haus aus keinen SIGTERM-Handler mit.
# PID 1 im Container prüfen
docker exec meincontainer ps -o pid,comm -p 1
# Signal-Handler eines laufenden Prozesses anzeigen (SigCgt-Feld, hex-Bitmaske)
docker exec meincontainer cat /proc/1/status | grep Sig
3. Die Shell-Form-Falle: CMD und Signalweiterleitung
Eine besonders verbreitete Ursache für ignoriertes SIGTERM liegt in der Schreibweise von CMD und ENTRYPOINT im Dockerfile. Wird der Befehl in der Shell-Form geschrieben, etwa CMD node server.js, führt Docker ihn intern als /bin/sh -c "node server.js" aus. Damit wird nicht die Anwendung selbst zu PID 1, sondern die Shell, und die meisten Shells wie sh oder bash leiten empfangene Signale standardmäßig nicht an ihre Kindprozesse weiter.
Die Lösung ist die Exec-Form von CMD, geschrieben als JSON-Array: CMD ["node", "server.js"]. Dabei wird der Prozess direkt gestartet, ohne umschließende Shell, und erhält SIGTERM unmittelbar als PID 1. Dieser Unterschied wirkt auf den ersten Blick kosmetisch, entscheidet in der Praxis aber darüber, ob ein Graceful Shutdown überhaupt eine Chance hat, jemals ausgelöst zu werden.
# FALSCH: Shell-Form, Shell wird PID 1, leitet SIGTERM nicht weiter
CMD node server.js
# RICHTIG: Exec-Form, die Anwendung selbst wird PID 1
CMD ["node", "server.js"]
# Gleiches gilt fuer ENTRYPOINT
ENTRYPOINT ["php-fpm", "--nodaemonize"]
4. Graceful Shutdown in Node.js-Anwendungen implementieren
In Node.js lässt sich ein SIGTERM-Handler mit process.on registrieren. Wichtig ist, dass der Server dabei nicht abrupt beendet wird, sondern zunächst keine neuen Verbindungen mehr annimmt, laufende Requests aber zu Ende bearbeiten darf. Das HTTP-Server-Objekt bietet dafür die Methode close, die genau dieses Verhalten umsetzt: Sie schließt den Listening-Socket sofort, wartet aber auf den Abschluss aller bereits offenen Verbindungen, bevor der Callback aufgerufen wird.
Zusätzlich sollten Datenbank-Connection-Pools, Message-Queue-Verbindungen und offene Dateihandles explizit geschlossen werden, statt sich auf implizites Aufräumen beim Prozessende zu verlassen. Ein Timeout als Sicherheitsnetz ist ebenfalls sinnvoll, damit ein einzelner hängender Request den gesamten Shutdown nicht endlos blockiert und den Prozess bis zum SIGKILL verzögert.
# server.js (Node.js Graceful Shutdown)
const http = require('http');
const server = http.createServer(app);
server.listen(3000);
let shuttingDown = false;
function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
console.log(`${signal} empfangen, fahre sauber herunter...`);
server.close(async () => {
console.log('Keine offenen HTTP-Verbindungen mehr.');
await dbPool.end(); // Datenbank-Connections schliessen
await messageQueue.close(); // Queue-Verbindung schliessen
process.exit(0);
});
// Sicherheitsnetz: nach 10s hart beenden, falls close() haengt
setTimeout(() => {
console.error('Shutdown-Timeout erreicht, erzwinge Beendigung.');
process.exit(1);
}, 10000).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
5. Graceful Shutdown in PHP-Anwendungen (PHP-FPM)
PHP-FPM verhält sich anders als ein klassischer Node.js-Prozess, weil es pro Request einen kurzlebigen Worker-Prozess nutzt, statt eine dauerhafte Event-Loop zu betreiben. Der Master-Prozess von PHP-FPM behandelt SIGTERM standardmäßig bereits sinnvoll: Er stoppt neue Requests, wartet aber, bis laufende Worker ihre aktuelle Anfrage beendet haben, bevor er sich selbst beendet. Wichtig ist, PHP-FPM im Vordergrund und als PID 1 laufen zu lassen, mit der Option --nodaemonize, statt den klassischen Daemon-Modus zu verwenden.
Bei langlebigen PHP-Prozessen außerhalb von FPM, etwa einem Swoole- oder ReactPHP-basierten Worker, muss der Entwickler das Signal-Handling selbst übernehmen, ähnlich wie in Node.js. Die pcntl-Erweiterung stellt dafür pcntl_signal bereit, mit dem sich ein Callback für SIGTERM registrieren lässt. Entscheidend ist dabei, dass pcntl_async_signals(true) aktiviert wird, damit Signale asynchron zwischen den Verarbeitungsschritten abgefangen werden, statt nur an expliziten pcntl_signal_dispatch-Aufrufen.
# Dockerfile: PHP-FPM im Vordergrund als PID 1
FROM php:8.4-fpm-alpine
COPY php-fpm.conf /usr/local/etc/php-fpm.d/zz-custom.conf
CMD ["php-fpm", "--nodaemonize"]
6. docker stop -t: Die Grace Period konfigurieren
Standardmäßig wartet docker stop zehn Sekunden zwischen SIGTERM und dem finalen SIGKILL. Diese Frist lässt sich über die Option -t (oder --time) anpassen, sowohl beim einmaligen Aufruf als auch dauerhaft über stop_grace_period in der Compose-Datei. Für Anwendungen, die typischerweise nur kurze Requests bearbeiten, reichen zehn Sekunden meist aus. Anwendungen mit lang laufenden Operationen, etwa Datei-Uploads, Report-Generierung oder WebSocket-Verbindungen, benötigen unter Umständen deutlich mehr Zeit.
Eine zu kurze Grace Period führt dazu, dass Graceful-Shutdown-Logik zwar korrekt implementiert ist, aber schlicht nicht rechtzeitig fertig wird, bevor SIGKILL zuschlägt. Eine zu lange Grace Period wiederum verlangsamt Deployments und Rolling Updates unnötig, weil jeder Container-Austausch so lange dauert wie die konfigurierte Wartezeit, selbst wenn die Anwendung längst fertig heruntergefahren ist. Die richtige Balance ergibt sich aus der realistischen maximalen Dauer der längsten offenen Operation plus einem Sicherheitspuffer.
# docker-compose.yml
services:
api:
image: mironsoft/api:latest
stop_grace_period: 30s # SIGTERM, dann bis zu 30s warten vor SIGKILL
stop_signal: SIGTERM # Standard, aber explizit dokumentiert
# Aequivalent per CLI:
# docker stop -t 30 api-container
7. docker stop vs. docker kill: ein wichtiger Unterschied
Neben docker stop existiert mit docker kill ein zweiter Befehl, der auf den ersten Blick ähnlich wirkt, sich aber grundlegend anders verhält. docker kill sendet standardmäßig sofort ein SIGKILL, ganz ohne vorheriges SIGTERM und ohne jede Grace Period. Der Container wird augenblicklich beendet, unabhängig davon, ob gerade Requests bearbeitet werden oder Datenbank-Transaktionen offen sind. Für den produktiven Betrieb ist das fast immer die falsche Wahl.
docker kill ist trotzdem nützlich, aber für einen anderen Zweck: als letztes Mittel, wenn ein Container trotz gesendetem SIGTERM partout nicht reagiert, etwa weil er in einem Deadlock hängt, oder für Testzwecke, bei denen bewusst ein hartes Verhalten wie nach einem Stromausfall simuliert werden soll. Auch mit docker kill lässt sich über die Option -s ein anderes Signal als SIGKILL senden, etwa docker kill -s SIGTERM, was dann aber ohne Grace Period sofort läuft, im Gegensatz zu docker stop, das nach dem Senden aktiv auf das Prozessende wartet.
8. SIGKILL erkennen und vermeiden
Ein Container, der regelmäßig per SIGKILL statt sauber beendet wird, zeigt sich im docker events-Log und im Exit-Code des Containers, häufig als Exit-Code 137, was 128 plus Signal 9 entspricht. Dieser Wert ist ein zuverlässiges Diagnose-Signal dafür, dass der Graceful-Shutdown-Pfad nicht funktioniert, entweder weil kein Handler registriert ist, weil die Shell-Form das Signal blockiert, oder weil die Grace Period zu knapp bemessen ist.
Zur systematischen Fehlersuche hilft es, den Shutdown-Vorgang lokal zu testen, indem man docker stop mit einer sehr kurzen Grace Period gegen die Logs des Containers laufen lässt und beobachtet, ob die eigene Shutdown-Logik überhaupt aufgerufen wird. Bleiben die erwarteten Log-Zeilen wie SIGTERM empfangen aus, liegt das Problem meist entweder am PID-1-Signal-Handling oder an der Shell-Form im CMD, nicht an der eigentlichen Anwendungslogik.
9. Zusammenspiel in der Praxis: Ein vollständiger Ablauf
Ein sauberer Shutdown-Ablauf sieht folgendermaßen aus: Der Orchestrator oder Administrator ruft docker stop auf, Docker sendet SIGTERM an PID 1 im Container. Die Anwendung, korrekt als Exec-Form-CMD gestartet, empfängt das Signal über ihren registrierten Handler, stoppt die Annahme neuer Verbindungen, lässt bestehende Requests zu Ende laufen, schließt Datenbank-Pools und Queue-Verbindungen sauber und beendet sich schließlich selbst mit Exit-Code 0, alles innerhalb der konfigurierten Grace Period.
Bei Load-Balancer-gestützten Setups kommt eine zusätzliche Ebene hinzu: Der Load Balancer muss den Container idealerweise schon vor dem SIGTERM aus der Verteilung nehmen, etwa über einen Readiness-Check, der kurz vor dem Stopp fehlschlägt, damit keine neuen Requests mehr überhaupt beim sterbenden Container ankommen. Diese Kombination aus Signal-Handling in der Anwendung, korrekter PID-1-Konfiguration und angemessener Grace Period ist die Grundlage für echte Zero-Downtime-Deployments.
| Ursache | Symptom | Lösung | Betroffene Komponente |
|---|---|---|---|
| CMD in Shell-Form | SIGTERM erreicht Anwendung nie, Exit-Code 137 | CMD als JSON-Array (Exec-Form) schreiben | Dockerfile |
| Kein Signal-Handler registriert | Container hängt bis Grace Period abläuft | process.on('SIGTERM') bzw. pcntl_signal einbauen | Anwendungscode |
| Grace Period zu kurz | Shutdown-Logik startet, wird aber abgebrochen | stop_grace_period erhöhen, an längste Operation anpassen | docker-compose.yml |
| Load Balancer kennt Shutdown nicht | Requests landen auf sterbendem Container | Readiness-Check vor SIGTERM fehlschlagen lassen | Orchestrierung/LB |
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
Graceful Shutdown in Containern: Das Wichtigste auf einen Blick
PID 1 Problem
Ohne registrierten Handler ignoriert PID 1 SIGTERM, anders als gewöhnliche Prozesse.
Exec- statt Shell-Form
CMD als JSON-Array verhindert, dass eine Shell das Signal verschluckt.
Explizites Handling
Node.js und PHP brauchen expliziten Code, um Verbindungen sauber zu schließen.
Grace Period abstimmen
stop_grace_period bzw. docker stop -t an die längste reale Operation anpassen.