Why does a program running as PID 1 inside a container often ignore SIGTERM entirely, even though the same binary shuts down correctly when you run it on a normal Linux host?
answer
- SIG_DFL is discarded for PID 1
- Kernel protects init from being killed by accident
- Container PID namespace → your app IS init
- No handler installed = SIGTERM does nothing
- /proc/1/status SigCgt bitmask; tini/--init restores defaults
basics
~20 sThe Linux kernel treats PID 1 specially: signals with no explicitly installed handler are discarded instead of applying their default action. On a host your process is not PID 1, so SIGTERM's default action (terminate) applies. As PID 1, no handler means nothing happens.
solid answer
~60 sEvery signal has a *disposition*: a user-installed handler, SIG_IGN, or the kernel's default action. For SIGTERM the default action is to terminate the process — which is why `kill <pid>` works on an ordinary host process that never wrote a line of signal code. The kernel deliberately exempts PID 1 from default actions. On a real machine PID 1 is init/systemd, and letting a stray SIGTERM kill it would panic the system, so the kernel drops signals sent to PID 1 unless that process has explicitly installed a handler. Containers get their own PID namespace, and the same rule applies to the namespace's PID 1. Consequence: your app running as container PID 1 must *explicitly* handle SIGTERM. If it does not, `docker stop` appears to do nothing, the grace period expires, and SIGKILL — which is never deliverable to a handler and is not subject to the PID 1 exemption — ends it with exit 137. The fixes are to install a handler in the app, or run a real init such as tini via `docker run --init`.
code
bash · 3 linesdocker exec api grep -E 'SigCgt|SigIgn' /proc/1/status
# SigCgt: 0000000000000000 -> catches nothing; docker stop will time out
# SigCgt: 0000000180004a03 -> SIGTERM among others is caughtgo deeper
It is enough to know that PID 1 is special: a process that installs no SIGTERM handler simply ignores it inside a container, so apps must handle it explicitly.
State the rule precisely — the kernel discards SIG_DFL signals for a PID namespace's init — and contrast with the same binary at a normal PID on a host.
Diagnose it live (/proc/1/status SigCgt, stop timing, exit 137), distinguish it from the shell-wrapper bug, and pick between fixing the app and inserting tini.
Treat graceful shutdown as a platform contract: base images ship an init, services must handle SIGTERM, and stop timeouts are set consistently across local Docker and the orchestrator.
## Signal dispositions, briefly A POSIX signal is an asynchronous notification delivered to a process. For each signal a process has a *disposition*, one of three things: 1. A **handler function** the process registered with `signal()`/`sigaction()`. 2. **SIG_IGN** — explicitly ignore it. 3. **SIG_DFL** — the kernel's default action for that signal. Default actions vary: SIGTERM, SIGINT, SIGHUP and SIGQUIT terminate; SIGCHLD and SIGWINCH are ignored; SIGSTOP suspends. SIGKILL and SIGSTOP are special — they cannot be caught, blocked or ignored at all. This is why `kill 12345` kills an ordinary process that contains no signal-handling code whatsoever: no handler is installed, so SIG_DFL applies, and SIGTERM's default action is termination. ## The PID 1 exemption The Linux kernel makes one exception. In `kernel/signal.c` the delivery path checks whether the target is the init process of its PID namespace, and if the signal's disposition is SIG_DFL, the signal is **discarded** rather than acted upon. Only signals with an explicitly installed handler (or SIGKILL/SIGSTOP sent from *outside* the namespace) reach PID 1. The rationale is system integrity. On a bare host PID 1 is init or systemd. If any process could `kill -TERM 1` and take down init, the machine would panic instantly. The kernel therefore refuses to apply default lethal actions to PID 1, making init killable only by code it wrote itself. ## Why containers inherit the problem A container gets its own PID namespace (`CLONE_NEWPID`). Inside it, the first process is numbered 1 and is, as far as the kernel is concerned, the namespace's init — even though it is really your Spring Boot jar, your nginx, your Python script. The exemption applies verbatim. So the exact same binary behaves differently in two contexts: - On a host, as PID 4711: `kill 4711` → SIG_DFL → terminated. Works, no code needed. - In a container, as PID 1: `docker stop` → SIGTERM → no handler → **discarded**. Nothing happens. Ten seconds later SIGKILL, exit 137. A nasty corollary for debugging: SIGKILL sent from the host (which is what Docker does, from outside the namespace) still works, because the exemption protects a namespace's init from signals *within* the namespace and from default actions, not from an external SIGKILL. So the container always eventually dies — it just dies violently, which is why the bug hides so well. It looks like it works; it just takes ten seconds and loses in-flight work. This also explains why `docker exec <c> kill -TERM 1` typically does nothing at all, and why `Ctrl+C` in `docker run -it` (which sends SIGINT to PID 1) sometimes fails to stop a container. ## Fixes, in order **1. Handle the signal in the application.** This is the correct fix, because only the application knows what "drain cleanly" means. Every mainstream runtime supports it: - Go: `signal.Notify(ch, syscall.SIGTERM)`. - Node: `process.on('SIGTERM', () => server.close(...))`. - Python: `signal.signal(signal.SIGTERM, handler)`. - Java/JVM: `Runtime.getRuntime().addShutdownHook(...)`; Spring Boot registers one and supports graceful web-server shutdown. - nginx, Postgres, Redis and other mature servers already install handlers — this is why they stop fast. **2. Run a real init as PID 1.** `docker run --init` inserts Docker's bundled **tini** as PID 1; tini installs handlers, forwards signals to your process (which is now PID 2 and therefore subject to normal default actions), and reaps zombies. You can also bake `ENTRYPOINT ["/sbin/tini","--"]` into the image, or use `dumb-init`. This makes the *default action* work again for programs you cannot modify. **3. Make sure the signal reaches the right process at all.** The PID 1 exemption is often confused with the shell-wrapper problem: shell-form `CMD` puts `/bin/sh -c` at PID 1, and the shell both fails to forward and is itself exempt. Use exec form or `exec "$@"`. In practice these two bugs frequently coexist in the same image. ## Diagnosing it Inside a running container, `cat /proc/1/status` shows `SigCgt` — a hex bitmask of caught signals. Bit 15 (value 0x4000) set means SIGTERM is caught. `SigCgt: 0000000000000000` means the process catches nothing and will ignore every graceful stop you send. Combine with `time docker stop` and the exit code: fast exit and code 0 is healthy; exactly-the-timeout and 137 is not.
- If PID 1 ignores signals with no handler, how does Docker ever manage to stop such a container?With SIGKILL after the grace period. SIGKILL can never be caught, blocked or ignored, and the PID 1 exemption covers default actions for catchable signals, not an external SIGKILL delivered from outside the namespace. The container dies — just abruptly, with exit code 137 and no cleanup.
- How would you prove, from inside a running container, that PID 1 has no SIGTERM handler?Read `/proc/1/status` and inspect the `SigCgt` field, a hex bitmask of caught signals. All zeros means the process installed no handlers at all. SIGTERM is signal 15, so bit 15 — hex 0x4000 — must be set in that mask for a graceful stop to have any effect.
- Does `docker run --init` remove the need for the application to handle SIGTERM?No. tini forwards SIGTERM to your process, which is now PID 2 and therefore subject to the normal default action of terminating — so the container stops promptly. But terminating is not draining: without an application handler you still drop in-flight requests and skip flushes. `--init` fixes signal *delivery* and zombie reaping, not shutdown *semantics*.
The kernel gives PID 1 diplomatic immunity so a stray signal cannot topple the government. Your app inherits that immunity by accident — and then ignores the notice it was supposed to act on.
saying these in an interview costs you the question
- Claiming SIGTERM always kills a process regardless of PID
- Confusing this with the shell-wrapper problem and stopping there, without knowing the kernel exemption
- Thinking Docker cannot stop such a container at all, rather than SIGKILLing it after the timeout
- Believing SIGKILL can be handled or that PID 1 is immune to it
- Assuming --init or tini gives you graceful draining without any application code