The first process in a container is PID 1 and takes on kernel responsibilities, most importantly reaping orphaned child processes and forwarding signals. If your application runs as PID 1 without those duties, you get zombies and containers that ignore docker stop.
Before you start
You should understand processes, signals and the container lifecycle. This article covers PID 1 duties and the init shim.
Step-by-step walkthrough
Step 1: PID 1 must reap children
When a child process exits, its parent must call wait to collect its exit status; otherwise it becomes a zombie. As PID 1, an application inherits orphaned processes and must reap them, which most applications do not do.
Step 2: Put the command in exec form
Use the JSON exec form CMD ["node", "server.js"], not the shell form CMD node server.js. The shell form runs the command under /bin/sh, so /bin/sh is PID 1 and the app is a child; the shell may not forward SIGTERM, and the app never sees it, so docker stop times out and escalates to SIGKILL.
Step 3: Use an init shim
Adding --init (or an explicit tini) inserts a tiny init as PID 1 that reaps zombies and forwards signals to your process. It is the simplest fix and costs almost nothing, so most containers should use it.
Worked scenario
The shim reaps children and forwards signals.
FROM node:22-alpine
WORKDIR /app
COPY . .
# exec form keeps the app as the direct target of signals
CMD ["node", "server.js"]
# run with: docker run --init ...Walk through the example
The exec form ensures the app, not a shell, receives SIGTERM, so it can shut down gracefully. Running with --init puts the init shim at PID 1, which forwards the signal and reaps any orphaned children. Together they make the container stop cleanly and avoid zombies.
Common mistake
Using the shell form of CMD, so signals go to the shell and the app is killed hard. Another is running multi-process containers without an init, so exited children become zombies.
Verify the behavior
Send docker stop with the shell form and observe the timeout and hard kill; switch to exec form and confirm a fast, graceful exit. Run a child process that exits and confirm zombies accumulate without --init and are reaped with it.
Interview exercise
Why does the shell form of CMD break graceful shutdown?
Answer and reasoning
It runs /bin/sh -c "node server.js", so the shell is PID 1 and the app is its child. The shell often does not forward SIGTERM, so the app never receives the stop signal, and the runtime waits out the grace period before sending SIGKILL. The exec form makes the app PID 1 and the direct signal target.
Continue learning
Compare signal handling in Docker signal handling and Entrypoint vs CMD. Read the Docker init documentation and try the Docker interview questions.