Every container runs with PID 1 as its first process. This init process has a critical responsibility that most developers overlook: signal handling and zombie process reaping. Using the wrong PID 1 process leads to containers that ignore shutdown signals, accumulate zombie processes, and refuse to stop gracefully. This guide compares three init process solutions: tini, dumb-init, and docker-init.
The PID 1 Problem in Containers
In a standard Linux system, PID 1 (systemd, SysVinit, or OpenRC) handles two responsibilities that other processes do not:
Signal forwarding: When the kernel sends SIGTERM or SIGINT to PID 1, the default behavior is to ignore the signal. Only processes that explicitly install signal handlers will respond to shutdown signals.
Zombie reaping: When a child process exits, it becomes a zombie until its parent calls
wait(). PID 1 is responsible for reaping zombies that become orphaned (when their original parent dies before waiting).
Most application processes (Node.js, Python, Java, etc.) are not designed to be PID 1. They don’t install SIGTERM handlers and they don’t reap orphaned children. Running them directly as PID 1 in a container causes:
- Graceful shutdown failures:
docker stopsends SIGTERM to PID 1, which ignores it. After 10 seconds, Docker sends SIGKILL, terminating the process without cleanup. - Zombie accumulation: If your application spawns child processes (workers, subprocesses) and they exit, they remain as zombies consuming PID table entries.
- Unreliable health checks: A container with a dead PID 1 but living child processes appears healthy to Docker’s health check.
For related reading on container process management, see our Supervisord vs s6-overlay vs Runit guide and our OCI Runtime Hooks guide. For container security hardening, check our Container Security Hardening guide.
tini
tini is a tiny but valid init process for containers. It was created by Thomas Orozco and is now included by default in Docker as docker-init.
How tini Works
tini runs as PID 1 and performs two functions:
- Signal forwarding: Registers signal handlers for SIGTERM, SIGINT, SIGHUP, SIGQUIT, and forwards them to the child process.
- Zombie reaping: Calls
waitpid()in a loop to reap any zombie children.
Docker Compose with tini
| |
The init: true flag automatically injects tini as PID 1. Alternatively, install tini manually:
| |
Custom Signal Handling
tini supports signal forwarding to process groups and custom subreaping:
| |
Key Features
- Minimal: 23KB binary, zero dependencies
- Signal forwarding: Forwards all standard signals to child process
- Zombie reaping: Reaps all orphaned zombie children
- Process group support:
-gflag forwards signals to entire process group - Subreaping:
-sflag acts as a subreaper for complex process trees - Docker default: Built into Docker as
docker-initvia--initflag - Well-tested: Used in millions of production containers
dumb-init
dumb-init is a simple init system written in C by Yelp. It is designed to be a drop-in replacement for any container entrypoint.
How dumb-init Works
Like tini, dumb-init runs as PID 1, forwards signals, and reaps zombies. However, dumb-init has additional features for signal rewriting and session management.
Docker Compose with dumb-init
| |
Dockerfile with dumb-init
| |
Signal Rewriting
dumb-init’s unique feature is signal rewriting, which maps incoming signals to different signals for the child process:
| |
This is particularly useful for:
- Java applications: JVM handles SIGQUIT gracefully for shutdown hooks, but may ignore SIGTERM
- Go applications: Some Go apps handle SIGQUIT but not SIGTERM
- Legacy applications: Applications expecting specific signals that Docker doesn’t send by default
Key Features
- Signal rewriting: Map any signal to any other signal
- Session leadership: Can run as session leader for proper terminal behavior
- Verbose mode:
--verboseflag for debugging signal flow - C implementation: No runtime dependencies, compiled to static binary
- Yelp-backed: Developed and maintained by Yelp’s infrastructure team
- Strict mode:
--strictmode for debugging with enhanced signal handling
docker-init (Built-in tini)
docker-init is Docker’s built-in init process, which is essentially a packaged version of tini. It’s activated via the --init flag or the init: true Compose option.
Usage
| |
How It Differs from Manual tini
| Aspect | docker-init | Manual tini |
|---|---|---|
| Installation | Built into Docker | Must install in image |
| Version | Fixed by Docker version | Choose specific version |
| Configuration | No custom flags | Full tini CLI options |
| Image size | No impact on image | Adds ~23KB to image |
| Process group | Default forwarding | -g for process group |
Docker daemon.json Default Init
You can enable init by default for all containers:
| |
This eliminates the need to specify init: true in every Compose file.
Comparison Table
| Feature | tini | dumb-init | docker-init |
|---|---|---|---|
| Language | C | C | C (tini fork) |
| Binary Size | 23KB | 15KB | 150KB |
| Signal Forwarding | Yes | Yes | Yes |
| Zombie Reaping | Yes | Yes | Yes |
| Signal Rewriting | No | Yes | No |
| Process Group Mode | Yes (-g) | Yes | No |
| Subreaping | Yes (-s) | No | No |
| Docker Built-in | Yes (as docker-init) | No | Yes |
| Image Installation | Required (if not using docker-init) | Required | Not required |
| GitHub Stars | 10,000+ | 5,000+ | N/A (Docker built-in) |
| Best For | General containers | Apps needing signal mapping | Simple Docker setups |
When to Use Each Tool
Use docker-init (tini) when:
- You’re using Docker and want zero-configuration init
- Your application just needs basic signal forwarding and zombie reaping
- You don’t want to modify your Dockerfile or add packages
Use manual tini when:
- You need process group signal forwarding (
-gflag) - You’re using Podman or containerd (not Docker)
- You need subreaping for complex process trees (
-sflag) - You want a specific version of tini regardless of Docker version
Use dumb-init when:
- Your application needs signal rewriting (e.g., SIGTERM → SIGQUIT for Java)
- You need verbose debugging of signal flow
- You’re running applications that have specific signal expectations
- You want the smallest possible init binary (15KB vs 23KB)
Verification: Testing Signal Handling
Verify your init process handles signals correctly:
| |
For zombie reaping verification:
| |
FAQ
Do I need an init process if my application handles SIGTERM?
If your application explicitly installs a SIGTERM handler and doesn’t spawn child processes, you might not need an init. However, most applications don’t handle SIGTERM by default, and even if they do, they won’t reap orphaned zombies. Using an init process is a cheap insurance policy — it adds negligible overhead and prevents hard-to-debug shutdown issues.
Does docker-init add overhead to my container?
No measurable overhead. docker-init (tini) is a tiny C program that does two things: forward signals and reap zombies. It uses minimal CPU and memory. The only impact is an additional process in your process tree (PID 1 instead of your application directly).
Can I use tini with Podman?
Yes. Podman doesn’t have a built-in init like Docker’s --init flag, but you can install tini in your image and set it as the entrypoint: ENTRYPOINT ["tini", "--"]. Alternatively, Podman supports --init in newer versions using the same tini binary.
Why does dumb-init rewrite SIGTERM to SIGQUIT for Java?
The JVM’s shutdown hooks are triggered by SIGTERM, but some Java frameworks (particularly older ones) have custom signal handlers that respond better to SIGQUIT. Additionally, some application servers (like older Tomcat versions) use SIGQUIT for graceful shutdown. The --rewrite 15:3 flag ensures the JVM receives the signal it expects for clean shutdown.
What is zombie reaping and why does it matter?
When a process exits, it becomes a “zombie” — its process table entry remains until the parent calls wait(). If the parent exits without waiting, the zombie is reparented to PID 1, which must reap it. In containers without an init, orphaned zombies accumulate and consume PID table entries. On systems with limited PID space (default: 32768), enough zombies can prevent new processes from spawning.
Can I use both tini and dumb-init?
No, and you shouldn’t need to. Both serve the same purpose as PID 1 init processes. Using two init processes would create unnecessary nesting (init → init → app). Choose one based on your needs: docker-init for simplicity, tini for process group support, or dumb-init for signal rewriting.