SIGTERM-Handling und Graceful Shutdown in containerisierten Apps
AI generated
FROM
RUN
Docker · Betrieb · Signal-Handling
SIGTERM-Handling und Graceful Shutdown
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.

16 Min. Lesezeit SIGTERM graceful shutdown docker stop -t PID 1 Zero-Downtime

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.

11. FAQ: Graceful Shutdown in Containern: Das Wichtigste auf einen Blick

1Warum ignoriert meine Anwendung SIGTERM im Container, obwohl sie es lokal verarbeitet?
Meistens liegt es an der Shell-Form im CMD, die eine zusätzliche Shell-Ebene als PID 1 einfügt und Signale nicht weiterleitet, oder daran, dass PID 1 ohne registrierten Handler Signale grundsätzlich anders behandelt als gewöhnliche Kindprozesse.
2Was bedeutet der Exit-Code 137 bei einem Docker-Container?
137 ergibt sich aus 128 plus Signalnummer 9, also SIGKILL. Er zeigt an, dass der Container nicht sauber auf SIGTERM reagiert hat und Docker ihn nach Ablauf der Grace Period hart beendet hat.
3Wie lange wartet docker stop standardmäßig, bevor SIGKILL gesendet wird?
Der Standardwert liegt bei zehn Sekunden. Er lässt sich per docker stop -t für einen einzelnen Aufruf oder über stop_grace_period dauerhaft in der Compose-Datei anpassen.
4Muss ich in Node.js einen eigenen SIGTERM-Handler schreiben?
Ja, Node.js registriert von sich aus keinen sinnvollen SIGTERM-Handler. Ohne eigenen process.on('SIGTERM')-Callback beendet der Node-Prozess laufende Requests nicht sauber, sondern wird nach Ablauf der Grace Period einfach hart gekillt.
5Verhält sich PHP-FPM beim Empfang von SIGTERM automatisch korrekt?
Der PHP-FPM-Master-Prozess behandelt SIGTERM bereits sinnvoll, sofern er im Vordergrund mit --nodaemonize als PID 1 läuft. Er stoppt neue Requests und wartet, bis laufende Worker fertig sind, bevor er sich beendet.
6Was ist der Unterschied zwischen CMD in Shell-Form und Exec-Form?
Die Shell-Form, etwa CMD node server.js, wird intern als /bin/sh -c ausgeführt, wodurch die Shell zu PID 1 wird. Die Exec-Form, geschrieben als JSON-Array wie CMD ["node", "server.js"], startet die Anwendung direkt als PID 1 ohne umschließende Shell.
7Wie kann ich testen, ob mein Graceful Shutdown tatsächlich funktioniert?
Am einfachsten mit docker stop bei laufenden Logs im Hintergrund: Erscheinen die erwarteten Log-Zeilen wie SIGTERM empfangen und schließe Verbindungen, funktioniert der Handler. Bleiben sie aus, liegt das Problem meist an PID-1-Signal-Handling oder der Shell-Form im CMD.
8Reicht ein try-finally-Block in PHP aus, um Datenbank-Connections beim Shutdown zu schließen?
Für klassisches PHP-FPM mit einem Request-Prozess pro Aufruf reicht das request-lokale Aufräumen meist aus, da jeder Worker nach dem Request ohnehin endet. Für langlebige PHP-Prozesse mit pcntl-Signal-Handling muss das Schließen explizit im SIGTERM-Callback erfolgen, ein try-finally allein reagiert nicht auf Signale.
9Warum sollte ein Load Balancer schon vor SIGTERM Bescheid wissen?
Weil sonst neue Requests noch beim Container ankommen können, während er bereits herunterfährt. Ein Readiness-Check, der kurz vor dem Stopp fehlschlägt, nimmt den Container rechtzeitig aus der Verteilung, bevor der eigentliche Shutdown beginnt.
10Kann eine zu lange Grace Period Probleme verursachen?
Ja, sie verlangsamt Deployments und Rolling Updates unnötig, weil jeder Container-Austausch so lange dauert wie die konfigurierte Wartezeit, selbst wenn die Anwendung bereits längst fertig heruntergefahren ist und nur ungenutzt auf SIGKILL wartet.