Skip to main content
RunBook Academy

Docker & ContainersII · Linux InternalsProcesses

Processes, signals, and PID 1

Intermediate⏱ ~22 min

What you'll learn

  • Explain the special signal semantics of PID 1 in a namespace
  • Choose between exec, tini, dumb-init, and a process manager
  • Diagnose why a container takes 10 seconds to stop

Prerequisites

Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-09

Not yet marked complete on this device.

Every container has a PID 1. It is the process that the OCI runtime invokes via execve(2). If that process does not handle signals correctly, the container cannot be stopped gracefully.

This is the root cause of “container takes 10 seconds to stop.” The default --stop-timeout is 10 seconds. If PID 1 ignores SIGTERM, docker stop waits the full timeout before sending SIGKILL.

The special semantics of PID 1

In Linux, when the kernel delivers a signal whose default action is to terminate the process, PID 1 is exempt. PID 1 only acts on signals that have an installed handler.

This is a feature for the system init process. The kernel assumes that PID 1 knows what it is doing and only acts when PID 1 chooses to. For container workloads, this feature becomes a footgun: most workloads do not know that they are PID 1 and do not install handlers.

sequenceDiagram
  participant D as docker stop
  participant K as Kernel
  participant PID1 as Container PID 1
  participant App as Application
  D->>K: send SIGTERM to PID 1
  alt PID 1 has a handler
    K->>PID1: deliver SIGTERM
    PID1->>App: forward or handle
    App-->>PID1: exit
    PID1-->>K: exit 0
  else PID 1 has no handler (e.g. shell)
    K->>PID1: ignore (default for PID 1)
    Note over K: wait --stop-timeout
    D->>K: timeout reached
    D->>K: send SIGKILL to PID 1
    K->>PID1: deliver SIGKILL
    PID1-->>K: killed (no cleanup)
  end

The single commonest cause: a shell-form CMD

Read-only / Safeis PID 1 a shell?
CONTAINER=web
docker inspect --format 'Path:  {{.Path}}{{"\n"}}Args:  {{json .Args}}' "$CONTAINER"
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
tr '\0' ' ' < "/proc/$PID/cmdline"; echo
Path:  /bin/sh
Args:  ["-c","python3 /app/server.py"]
/bin/sh -c python3 /app/server.py

Illustrative output

A healthy container looks like this instead — .Path is the application itself, and /proc/<pid>/cmdline agrees:

Path:  python3
Args:  ["/app/server.py"]
python3 /app/server.py
Service impact possiblemeasure it, do not guess
CONTAINER=web
time docker stop "$CONTAINER"
docker inspect --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' "$CONTAINER"
web

real	0m10.043s
user	0m0.019s
sys	0m0.011s
exit=137 oom=false

Illustrative output

The pair of numbers is the verification, and each one rules something out on its own:

  • Duration at the timeout, exit 137. 137 is 128 + 9: SIGKILL. With oom=false, nothing ran out of memory — the stop timeout expired. PID 1 ignored SIGTERM.
  • Fast stop, exit 143. 143 is 128 + 15: SIGTERM. The signal was delivered and acted on. This is the working case.
  • Fast stop, exit 0. PID 1 handled SIGTERM, ran its shutdown path, and exited deliberately. This is the best case.

Diagnosing a container that takes 10 seconds to stop

Read-only / Safeinspect stop behavior
docker inspect CONTAINER --format '{{.Path}}'
echo '---'
docker inspect CONTAINER --format '{{json .Config.Cmd}}'
Read-only / SafePID 1 details
PID=$(docker inspect --format '{{.State.Pid}}' CONTAINER)
cat /proc/$PID/status | grep -E '^(Name|Pid|State|PPid)'

The fix is one of:

  • --init — Docker injects tini as PID 1. tini installs default signal handlers and reaps zombies.
  • tini in the image — bake tini into your image and use it as the entrypoint.
  • dumb-init — alternative to tini with similar behavior.
  • Process manager — s6-overlay, supervisord, or systemd (in a container — for system services).
  • Application handles signals directly — fine for compiled apps in Go, Rust, etc. that respect SIGTERM by default.

A container PID 1 that handles signals correctly

Configuration change--init
docker run --init --name web -d nginx:1.27
Configuration changetini via ENTRYPOINT
# Dockerfile
COPY --from=ghcr.io/krallin/tini:latest /usr/local/bin/tini /sbin/tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["python3", "/app/server.py"]

What other signals matter

  • SIGINT (2) — interrupt, default action: terminate. Usually same handling as SIGTERM.
  • SIGQUIT (3) — quit, default action: core dump. Rarely handled.
  • SIGHUP (1) — hangup, default: terminate. Used by some apps to trigger config reload (nginx, sshd).
  • SIGUSR1 (10) / SIGUSR2 (12) — user-defined. Java apps often use SIGUSR1 for heap dump.
  • SIGCHLD (17) — child terminated. PID 1 needs to wait() on this to reap zombies.

Graceful shutdown in practice

A production container should:

  1. Receive SIGTERM.
  2. Stop accepting new connections (mark unhealthy on health check).
  3. Drain in-flight requests (typical grace: 10–30 seconds).
  4. Close database connections cleanly.
  5. Flush logs / metrics.
  6. Exit 0.

The container’s --stop-timeout should be at least as long as the application’s graceful shutdown timeout, plus a buffer. Anything shorter forces SIGKILL before the app has finished draining.

Knowledge check

Knowledge check · 6 questions

  1. Q1. PID 1 inside a container is special because:

  2. Q2. A shell as PID 1 will receive SIGTERM and shut down cleanly.

  3. Q3. Name the small init process designed to forward signals and reap zombies for containers.

  4. Q4. A container always takes exactly 10 seconds to stop and exits 137, with State.OOMKilled false. docker inspect shows Path=/bin/sh and Args=["-c","python3 /app/server.py"]. What should you change?

  5. Q5. Which of these result in the application process receiving SIGTERM directly as PID 1? Select all that apply.

  6. Q6. Because the default STOPSIGNAL is SIGTERM, every official image shuts down gracefully on `docker stop` without further configuration.

Passing score: 75%. Answers are checked in this browser.