open atlas
↑ Back to track
Docker, containers as a system DOCK · 02 · 04

The container lifecycle: from docker run to a reaped exit

A container's life is dockerd → containerd → shim → runc. runc sets up namespaces/cgroups, execs your binary, and exits — the shim stays as PID parent. Stop is SIGTERM, a 10 s grace, then SIGKILL; signals only reach PID 1 if it forwards them.

DOCK Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

Deploys had a tax nobody could explain: every rollout added about ten seconds per pod of pure dead time. The app logged “shutting down gracefully,” the orchestrator logged the SIGTERM, yet the container always lingered the full grace period before dying. The smoking gun was the entrypoint: ENTRYPOINT sh -c "node server.js". The shell was PID 1. When the orchestrator sent SIGTERM, it went to PID 1 — the shell — which has no SIGTERM handler and, as PID 1, receives no default action from the kernel, so it simply ignored the signal. node never saw it, kept serving, and only died when the grace timer expired and the runtime escalated to SIGKILL — exactly ten seconds later, every single time. The fix was the exec form, ENTRYPOINT ["node", "server.js"], so node was PID 1 and received SIGTERM directly. The ten-second tax was the lifecycle’s stop sequence colliding with a shell that swallowed the signal.

The chain that starts a container

docker run does not directly spawn your process. There is a deliberate chain, each layer with one job:

  • dockerd (the Docker daemon) — the API and image management; it does not parent containers.
  • containerd — the container supervisor: pulls images, manages the lifecycle, talks to the runtime.
  • containerd-shim — one per container; it is the lasting parent of the container process. The shim is what lets containerd (and even dockerd) restart without killing running containers, keeps the stdio open, and reports the exit code.
  • runc — the OCI runtime. It does the actual kernel work: clone()/unshare() the namespaces, write the cgroup limits, set capabilities and the seccomp filter, pivot_root into the image, then execve() your binary so it replaces runc itself. runc then exits — it is not a running supervisor, just a setup tool.

Together, these four layers separate concerns so precisely that each can fail or be upgraded in isolation: dockerd can restart without touching running containers, containerd can be patched while the shim holds the process, and runc can be replaced with a different OCI runtime without changing the supervisor chain. Without the shim, any containerd restart would orphan every running container.

That last point surprises people: after start, runc is gone. Your process is now PID 1 in its namespace, parented (from the host’s view) by the shim. This is why containers are cheap to run thousands of: no fat per-container daemon, just a tiny shim and your process directly on the host kernel.

dockerd ──API──▶ containerd ──▶ containerd-shim-runc-v2  (stays as parent)

                                      └─▶ runc create/start
                                            ├─ unshare namespaces
                                            ├─ apply cgroup limits
                                            ├─ set caps + seccomp
                                            ├─ pivot_root → image
                                            └─ execve(entrypoint)  ← becomes PID 1
                                      runc exits; shim reaps the exit code

Stop: SIGTERM, grace, SIGKILL

Stopping a container is not a kill — it is a negotiation with a deadline. docker stop (and Kubernetes pod termination) sends SIGTERM to PID 1, waits a grace period (Docker default 10 s; Kubernetes terminationGracePeriodSeconds default 30 s), and if the process is still alive, sends SIGKILL, which the process cannot catch or ignore. The contract is: PID 1 should handle SIGTERM, stop accepting new work, drain in-flight requests, and exit before the timer. The catch is the one from the Hook: signals are delivered to PID 1, and PID 1 alone — if PID 1 is a shell or a wrapper that does not forward signals to its children, the real server never hears SIGTERM and the grace period is pure latency before the SIGKILL. This is the operational reason the exec form (ENTRYPOINT ["app"]) and init shims (--init, tini) matter: they put a signal-aware process at PID 1.

Quiz

After runc starts a container, which process is the lasting parent of the container's PID 1 on the host, and what role does runc play afterward?

Why this works

Why a shim at all — why not let containerd be the parent? Decoupling. If containerd were the direct parent, restarting or upgrading containerd would orphan or kill every running container, and you could never patch the supervisor without downtime. The shim is a tiny, long-lived process that owns exactly one container’s stdio and exit status, so containerd (and dockerd) can crash, restart, or upgrade while your containers keep running and their exit codes are still captured when they finish. It is the same separation-of-concerns instinct as the lifecycle chain itself: each layer can fail or be replaced without taking the others down.

Pause, restart, and the states

When you see a container listed in docker ps -a long after it stopped, you might wonder why it’s still there. The answer is in how the runtime tracks state.

Between created and exited a container moves through states the runtime tracks: created (namespaces and cgroups set up, entrypoint not yet running), running, paused, stopped/exited. Pause is its own mechanism — the cgroup freezer (cgroup.freeze) suspends every process in the cgroup without sending a signal, so the process cannot react and no SIGSTOP handler runs; it is frozen mid-instruction and thaws exactly where it was. Restart policies (--restart=on-failure, unless-stopped, always) are containerd watching the exit code and re-running the same setup chain. The key mental model: a stopped container still exists as a record (its writable layer, its config) until removed — docker ps -a shows it — which is why an exited container’s logs and exit code remain inspectable, and why a restart re-execs the entrypoint rather than resuming a frozen process.

Quiz

A container's entrypoint is a shell wrapper (sh -c 'exec-less') that does not forward signals. On docker stop, what happens during the grace period and why?

Recall before you leave
  1. 01
    Walk the start chain from docker run to a running PID 1, naming each layer's job and what runc does and becomes.
  2. 02
    Describe the stop sequence and explain why a shell entrypoint can make a container take the full grace period to die.
Recap

A container’s life is a chain of cooperating processes, not a monolith. docker run reaches dockerd, the API daemon that manages images but deliberately does not parent containers; dockerd calls containerd, the supervisor that pulls images and drives the lifecycle; containerd starts a containerd-shim per container, and that shim is the lasting parent — it holds the container’s stdio and exit code so containerd and dockerd can restart or upgrade without killing running containers. The shim invokes runc, the OCI runtime, which performs the real kernel work: unshare the namespaces, write the cgroup limits, set capabilities and the seccomp filter, pivot_root into the image, and finally execve() the entrypoint so your binary replaces runc and becomes PID 1. runc then exits — it is a setup tool, which is why running thousands of containers is cheap: a tiny shim and your process directly on the shared kernel, no per-container daemon. Stopping is a negotiation with a deadline: SIGTERM goes to PID 1, the runtime waits a grace period (10 s in Docker, 30 s in Kubernetes), and then sends an uncatchable SIGKILL. Because signals reach only PID 1, a shell or wrapper that does not forward them swallows SIGTERM and burns the whole grace period as dead latency before the kill — fixed with the exec form or an init shim that sits, signal-aware, at PID 1. Pause is the cgroup freezer suspending the cgroup without any signal; restart policies re-run the same setup chain; and a stopped container persists as an inspectable record, with logs and exit code, until it is removed. Now when you see a rollout taking ten extra seconds per pod, or a container that ignores docker stop, check the entrypoint first: is PID 1 your app or a shell wrapper?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.