Skip to main content
KBY Technologies Logo
cd ../lexicon
sys/docs/lexicon/pid-1-problem-container-init-process.md
Lexicon Entry

PID 1 Problem (Container Init Process)

In a Linux PID namespace, the first process (PID 1) inherits kernel-level init responsibilities—reaping zombie children and receiving default signal dispositions—that ordinary application binaries do not implement. This mismatch causes zombie process accumulation and broken SIGTERM propagation inside containers, undermining graceful shutdown guarantees expected by orchestrators.

Practical example

A Node.js image defined as `CMD ["npm", "start"]` runs npm as PID 1; on pod deletion, kubelet's SIGTERM hits npm, which does not forward it to the spawned `node` child. The pod hangs for the full 30s grace period, then gets SIGKILL, dropping active WebSocket connections. Switching to `ENTRYPOINT ["tini", "--"]` with `CMD ["node", "server.js"]` fixes signal propagation and enables reaping.

The Linux kernel treats PID 1 specially: any signal sent to it that lacks an explicit handler is ignored by default, rather than applying the standard default action (terminate, core dump, etc.). This exception exists because traditional init systems (systemd, sysvinit) must never die accidentally. When a container runtime creates a new PID namespace, whatever process you designate as the entrypoint becomes PID 1 in that namespace and inherits this exact semantic—regardless of whether it was ever designed to be an init system.

Most application binaries, shell scripts, and interpreted runtimes (Node.js, Python, Java) do not install a SIGTERM handler and do not call wait()/waitpid() on orphaned descendants. Two failure modes result: (1) zombie accumulation—child processes that exit are reparented to PID 1 and never reaped, eventually exhausting the PID namespace or leaking file descriptors; (2) signal black-holing—when CMD ["sh", "-c", "node server.js"] is used, the shell becomes PID 1 and, in many shells, does not forward SIGTERM to its child process tree at all, so the actual application never sees the termination request.

This directly collides with Kubernetes’ pod termination lifecycle: kubelet sends SIGTERM to the container’s PID 1, waits terminationGracePeriodSeconds (default 30s), then sends SIGKILL. If PID 1 ignores or fails to propagate SIGTERM, every pod termination silently degrades into a hard SIGKILL after the full grace period—dropping in-flight connections, aborting transactions, and inflating shutdown latency across rolling deployments. Sidecar-heavy pods and multi-process containers amplify this because orphan reparenting and signal fan-out both scale with process tree depth.

The standard mitigation is inserting a minimal init shim—tini, dumb-init, or Docker’s built-in --init flag (which wraps tini)—as PID 1. These shims correctly reap zombies via a SIGCHLD loop and forward received signals to the real application process group. Heavier alternatives like s6-overlay add supervised multi-process management for images intentionally running several daemons. Statically compiled binaries (Go, Rust) that explicitly register signal handlers and have no subprocesses can safely run directly as PID 1 without a wrapper, since they satisfy the reaping/signal contract themselves.