The Problem: Your Containers Are Leaking Zombies

You have been running Docker containers for months. Everything looks fine from the outside — the containers are up, your app responds, monitoring shows green. But something is happening inside that you cannot see.

Orphaned processes are accumulating. When a container spawns a child process and that process exits, it becomes a zombie — a dead process that still occupies an entry in the process table. Normally, the parent process would “reap” these zombies. But if the parent is your application, not a proper init system, zombies pile up over time.

The symptoms are subtle at first. Memory usage creeps up. Process table entries grow. Eventually, the container runs out of file descriptors or process slots. docker stop takes 10 seconds instead of 1 because your app never receives the signal properly. Your containers restart unexpectedly and you spend hours debugging why.

This is not a Docker bug. This is how Linux processes work. Docker containers run a single process by default — your application. That process becomes PID 1. And PID 1 has responsibilities most applications were never designed to handle.

What Is tini?

tini is a tiny init system designed specifically for containers. It sits between Docker and your application, acting as PID 1. Your app runs as a child process under tini.

Think of tini as a responsible parent. When your application spawns child processes, tini ensures those processes are properly reaped when they exit. When Docker sends a signal to stop the container, tini forwards that signal to your app immediately and correctly. If your app crashes, tini exits too — so the container exits, which is exactly what you want for proper orchestration.

tini is:

  • Tiny: About 20KB compiled. No dependencies. No configuration files.
  • Purpose-built: Designed specifically for containers, not full systems.
  • Battle-tested: Used by millions of containers. Bundled into Docker itself since 2016.
  • Transparent: Your app does not know tini exists. It just works.

The name is literally “tiny init.” It does one job and does it well.

How tini Works

When a Docker container starts without tini, the process tree looks like this:

PID 1: /usr/bin/python app.py  ? Your app, but also PID 1
??? PID 7: /usr/bin/python worker.py  ? Child process

Your application becomes PID 1. In Linux, PID 1 has special responsibilities:

  1. Signal handling: PID 1 does not respond to signals the same way other processes do. By default, SIGTERM and SIGINT are ignored. Docker has to force-kill your container after the 10-second timeout.
  2. Zombie reaping: Orphaned processes get reparented to PID 1. PID 1 is expected to wait() on these zombies. If your app is PID 1 and never calls wait(), zombies accumulate forever.

With tini, the process tree changes:

PID 1: /sbin/tini -- /usr/bin/python app.py  ? tini handles init duties
??? PID 7: /usr/bin/python app.py  ? Your app runs normally
    ??? PID 12: /usr/bin/python worker.py  ? Child processes work as expected

Now tini handles:

  • Signal forwarding: When Docker sends SIGTERM to PID 1, tini forwards it to your app immediately. Your app shuts down gracefully. No 10-second timeout.
  • Zombie reaping: When child processes exit and become zombies, tini automatically reaps them. No process table leaks.
  • Exit propagation: When your app exits (crash or normal), tini exits with the same code. The container stops. Your orchestrator (Kubernetes, Docker Swarm, Docker Compose) notices and takes action.

The Signal Problem in Detail

Here is what happens when you run docker stop on a container without tini:

  1. Docker sends SIGTERM to PID 1 (your app).
  2. PID 1 ignores SIGTERM by default (Linux kernel behaviour for PID 1).
  3. 10 seconds pass.
  4. Docker sends SIGKILL (force kill).
  5. Your app dies instantly — no cleanup, no graceful shutdown.

With tini:

  1. Docker sends SIGTERM to PID 1 (tini).
  2. tini forwards SIGTERM to your app.
  3. Your app handles the signal, finishes requests, closes connections.
  4. Your app exits cleanly.
  5. tini exits with the same exit code.
  6. Container stops in 1-2 seconds, not 10+.

When You Need tini (and When You Do Not)

You NEED tini if:

  • Your app spawns child processes. Python multiprocessing, Node.js worker threads, Java thread pools, shell scripts that background tasks — all of these create child processes that can become zombies.
  • You want fast, graceful shutdowns. If docker stop consistently times out, you have a signal problem.
  • Containers run for days or weeks. Short-lived containers might not accumulate enough zombies to matter. Long-running containers definitely will.
  • You use an orchestrator. Kubernetes, Swarm, and Nomad all expect containers to respond to signals. tini ensures they do.

You do NOT need tini if:

  • Your app is already a proper init system. If you are running systemd or s6 inside a container (rare, but possible), you already have an init.
  • Your app never spawns children. A single-process Go binary with no goroutine-spawned subprocesses might be fine. But verify with docker exec — check for zombie processes.
  • Using Docker Swarm or Kubernetes with built-in init. Some platforms inject an init automatically. Check your platform docs.

Practical Example: Adding tini to a Python App

Let’s walk through adding tini to a real Dockerfile. We will use a Python web app that spawns background workers.

Before (problematic):

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Problem: Python becomes PID 1
CMD ["python", "app.py"]

When you run this container:

# Check for zombie processes after running for a while
$ docker exec -it myapp ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  1.2 123456 12345 ?        Ss   10:00   0:01 python app.py
root         7  0.0  0.0      0     0 ?        Z    10:05   0:00 [python] <defunct>
root         8  0.0  0.0      0     0 ?        Z    10:10   0:00 [python] <defunct>
root         9  0.0  0.0      0     0 ?        Z    10:15   0:00 [python] <defunct>

Those <defunct> entries are zombies. They will never go away.

After (with tini):

FROM python:3.11-slim

# Install tini
RUN apt-get update && apt-get install -y --no-install-recommends tini \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# tini becomes PID 1, Python runs as a child
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["python", "app.py"]

Now check the process list:

$ docker exec -it myapp ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   1234   567 ?        Ss   10:00   0:00 /usr/bin/tini -- python app.py
root         7  0.1  1.2 123456 12345 ?        S    10:00   0:01 python app.py
root        15  0.0  0.5  67890  5678 ?        S    10:05   0:00 python worker.py

No zombies. tini is reaping them automatically.

Even Simpler: Use Docker’s Built-in Init

Since Docker 1.13, you can enable tini without modifying your Dockerfile:

# Run container with built-in init
docker run --init myapp

# Or in docker-compose.yml
services:
  app:
    image: myapp:latest
    init: true  # Enables tini automatically

The --init flag tells Docker to run tini as PID 1 automatically. Your Dockerfile stays unchanged.

Getting Started

Option 1: Docker Compose (Recommended)

The easiest approach for most deployments:

# docker-compose.yml
services:
  web:
    image: myapp:latest
    init: true  # That's it
    restart: unless-stopped

Option 2: Dockerfile with tini

For more control or non-Compose environments:

# Alpine-based images
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

# Debian/Ubuntu-based images
RUN apt-get update && apt-get install -y --no-install-recommends tini \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "server.js"]

Hosting Recommendation

Running containers that need process management? You need a host with full control over your Docker environment. Canadian Web Hosting’s Cloud VPS gives you root access to run Docker, Docker Compose, or full orchestrators like Kubernetes.

For a container-heavy workload:

  • 2 vCPU, 4GB RAM: Small workloads, development, staging
  • 4 vCPU, 8GB RAM: Production apps with multiple containers
  • 8 vCPU, 16GB RAM: Container orchestration platforms

All CWH Cloud VPS instances include Canadian data centres (Vancouver, Toronto), SOC 2 Type II certified infrastructure, and 24/7 support if you need help with your container setup.

Conclusion

tini solves a real problem that affects almost every long-running Docker container. Zombie processes accumulate. Signals get mishandled. Containers take 10 seconds to stop when they should stop in 1.

The fix is simple: add one line to your Docker Compose file (init: true) or install tini in your Dockerfile. The result: faster shutdowns, cleaner process management, and containers that behave predictably.

If you are running containers in production, you should be using an init system. tini is the smallest, simplest, and most widely adopted option. It takes 5 minutes to add and solves problems that would otherwise take hours to debug.

For more container best practices, see our guides on controlling Docker Compose startup order and better container startup orchestration.