tini and dumb-init: Avoiding Zombie Processes in Containers
AI generated
FROM
RUN
Docker · Linux · Process Management
tini and dumb-init
avoiding zombie processes in containers

A container without a real init process can accumulate zombie processes and mishandle signals, two problems that look harmless at first glance but can destabilize long running production systems over weeks. tini and dumb-init solve both problems with just a few kilobytes of extra code.

16 min read tini dumb-init PID 1 zombie process docker run --init

1. The PID 1 problem in containers

Every Linux process has a parent process, and when a child process terminates, its parent must collect its exit status via wait() so the kernel can free the associated resources. If that never happens, the terminated process lingers in the process table as a so called zombie: no longer an active process, but an entry that occupies memory and shows up in ps with status Z. On a normal Linux system, PID 1, the init process, automatically reaps orphaned zombies whose original parent has already terminated itself.

Inside a container, this init process is usually absent, since the application's main process runs directly as PID 1. If that process itself spawns child processes, for example a shell that in turn invokes a helper tool, and does not explicitly take care of reaping them, zombies accumulate. Most applications were never written to take on this init responsibility, since in a classic environment they never had to run as PID 1.

2. How zombie processes actually appear

A typical example: a cron style Node.js or PHP process regularly invokes an external command line tool via child_process.exec or exec(), for instance for image conversion with ImageMagick or to call a backup script. As long as the application itself correctly collects the exit code of every spawned child, everything is fine. If that collection is missing, for example because a library spawns the child through a poorly managed process pool, a zombie is left behind as soon as the child terminates.

This becomes especially problematic with shell scripts as an entry point that themselves spawn and terminate further processes without taking care of reaping them, simply because they were never built to act as an init process. In a long running container that runs for weeks without a restart and regularly spawns such subprocesses, zombies accumulate slowly but steadily until the system's maximum process count (pid_max) is eventually reached and no new processes can be started at all.


# Find zombie processes in a running container
docker exec mycontainer ps aux | awk '$8=="Z" {print}'

# Count the number of zombies
docker exec mycontainer sh -c "ps -eo stat | grep -c '^Z'"

3. The second problem: broken signal forwarding

Besides reaping zombies, PID 1 has a second special job: signals like SIGTERM without a registered handler are not treated with the default behavior that applies to ordinary processes when they arrive at PID 1. If a shell runs as PID 1, for example because CMD was written in shell form, that shell does receive the SIGTERM, but it typically does not automatically forward it to its child processes, so the actual application never learns about the termination request.

A minimal init process like tini or dumb-init fixes exactly this problem by running as PID 1 itself, correctly forwarding received signals to the actual application process, and reliably reaping its child processes. That means it takes on exactly the two core responsibilities of a full init system like systemd, but in a tiny, container tailored footprint of a few hundred kilobytes instead of a full systemd stack.

4. tini: the de facto standard for minimal init processes

tini is a very small init program written in C, specifically built for containers, and by now even officially integrated into the Docker daemon. It launches the actual application process as its only child, correctly forwards every signal it receives, and reliably reaps any zombie processes that arise in the container, no matter how deeply nested the process hierarchy below it is.

It can be added either by explicitly installing the tini binary in the Dockerfile and using it as ENTRYPOINT, or, since Docker 1.13, much more simply through the runtime option --init, which injects tini automatically without it needing to be part of the image. For many standard cases, the runtime option is entirely sufficient; for images that need to be reproducible regardless of the Docker version, explicit installation in the Dockerfile is the more robust choice.


# Dockerfile: install tini explicitly and use it as init
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 in detail: subreaper mode and debugging

By default, tini runs as a classic PID 1 process and reaps only its direct and indirect children within its own process hierarchy. In rarer setups, for example when a small supervisor program inside the container itself spawns several independent process trees, it can make sense to run tini as a so called subreaper. For that purpose there is the TINI_SUBREAPER environment variable, which instructs tini to use the corresponding kernel mechanism PR_SET_CHILD_SUBREAPER and thereby reliably reap orphaned grandchild and great-grandchild processes as well.

For troubleshooting, tini also offers a verbose mode, enabled via the -v flag on invocation, or repeated as -vv for even more detailed output. In verbose mode, tini logs every received signal and every reaped process to standard error, which is excellent for verifying whether a suspected zombie process is actually being collected, or whether a signal was forwarded to the application as expected.


# Start tini in verbose mode to see signal and reap events
docker run --rm -it \
  --entrypoint /sbin/tini \
  mironsoft/app:latest \
  -vv -- node server.js

# Enable subreaper mode for nested process trees
docker run -e TINI_SUBREAPER=1 --init mironsoft/app:latest

6. dumb-init: Yelp's alternative

dumb-init, developed by Yelp, follows the same approach as tini and solves the same two problems: correct signal forwarding and reliable zombie reaping. Functionally the two tools barely differ, but dumb-init offers a few extra configuration options, such as explicitly rewriting certain signals before forwarding them to the child, which can be useful in niche cases with unusual signal handling requirements.

In practice, tini has become the de facto standard, not least because it is integrated directly into Docker and usable via --init without any image changes at all. dumb-init remains a solid, actively maintained alternative nonetheless, particularly for teams that already have experience with it or specifically need one of its extra configuration options.


# Dockerfile: dumb-init as an alternative to 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: the built in alternative

Since Docker 1.13, there has been a runtime option --init that makes Docker automatically use a minimal, internally bundled version of tini as PID 1, without the image itself having to bring anything for it. The actual application process then runs as a child of this built in tini and benefits from the same correct signal forwarding and the same zombie reaping as with an explicit installation.

The advantage of this approach lies in its simplicity: a single extra flag on docker run, or init: true in the Compose file, is enough, with no Dockerfile change required. The downside is that this setting has to be applied by whoever runs the container and does not travel with the image automatically, which is easy to forget when an image is handed off to third parties or run in orchestrator environments without explicit configuration.


# docker-compose.yml: enable the built in tini via init
services:
  worker:
    image: mironsoft/image-processor:latest
    init: true   # equivalent to docker run --init

# Equivalent via CLI:
# docker run --init -d mironsoft/image-processor:latest

8. When an init process is actually necessary

Not every container strictly needs tini or dumb-init. If the application itself never spawns child processes of its own, for example a simple Go binary or a Node.js server that works entirely in process, no zombies can arise at all, since there are no child processes to be collected. In that case, the main motivation for an init process is more about correct signal forwarding, in case the application itself as PID 1 does not register its own handler.

An init process becomes nearly indispensable, though, as soon as the application spawns subprocesses, for example a PHP script that calls external tools via exec(), a Node process using child_process, or a shell wrapper script orchestrating several background processes. Multi process setups within a single container, such as supervisor managed applications with several worker processes, also practically require a real init process to reliably avoid zombies.

9. tini, dumb-init, and --init side by side

All three approaches solve the same two core problems but differ in integration and flexibility. Explicitly installing tini or dumb-init in the Dockerfile makes the image self contained: it behaves correctly regardless of which flags it is later started with, which matters especially for publicly distributed images or base images used by other teams. docker run --init, by contrast, shifts that responsibility onto whoever runs the container.

For internal projects where the Docker version and start command are under your own control, --init is often the more pragmatic choice, since it avoids Dockerfile changes altogether. For images that get distributed or used by third parties, explicit installation as ENTRYPOINT is recommended instead, so correct behavior is guaranteed no matter how the container is later started. The table below summarizes the differences.

Approach Integration Signal forwarding Zombie reaping Recommended for
tini (in the Dockerfile) Installed as ENTRYPOINT Yes, correct Yes, reliable Distributed/public images
dumb-init (in the Dockerfile) Installed as ENTRYPOINT Yes, with rewrite options Yes, reliable Teams with special signal requirements
docker run --init Set at runtime by the caller Yes, correct (uses tini internally) Yes, reliable Internal projects, full control over start flags
No init process None Only if the app has its own handler No, zombies possible Only processes that never spawn subprocesses

Mironsoft

Container infrastructure, CI pipelines and deployment automation

Docker setups that hold up across the team and in production?

We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.

Dockerfile Review

Systematically optimizing multi-stage builds, layer caching and image size.

Security Audit

Hardening container isolation, secrets handling and image scanning against real attack surfaces.

CI/CD Integration

Building build pipelines, registries and deployment strategies for reproducible releases.

10. Summary

tini and dumb-init: Key Takeaways

PID 1 problem

PID 1 does not automatically reap zombies or forward signals, unlike a real init system.

tini

Minimal init process integrated into Docker, the de facto standard for containers.

dumb-init

Functionally equivalent alternative from Yelp with extra signal rewrite options.

docker run --init

Built in runtime alternative with no Dockerfile change, responsibility rests with the caller.

11. FAQ: tini and dumb-init: Key Takeaways

1What is a zombie process in a Docker container?
A zombie is an already terminated process whose exit status has not yet been collected by its parent via wait(). It still occupies an entry in the process table even though it no longer does any actual work, visible in ps with status Z.
2Why do zombie processes appear more often in containers than on normal Linux systems?
Because the application's main process runs directly as PID 1 in the container, with no full init process like systemd in front of it. If that process spawns children without taking on reaping itself, zombies accumulate, something a normal system's init would prevent automatically.
3Does every Docker container need tini or dumb-init?
No, only containers whose main process itself spawns child processes, for example through exec() calls or shell scripts. A simple single process server with no subprocesses creates no zombies and mainly needs an init process if signal forwarding is a concern.
4How do I enable tini without changing the Dockerfile?
Through the runtime option docker run --init, or in Compose via init: true for the given service. Docker then automatically injects an internally bundled, minimal tini version as PID 1 without the image itself needing to contain tini.
5What is the difference between tini and dumb-init?
Functionally both solve the same core problems, correct signal forwarding and zombie reaping. dumb-init additionally offers options to explicitly rewrite certain signals before forwarding, while tini is directly integrated into Docker and therefore somewhat more widespread.
6Can I use tini and docker run --init at the same time?
Technically yes, but it is unnecessary and can lead to duplicate signal handling. If tini is already explicitly installed as ENTRYPOINT in the Dockerfile, the extra --init flag should be skipped to avoid nesting two init processes.
7Does an init process like tini cause noticeable performance overhead?
No, tini and dumb-init are very small programs written in C with a minimal memory and CPU footprint. The overhead is negligible in practice, even for very resource constrained containers.
8How do I check whether my container is actually accumulating zombie processes?
With docker exec ps aux you can inspect process status, zombies show up there with status Z in the STAT column. For long running containers it is worth sampling this regularly, especially if the application frequently calls external commands.
9What happens if too many zombie processes accumulate in a container?
Each zombie occupies an entry in the system's limited process table. Once enough of them accumulate, the maximum process count (pid_max) is eventually reached, preventing any new processes from starting, which in practice causes failures in seemingly unrelated functionality.
10Is explicitly installing tini in the Dockerfile better than --init at runtime?
For images that get distributed or run by third parties, yes, because behavior is then guaranteed regardless of the start command. For internal projects with full control over the start flags, --init is often the simpler and equally reliable choice.