fivenines
23/40
Lesson 23 · Runtime

Stopping Gracefully

In one sentence: stop is a negotiation — SIGTERM, a grace period (default 10 s), then SIGKILL — and it only works if the container's PID 1 actually receives and handles signals, which is why init processes matter.

You already know

  • From Lesson 4: the container's main process is PID 1 in its own PID namespace.
  • From Lesson 21: "user stopped it" travels with the exit event and gates restarts.

Ask, wait, insist

SIGTERM means "please finish up": close connections, flush buffers, release locks. SIGKILL means "you no longer exist" — unhandleable, uncleanable. The grace period between them is the app's contract with the platform: handle TERM within N seconds or lose the right to clean up. The engine picks N per workload — 10 s suits a web server; a database checkpointing to disk may need minutes. Exit codes tell the story afterward: 143 (128+15) means TERM was honored; 137 (128+9) means the deadline passed and KILL fired.

The PID 1 trap

Two kernel quirks make PID 1 special inside a namespace: signals with default handlers are ignored for it (an app that never installs a TERM handler simply won't die politely), and orphaned child processes reparent to it, becoming zombies unless someone reaps them. Shell-wrapper entrypoints hit both — the shell holds PID 1, doesn't forward signals, doesn't reap. The fix is a tiny init as PID 1 that forwards signals to the real app and reaps orphans.

Architecture — why a tiny init sits at PID 1
flowchart TB
  E["engine: stop c-42"] --> SH["shim"]
  SH -- "SIGTERM" --> I["tiny init (PID 1)
forwards signals · reaps zombies"] I -- "forward TERM" --> APP["app (PID 2)
closes conns, flushes"] APP -. "orphaned children
reparent here" .-> I SH -. "if deadline passes:
SIGKILL (unblockable)" .-> I
Sequence — a graceful stop and a stubborn one
sequenceDiagram
  participant E as Engine
  participant S as Shim
  participant P as PID 1
  E->>S: stop (grace: 10s)
  S->>P: SIGTERM
  alt app handles TERM in time
    P->>P: cleanup, exit(0)
    S-->>E: exited 143 — graceful
  else deadline passes
    S->>P: SIGKILL
    S-->>E: exited 137 — forced, no cleanup ran
  end
  E->>E: mark "user-stopped" (restart policies stand down — L21)
    

Anchor these

  • stop = TERM → grace period → KILL. The grace period is the app's cleanup contract.
  • PID 1 ignores unhandled signals and inherits orphans — use a tiny init.
  • Exit 143 = graceful; 137 = the deadline won. The code is the audit trail.
Next step

See what actually stuck.

Take the practice scenarios now.