tini und dumb-init: Zombie-Prozesse in Containern vermeiden
AI generated
FROM
RUN
Docker · Linux · Prozess-Management
tini und dumb-init
Zombie-Prozesse in Containern vermeiden

Ein Container ohne echten Init-Prozess kann Zombie-Prozesse ansammeln und Signale falsch verarbeiten, zwei Probleme, die auf den ersten Blick harmlos wirken, aber langlaufende Produktivsysteme über Wochen hinweg destabilisieren können. tini und dumb-init lösen beide Probleme mit wenigen Kilobyte Zusatzcode.

16 Min. Lesezeit tini dumb-init PID 1 Zombie-Prozess docker run --init

1. Das PID-1-Problem in Containern

Jeder Linux-Prozess hat einen Elternprozess, und wenn ein Kindprozess endet, muss der Elternprozess dessen Exit-Status per wait() abholen, damit der Kernel die zugehörigen Ressourcen freigeben kann. Geschieht das nicht, bleibt der beendete Prozess als sogenannter Zombie in der Prozesstabelle stehen: kein aktiver Prozess mehr, aber ein Eintrag, der Speicher belegt und in ps als Status Z sichtbar ist. Auf einem normalen Linux-System übernimmt PID 1, der Init-Prozess, automatisch das Reaping verwaister Zombies, deren ursprünglicher Elternprozess bereits selbst beendet wurde.

In einem Container fehlt dieser Init-Prozess in der Regel, denn der Hauptprozess der Anwendung läuft direkt als PID 1. Startet dieser Prozess selbst Kindprozesse, etwa eine Shell, die wiederum ein Hilfsprogramm aufruft, und kümmert sich nicht explizit um deren Reaping, sammeln sich Zombies an. Die meisten Anwendungen wurden nie dafür geschrieben, diese Init-Aufgabe zu übernehmen, weil sie in einer klassischen Umgebung nie als PID 1 laufen mussten.

2. Wie Zombie-Prozesse konkret entstehen

Ein typisches Beispiel: Ein Cron-artiger Node.js- oder PHP-Prozess ruft regelmäßig über child_process.exec beziehungsweise exec() ein externes Kommandozeilenwerkzeug auf, etwa zur Bildkonvertierung mit ImageMagick oder zum Aufruf eines Backup-Skripts. Solange die Anwendung selbst den Exit-Code jedes gestarteten Kindprozesses korrekt abholt, ist alles in Ordnung. Fehlt diese Abholung, etwa weil eine Bibliothek das Kind über einen unsauber verwalteten Prozess-Pool startet, bleibt ein Zombie zurück, sobald das Kind endet.

Problematisch wird es vor allem bei Shell-Skripten als Einstiegspunkt, die selbst wiederum weitere Prozesse starten und beenden, ohne sich um deren Reaping zu kümmern, weil sie schlicht nicht dafür gebaut wurden, als Init-Prozess zu fungieren. In einem langlaufenden Container, der über Wochen ohne Neustart läuft und regelmäßig solche Unterprozesse startet, sammeln sich Zombies langsam aber stetig an, bis irgendwann die maximale Prozessanzahl des Systems (pid_max) erreicht wird und keine neuen Prozesse mehr gestartet werden können.


# Zombie-Prozesse in einem laufenden Container aufspueren
docker exec meincontainer ps aux | awk '$8=="Z" {print}'

# Anzahl der Zombies zaehlen
docker exec meincontainer sh -c "ps -eo stat | grep -c '^Z'"

3. Das zweite Problem: fehlerhafte Signalweiterleitung

Neben dem Reaping von Zombies hat PID 1 noch eine zweite Sonderaufgabe: Signale wie SIGTERM ohne registrierten Handler werden bei PID 1 nicht mit dem Standardverhalten behandelt, das für gewöhnliche Prozesse gilt. Läuft eine Shell als PID 1, etwa weil CMD in Shell-Form geschrieben wurde, empfängt diese Shell zwar das SIGTERM, leitet es aber typischerweise nicht automatisch an ihre Kindprozesse weiter, sodass die eigentliche Anwendung von der Beendigungsanfrage nie erfährt.

Ein minimaler Init-Prozess wie tini oder dumb-init behebt genau dieses Problem, indem er selbst als PID 1 läuft, empfangene Signale korrekt an den eigentlichen Anwendungsprozess weiterleitet und dessen Kindprozesse zuverlässig reaped. Damit übernimmt er exakt die beiden Kernaufgaben eines vollwertigen Init-Systems wie systemd, jedoch in einem winzigen, für Container zugeschnittenen Umfang von wenigen hundert Kilobyte statt eines vollständigen Systemd-Stacks.

4. tini: Der De-facto-Standard für minimale Init-Prozesse

tini ist ein sehr kleines, in C geschriebenes Init-Programm, das speziell für Container entwickelt wurde und mittlerweile sogar offiziell in den Docker-Daemon integriert ist. Es startet den eigentlichen Anwendungsprozess als sein einziges Kind, leitet alle empfangenen Signale korrekt weiter und reaped zuverlässig alle Zombie-Prozesse, die im Container entstehen, unabhängig davon, wie tief verschachtelt die Prozesshierarchie darunter ist.

Die Einbindung erfolgt entweder durch explizites Installieren des tini-Binaries im Dockerfile und Verwendung als ENTRYPOINT, oder, seit Docker 1.13, deutlich einfacher über die Laufzeitoption --init, die tini automatisch injiziert, ohne dass es Teil des Images sein muss. Für viele Standardfälle reicht die Laufzeitoption völlig aus, für reproduzierbare Images unabhängig von der Docker-Version ist die explizite Installation im Dockerfile aber die robustere Wahl.


# Dockerfile: tini explizit installieren und als Init nutzen
FROM node:20-alpine
RUN apk add --no-cache tini
COPY . /app
WORKDIR /app
RUN npm ci --omit=dev

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

5. tini im Detail: Subreaper-Modus und Debugging

Standardmäßig läuft tini als klassischer PID-1-Prozess und reaped ausschließlich seine direkten und indirekten Kinder innerhalb der eigenen Prozesshierarchie. In selteneren Konstellationen, etwa wenn innerhalb des Containers ein eigenes kleines Supervisor-Programm mehrere unabhängige Prozessbäume startet, kann es sinnvoll sein, tini als sogenannten Subreaper laufen zu lassen. Dafür existiert die Umgebungsvariable TINI_SUBREAPER, die tini anweist, den entsprechenden Kernel-Mechanismus PR_SET_CHILD_SUBREAPER zu nutzen und dadurch auch verwaiste Enkel- und Urenkelprozesse zuverlässig zu reapen.

Für die Fehlersuche bietet tini außerdem einen Verbose-Modus, aktivierbar über das Flag -v beim Aufruf oder wiederholt als -vv für noch detailliertere Ausgaben. Im Verbose-Modus protokolliert tini jedes empfangene Signal und jeden gereapten Prozess auf der Standardfehlerausgabe, was sich hervorragend eignet, um zu überprüfen, ob ein vermuteter Zombie-Prozess tatsächlich abgeholt wird oder ob ein Signal wie erwartet an die Anwendung weitergeleitet wurde.


# tini im Verbose-Modus starten, um Signal- und Reap-Vorgaenge zu sehen
docker run --rm -it \
  --entrypoint /sbin/tini \
  mironsoft/app:latest \
  -vv -- node server.js

# Subreaper-Modus fuer verschachtelte Prozessbaeume aktivieren
docker run -e TINI_SUBREAPER=1 --init mironsoft/app:latest

6. dumb-init: Die Alternative von Yelp

dumb-init, entwickelt von Yelp, verfolgt denselben Ansatz wie tini und löst dieselben zwei Probleme: korrekte Signalweiterleitung und zuverlässiges Zombie-Reaping. Funktional unterscheiden sich beide Werkzeuge kaum, dumb-init bietet aber einige zusätzliche Konfigurationsoptionen, etwa das explizite Umschreiben bestimmter Signale, bevor sie an das Kind weitergeleitet werden, was in Nischenfällen mit exotischem Signal-Handling hilfreich sein kann.

In der Praxis hat sich tini als De-facto-Standard durchgesetzt, nicht zuletzt weil es direkt in Docker integriert ist und über --init ganz ohne Image-Änderung nutzbar ist. dumb-init bleibt trotzdem eine solide, aktiv gepflegte Alternative, besonders für Teams, die bereits Erfahrung damit haben oder eine der zusätzlichen Konfigurationsoptionen konkret benötigen.


# Dockerfile: dumb-init als Alternative zu tini
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init \
    && rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app

ENTRYPOINT ["dumb-init", "--"]
CMD ["python", "worker.py"]

7. docker run --init: Die eingebaute Alternative

Seit Docker 1.13 gibt es die Laufzeitoption --init, mit der Docker automatisch eine minimale, intern gebündelte Version von tini als PID 1 einsetzt, ohne dass das Image selbst irgendetwas dafür mitbringen muss. Der eigentliche Anwendungsprozess läuft dann als Kind dieses eingebauten tini und profitiert von derselben korrekten Signalweiterleitung und demselben Zombie-Reaping wie bei einer expliziten Installation.

Der Vorteil dieser Variante liegt in ihrer Einfachheit: Ein einziges zusätzliches Flag beim docker run beziehungsweise ein init: true in der Compose-Datei genügt, ganz ohne Dockerfile-Änderung. Der Nachteil ist, dass diese Einstellung vom jeweiligen Aufrufer des Containers gesetzt werden muss und nicht automatisch im Image mitkommt, was bei Weitergabe eines Images an Dritte oder in Orchestrator-Umgebungen ohne explizite Konfiguration leicht vergessen wird.


# docker-compose.yml: eingebautes tini per init aktivieren
services:
  worker:
    image: mironsoft/image-processor:latest
    init: true   # entspricht docker run --init

# Aequivalent per CLI:
# docker run --init -d mironsoft/image-processor:latest

8. Wann ein Init-Prozess wirklich notwendig ist

Nicht jeder Container braucht zwingend tini oder dumb-init. Wenn die Anwendung selbst niemals eigene Kindprozesse startet, etwa ein einfacher Go-Binary oder ein Node.js-Server, der ausschließlich In-Process arbeitet, entstehen gar keine Zombies, weil es keine Kindprozesse gibt, die abgeholt werden müssten. In diesem Fall ist die Hauptmotivation für einen Init-Prozess eher die korrekte Signalweiterleitung, falls die Anwendung selbst als PID 1 keinen eigenen Handler registriert.

Ein Init-Prozess wird hingegen fast unverzichtbar, sobald die Anwendung Subprozesse startet, etwa ein PHP-Skript, das über exec() externe Tools aufruft, ein Node-Prozess mit child_process, oder ein Shell-Wrapper-Skript, das mehrere Hintergrundprozesse orchestriert. Auch bei Multi-Prozess-Setups innerhalb eines einzigen Containers, etwa Supervisor-verwaltete Anwendungen mit mehreren Worker-Prozessen, ist ein echter Init-Prozess praktisch Pflicht, um Zombies zuverlässig zu vermeiden.

9. tini, dumb-init und --init im direkten Vergleich

Alle drei Ansätze lösen dieselben zwei Kernprobleme, unterscheiden sich aber in Integration und Flexibilität. Die explizite Installation von tini oder dumb-init im Dockerfile macht das Image self-contained: Es funktioniert korrekt, unabhängig davon, mit welchen Flags es später gestartet wird, was besonders für öffentlich verteilte Images oder Basis-Images für andere Teams wichtig ist. docker run --init verlagert diese Verantwortung dagegen auf den Aufrufer des Containers.

Für interne Projekte, bei denen Docker-Version und Startbefehl unter eigener Kontrolle stehen, ist --init oft die pragmatischste Wahl, weil sie Dockerfile-Änderungen erspart. Für Images, die weitergegeben oder von Dritten verwendet werden, empfiehlt sich hingegen die explizite Installation als ENTRYPOINT, damit korrektes Verhalten garantiert ist, egal wie der Container später gestartet wird. Die folgende Tabelle fasst die Unterschiede zusammen.

Ansatz Integration Signalweiterleitung Zombie-Reaping Empfohlen für
tini (im Dockerfile) Als ENTRYPOINT installiert Ja, korrekt Ja, zuverlässig Verteilte/öffentliche Images
dumb-init (im Dockerfile) Als ENTRYPOINT installiert Ja, mit Umschreibe-Optionen Ja, zuverlässig Teams mit speziellen Signal-Anforderungen
docker run --init Zur Laufzeit vom Aufrufer gesetzt Ja, korrekt (nutzt tini intern) Ja, zuverlässig Interne Projekte, eigene Kontrolle über Start-Flags
Kein Init-Prozess Keine Nur wenn App eigenen Handler hat Nein, Zombies möglich Nur Prozesse ohne Subprozess-Erzeugung

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

tini und dumb-init: Das Wichtigste auf einen Blick

PID-1-Problem

PID 1 reaped Zombies und leitet Signale nicht automatisch weiter, anders als bei echten Init-Systemen.

tini

Minimaler, in Docker integrierter Init-Prozess, De-facto-Standard für Container.

dumb-init

Funktional gleichwertige Alternative von Yelp mit zusätzlichen Signal-Umschreibe-Optionen.

docker run --init

Eingebaute Laufzeit-Alternative ohne Dockerfile-Änderung, Verantwortung liegt beim Aufrufer.

11. FAQ: tini und dumb-init: Das Wichtigste auf einen Blick

1Was ist ein Zombie-Prozess in einem Docker-Container?
Ein Zombie ist ein bereits beendeter Prozess, dessen Exit-Status noch nicht von seinem Elternprozess per wait() abgeholt wurde. Er belegt weiterhin einen Eintrag in der Prozesstabelle, obwohl er keine tatsächliche Arbeit mehr verrichtet, sichtbar in ps mit Status Z.
2Warum entstehen in Containern häufiger Zombie-Prozesse als auf normalen Linux-Systemen?
Weil der Hauptprozess der Anwendung im Container direkt als PID 1 läuft, ohne dass ein vollwertiger Init-Prozess wie systemd vorgeschaltet ist. Startet dieser Prozess Kindprozesse, ohne deren Reaping selbst zu übernehmen, sammeln sich Zombies an, was auf einem normalen System das Init-System automatisch verhindern würde.
3Braucht jeder Docker-Container tini oder dumb-init?
Nein, nur Container, deren Hauptprozess selbst Kindprozesse startet, etwa über exec()-Aufrufe oder Shell-Skripte. Ein einfacher Single-Prozess-Server ohne Subprozesse erzeugt keine Zombies und benötigt primär nur dann einen Init-Prozess, wenn Signalweiterleitung ein Thema ist.
4Wie aktiviere ich tini ohne das Dockerfile zu ändern?
Über die Laufzeitoption docker run --init, oder in Compose über init: true beim jeweiligen Service. Docker injiziert dann automatisch eine intern gebündelte, minimale tini-Version als PID 1, ohne dass das Image selbst tini enthalten muss.
5Was ist der Unterschied zwischen tini und dumb-init?
Funktional lösen beide dieselben Kernprobleme, korrekte Signalweiterleitung und Zombie-Reaping. dumb-init bietet zusätzlich Optionen zum expliziten Umschreiben bestimmter Signale vor der Weiterleitung, tini ist dafür direkt in Docker integriert und dadurch etwas verbreiteter.
6Kann ich tini und docker run --init gleichzeitig verwenden?
Technisch ja, aber es ist unnötig und kann zu doppeltem Signal-Handling führen. Wenn tini bereits explizit im Dockerfile als ENTRYPOINT installiert ist, sollte auf das zusätzliche --init-Flag verzichtet werden, um keine zwei verschachtelten Init-Prozesse zu erzeugen.
7Führt ein Init-Prozess wie tini zu spürbarem Performance-Overhead?
Nein, tini und dumb-init sind sehr kleine, in C geschriebene Programme mit minimalem Speicher- und CPU-Fußabdruck. Der Overhead ist in der Praxis vernachlässigbar, selbst bei sehr ressourcenbeschränkten Containern.
8Wie erkenne ich, ob mein Container tatsächlich Zombie-Prozesse ansammelt?
Mit docker exec ps aux lässt sich der Prozessstatus einsehen, Zombies erscheinen dort mit dem Status Z in der STAT-Spalte. Bei langlaufenden Containern lohnt sich eine regelmäßige Stichprobe, besonders wenn die Anwendung häufig externe Kommandos aufruft.
9Was passiert, wenn zu viele Zombie-Prozesse in einem Container entstehen?
Jeder Zombie belegt einen Eintrag in der begrenzten Prozesstabelle des Systems. Sammeln sich genug davon an, wird irgendwann die maximale Prozessanzahl (pid_max) erreicht, wodurch keine neuen Prozesse mehr gestartet werden können, was in der Praxis zu Ausfällen scheinbar unzusammenhängender Funktionen führt.
10Ist die explizite Installation von tini im Dockerfile besser als --init zur Laufzeit?
Für Images, die weitergegeben oder von Dritten gestartet werden, ja, weil das Verhalten dann unabhängig vom Startbefehl garantiert ist. Für interne Projekte mit voller Kontrolle über die Start-Flags ist --init oft die einfachere und ebenso zuverlässige Wahl.