Container Lifecycle and PID 1 Behavior: Signal Propagation and Process Management
Understanding why docker stop hangs for 10 seconds, zombie process reaping, and init systems.
Part 10 in Series — Catch up on the previous article: From Image to Container: Read-Only Layers, Writable Layers, and Execution State (Part 9) before diving into this post.
During a routine application rolling deployment, a developer executes:
docker stop order-processing-service
Instead of stopping instantly, the terminal hangs for 10 solid seconds.
After 10 seconds, the container abruptly terminates. System logs reveal a critical error: in-flight customer order payments that were midway through network processing were ungracefully killed, leaving orphaned pending charges in the database.
Why did docker stop wait 10 seconds before violently killing the container process?
The root cause is a fundamental rule of the Linux operating system kernel regarding Process ID 1 (PID 1) Signal Handling.
1. The Unique Status of PID 1 in Linux
Inside a traditional Linux virtual machine or bare-metal host, Process ID 1 is reserved for the system init daemon (such as systemd or sysvinit).
Inside a Linux container PID namespace, the primary process specified in the ENTRYPOINT or CMD instruction becomes PID 1.
[ Traditional Host Process Tree ] [ Container PID Namespace ]
PID 1: systemd (Reaps zombies, routes signals) PID 1: node app.js (Your app binary!)
├── PID 402: rsyslogd └── PID 12: worker process
└── PID 810: nginx
The Linux kernel treats PID 1 completely differently from all other processes:
- Ignored Default Signal Handlers: For normal processes (PID > 1), if no custom signal handler is registered for
SIGTERM, the kernel invokes a default action (terminating the process). For PID 1, the kernel disables default signal handlers. If PID 1 has not registered an explicit custom handler forSIGTERM, the kernel ignores the signal entirely! - Zombie Process Reaping Responsibility: When a child process terminates, it enters a
Zombie(Z) state until its parent process invokeswait()orwaitpid()to read its exit code. If a parent process dies, orphan processes are re-parented to PID 1. PID 1 must continuously reap orphan zombie processes, or system process IDs will leak until the host exhausts PIDs.
2. Anatomy of a docker stop Sequence
When you invoke docker stop <container_id>, the Docker daemon executes a two-stage shutdown protocol:
Docker Daemon Container PID 1
| |
|--- 1. Sends SIGTERM ------------------------------>|
| | (If PID 1 ignores SIGTERM...)
| [ Waits 10 Second Grace Period Timer ] | (Process keeps running!)
| |
|--- 2. Timer Expires! Sends SIGKILL -------------->|
v v
(Forced Termination!) (Abrupt Crash!)
- Stage 1 (
SIGTERM): Docker sends aSIGTERMsignal to PID 1, signaling that the process should flush file buffers, close database connection pools, and exit gracefully. - Stage 2 (
SIGKILL): Docker starts a 10-second grace period timer. If PID 1 is still running when the timer expires, Docker sendsSIGKILL(a kernel-level signal that cannot be caught, blocked, or ignored), instantly killing the process memory space.
If your container process runs wrapped in a shell script, /bin/sh runs as PID 1 and ignores SIGTERM, causing every docker stop command to hang for 10 seconds before being ungracefully killed by SIGKILL!
3. The Shell Form Trap vs Exec Form
The single most common cause of PID 1 signal handling failure is using Shell Form in Dockerfile instructions.
The Shell Form Problem
# Shell Form:
CMD node server.js
When Docker parses CMD node server.js, it wraps the command in a shell executable:
/bin/sh -c "node server.js"
Process Tree inside Container:
- PID 1:
/bin/sh - PID 7:
node server.js
When docker stop sends SIGTERM to PID 1 (/bin/sh), /bin/sh does not forward the signal to node server.js. The node application never receives SIGTERM, keeps running, and gets forcefully terminated by SIGKILL 10 seconds later.
The Exec Form Solution
# Exec Form (JSON Array):
CMD ["node", "server.js"]
Process Tree inside Container:
- PID 1:
node
node is executed directly via execve(2) as PID 1. When docker stop sends SIGTERM, node receives the signal immediately, executes its shutdown hooks, and exits within 50 milliseconds!
4. Reaping Zombies with Init Wrappers (tini)
If an application spawns background worker child processes (for example, a Python or Node service invoking child_process.fork()), those child processes may become orphaned zombies if the worker crashes.
Because standard application binaries are not designed to act as init systems, they fail to reap zombies.
To solve this, Docker includes a lightweight init system called Tini.
# Enable Tini init wrapper during docker run:
docker run -d --init --name web-app my-image:v1.0
When --init is enabled:
- Tini runs as PID 1.
- Tini handles
SIGTERMpropagation, forwarding signals cleanly to your application process. - Tini reaps any orphan zombie child processes automatically, preventing PID memory leaks.
Container Process Tree with Tini:
PID 1: docker-init (tini) <-- Handles signals & reaps zombies cleanly!
└── PID 6: node server.js
Summary & Next Steps
Container process management requires adhering to Linux kernel PID 1 mechanics:
- Linux PID 1 ignores default
SIGTERMactions unless an explicit handler is registered. docker stopsendsSIGTERM, waits 10 seconds, then issuesSIGKILLif the container process has not exited.- Shell Form (
CMD node app.js) puts/bin/shat PID 1, blocking signal forwarding. - Exec Form (
CMD ["node", "app.js"]) puts the application binary directly at PID 1. - Lightweight Init Systems (
tini/--init) provide proper signal forwarding and zombie process reaping for complex multi-process containers.
In the next article, we examine Enforcing Container Resource Limits: cgroups v1 vs v2 Memory and CPU Limits.
References & Further Reading
- Linux Kernel Organization. Linux System Calls: clone(2), unshare(2), setns(2), pivot_root(2). Linux Man Pages.
- Open Container Initiative. OCI Runtime Specification v1.0.2. OCI Standard.
- Go Standard Library. Package
syscallandgolang.org/x/sys/unix. Go Language Docs.
Part 11: Enforcing Container Resource Limits: cgroups v1 vs v2 Memory and CPU Limits
Continue to Part 11 →