This error occurs when orphaned child processes in a Docker container are not properly reaped, leading to zombie processes that accumulate over time. The fix involves using an init system like tini or Docker's --init flag to handle process reaping as PID 1.
The "zombie process detected in container" error indicates that processes inside your Docker container are terminating but not being properly cleaned up. In Linux, when a process exits, it becomes a "zombie" until its parent process calls wait() to retrieve its exit status. Only then is it fully removed from the process table. In Docker containers, the main process runs as PID 1, which has special responsibilities in Linux. Unlike traditional init systems (like systemd), most applications are not designed to handle the responsibilities of PID 1, particularly reaping orphaned zombie processes and handling signals properly. When child processes exit and their parent doesn't call wait(), or when the parent exits before the child, the orphaned zombie gets reparented to PID 1. If PID 1 doesn't reap these zombies, they accumulate in the process table. While zombies don't consume CPU or memory, they do occupy slots in the kernel's process table, and over time this can exhaust available PIDs and prevent new processes from being created.
First, confirm you have zombie processes by checking the process state:
# Check for zombie processes inside the container
docker exec <container_name> ps aux | grep -E 'Z|defunct'
# Or use docker top to see all processes
docker top <container_name>Zombie processes show:
- State 'Z' in the STAT column
- <defunct> in the command column
- They have a parent PID but are no longer running
To count zombies:
docker exec <container_name> sh -c 'ps aux | grep -c defunct'Check if zombies are accumulating over time by running this periodically.
The simplest solution is to use Docker's built-in init process. Since Docker 1.13, the --init flag adds tini as PID 1:
docker run --init <image>This injects tini (a minimal init system) that:
- Runs as PID 1 in the container
- Reaps zombie processes by calling wait()
- Forwards signals to child processes properly
For Docker Compose, add init: true to your service:
services:
myapp:
image: myimage:latest
init: trueVerify tini is running as PID 1:
docker exec <container_name> ps aux | head -2You should see /sbin/docker-init or tini as PID 1.
If you need the init system baked into your image (for environments where --init isn't available), add tini directly:
For Alpine-based images:
FROM alpine:3.18
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["your-application"]For Debian/Ubuntu-based images:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["your-application"]Download tini binary directly:
FROM your-base-image
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD ["your-application"]Important: Use the JSON array syntax for ENTRYPOINT and CMD to avoid shell wrapping.
dumb-init from Yelp is another lightweight init system option:
In your Dockerfile:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y dumb-init && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["python", "app.py"]For Alpine:
FROM alpine:3.18
RUN apk add --no-cache dumb-init
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["./your-app"]dumb-init provides:
- Signal forwarding to child processes
- Zombie reaping
- Process group management with --single-child flag
The behavior is very similar to tini. Choose based on your preference or what's easier to install in your base image.
If zombies are caused by your application not waiting for children, fix the code:
Python example - proper subprocess handling:
import subprocess
import signal
import os
# Reap children on SIGCHLD
def reap_children(signum, frame):
while True:
try:
pid, status = os.waitpid(-1, os.WNOHANG)
if pid == 0:
break
except ChildProcessError:
break
signal.signal(signal.SIGCHLD, reap_children)
# Always wait for subprocesses
proc = subprocess.Popen(['command'])
proc.wait() # Don't forget this!Node.js example:
const { spawn } = require('child_process');
const child = spawn('command', ['args']);
child.on('exit', (code, signal) => {
console.log(`Child exited with code ${code}`);
});
// Handle cleanup on exit
process.on('exit', () => {
child.kill();
});Shell script example:
#!/bin/bash
# Use exec for the final command to replace the shell
exec your-application
# Or if spawning background processes, wait for them
background_task &
pid=$!
wait $pidImportant limitation: docker run --init does NOT reap zombies from processes started via docker exec. These processes are managed separately.
If you frequently use docker exec and see zombies from those commands:
Option 1: Use tini with subreaper mode
Tini can register as a subreaper to adopt zombies even when not PID 1:
ENTRYPOINT ["/sbin/tini", "-s", "--"]The -s flag enables subreaper mode.
Option 2: Wrap exec commands with an init
docker exec <container> /sbin/tini -- your-commandOption 3: Use bash -c for simple commands
Bash has built-in zombie reaping when running as PID 1 of a process group:
docker exec <container> bash -c 'your-command'This helps for commands that spawn subprocesses.
Set up monitoring to catch zombie issues early:
Check zombie count periodically:
#!/bin/bash
ZOMBIES=$(docker exec mycontainer sh -c 'ps aux | grep -c defunct' 2>/dev/null || echo 0)
if [ "$ZOMBIES" -gt 10 ]; then
echo "WARNING: $ZOMBIES zombie processes detected!"
fiAdd a healthcheck that monitors zombies:
HEALTHCHECK --interval=60s --timeout=10s --retries=3 \
CMD [ "$(ps aux | grep -c defunct)" -lt 50 ] || exit 1Docker Compose healthcheck:
services:
myapp:
image: myimage
init: true
healthcheck:
test: ["CMD-SHELL", "[ $(ps aux | grep -c defunct) -lt 50 ]"]
interval: 60s
timeout: 10s
retries: 3For production, consider using Prometheus with a custom metric that exports zombie process count.
Understanding PID 1 in Linux: PID 1 has special kernel treatment. It won't receive signals it hasn't explicitly registered handlers for (unlike other processes where unhandled signals use default behavior). This means SIGTERM won't kill PID 1 unless it handles SIGTERM. Init systems like tini register signal handlers and forward them to child processes.
The zombie lifecycle: A process becomes a zombie when it exits but its parent hasn't called wait(). The zombie exists so the parent can retrieve the exit status. When the parent exits, orphaned zombies are reparented to PID 1. If PID 1 doesn't reap them, they persist until the container stops.
Process table limits: While zombies don't use CPU or memory, they occupy a slot in the process table. Linux has a limit (typically 32768 by default, configurable via /proc/sys/kernel/pid_max). If zombies exhaust this limit, the system cannot create new processes.
Using s6-overlay for complex scenarios: For containers running multiple services, consider s6-overlay which provides:
FROM your-base
ADD https://github.com/just-containers/s6-overlay/releases/download/v3.1.5.0/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ENTRYPOINT ["/init"]s6 handles zombie reaping, service supervision, and graceful shutdown of multiple processes.
Kubernetes considerations: In Kubernetes, you can enable shareProcessNamespace to allow containers to see each other's processes, but this doesn't help with zombie reaping. Always use an init system in your container image:
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: myimage # Image should include tini/dumb-initDebugging zombie parent processes: To find what's creating zombies, check the PPID:
docker exec container ps -eo pid,ppid,stat,cmd | grep ZThe PPID shows the parent. If PPID is 1, the original parent already exited and the zombie was reparented.
image operating system "linux" cannot be used on this platform
How to fix 'image operating system linux cannot be used on this platform' in Docker
manifest unknown: manifest unknown
How to fix 'manifest unknown' in Docker
cannot open '/etc/passwd': Permission denied
How to fix 'cannot open: Permission denied' in Docker
Error response from daemon: failed to create the ipvlan port
How to fix 'failed to create the ipvlan port' in Docker
toomanyrequests: Rate exceeded for anonymous users
How to fix 'Rate exceeded for anonymous users' in Docker Hub