The first process
A Socratic walk-through of the first process — reasoned out one step at a time, not lectured.
The question we started with
THE QUESTION #Why does a program that stops cleanly on your laptop refuse to stop inside a container?
Run your service in a terminal, press Ctrl-C, and it shuts down politely: finishes the request in flight, closes its database connections, exits. Put the identical binary in a container, ask the runtime to stop it, and nothing happens for ten seconds — then it dies abruptly, mid-request, connections dropped.
Same code, same signal, different outcome. That is the interesting part. Nothing about the program changed, so whatever changed must be about the position the program occupies. And it did change position: inside the container it is process number one.
Reasoning it through
REASONING #Ask first what a graceful shutdown actually depends on. It depends on the process receiving SIGTERM and having installed a handler that runs cleanup before exiting. If no handler is installed, the kernel applies a default action — for SIGTERM the default is to terminate immediately, which is abrupt but at least prompt.
Now the crucial fact. The kernel treats process ID 1 specially: for PID 1, default signal actions are not applied. A signal with no explicit handler installed is simply discarded. This exists to protect the system's init process from being killed by a stray signal, and it applies equally to whatever happens to be PID 1 inside a container's process namespace — the kernel does not know or care that your web server was never meant to be an init.
So follow the consequence. A program that relied on the kernel's default SIGTERM behaviour, and never installed a handler, gets nothing at all when the runtime asks it to stop. The runtime waits its grace period — ten seconds by default in Docker — and then sends SIGKILL, which cannot be caught or ignored by anyone, PID 1 included. That is the abrupt death you observed, and it explains a shutdown that takes exactly ten seconds every time.
But there is a second, more common cause, and it is worth separating. Write the start command in shell form and the runtime executes /bin/sh -c "your command". Now the shell is PID 1 and your program is its child. Ask what a plain shell does when it receives SIGTERM while waiting on a child: it does not forward it. Your program is sitting one level down, entirely unaware that anything was asked of it, while the shell above it either ignores the signal or dies without passing it on. The program has a perfectly good handler and never gets the chance to run it.
Notice these are different failures with the same symptom. In the first, the signal arrives at the right process and is discarded. In the second, the signal never reaches the right process at all. Both are fixed by different things, which is why "add a signal handler" sometimes helps and sometimes does nothing.
There is a third duty of PID 1 that is easy to miss. When any process exits, it stays in the process table as a zombie until its parent reads its exit status. If a process's parent dies first, the orphan is re-parented to PID 1, which is expected to reap it. A real init does this in a loop. Your web server does not — so in a container that spawns subprocesses whose parents exit, zombies accumulate against the namespace's process limit, and the container eventually cannot fork. It is a slow leak, not a crash, which is why it usually surfaces days later.
So what follows? Three practical moves. Use the exec form of the start command so your binary really is PID 1, with no shell in the way. Install an explicit SIGTERM handler, since as PID 1 you cannot rely on the default. And if your process genuinely spawns children, put a minimal init at PID 1 — tini, or Docker's --init flag, which does exactly that — so signals are forwarded down and orphans are reaped. Where an entrypoint script is unavoidable, end it with exec "$@" so the script replaces itself with your program rather than staying above it as a parent.
The analogy
THE ANALOGY #Think of a building where every instruction from outside comes through the person at the front desk. A normal employee who ignores a fire alarm is escorted out by the alarm system itself. But the front desk has a standing exemption: alarms it has not explicitly agreed to respond to are simply not passed on to it, because the building's designers assumed the person at that desk was the one running the evacuation and knew what they were doing.
Put an ordinary employee at that desk and the exemption follows the desk, not the person. The alarm sounds, nothing happens, and eventually the building is cleared by force.
a person at the desk would at least hear the alarm and could decide to act, whereas PID 1 without a handler never observes the signal at all — there is nothing to ignore, because the kernel discards it before the process could know it was sent.
Clarifying the model
THE MODEL #Three refinements.
First, the misconception worth naming: this is often read as a container runtime failing to stop things properly. The runtime does exactly what it says — SIGTERM, then SIGKILL after the grace period. The unusual behaviour is entirely in the kernel's PID 1 rule, which predates containers by decades and was never written with them in mind.
Second, SIGKILL is not a workaround. It cannot be handled by design, so shortening the grace period to make stops faster simply guarantees ungraceful ones. If shutdown is being cut short, the fix is upstream — make the signal arrive at a process that will act on it — not downstream in the timeout.
Third, being PID 1 is a role, not a privilege. Nothing about it grants extra power; it imposes extra duties — handle your own signals, forward what your children need, reap orphans. The reason so many images get this wrong is that these duties are invisible in every other context a program is ever run in.
A picture of it
THE PICTURE #How to readRead the top half as the broken case and the bottom half as the fixed one. In the top half, follow the crossed arrows: the signal stops at the shell, so the application never learns anything until the unblockable kill arrives after the grace period. In the bottom half the shell is gone entirely — the runtime's signal lands directly on the application, whose own handler does the draining and exits before the timer matters. The difference between the two halves is one word in the start command, not a change to the program.
What became clearer
WHAT CLEARED #A container gives your program the job of init without telling it, and the kernel's PID 1 rule means an unhandled signal is not merely ignored but never delivered. Graceful shutdown therefore depends on two things being true at once: the signal must reach your process rather than a shell standing above it, and your process must have explicitly asked to hear it.
Where to go next
ONWARD #- What
tiniand--initactually do, in about a hundred lines. - Kubernetes
preStophooks andterminationGracePeriodSeconds, and how they interact with this. - Zombie reaping: how to spot the accumulation before it exhausts the process limit.
Key terms
TERMS #| Term | What it means |
|---|---|
| PID 1 | the first process in a process namespace, for which the kernel does not apply default signal actions. |
| SIGTERM | the polite termination request, catchable so a program can shut down cleanly. |
| SIGKILL | the unconditional termination signal, which no process can catch or ignore. |
| Exec form | the start-command syntax that runs a binary directly rather than through a shell. |
| Reaping | reading a terminated child's exit status so the kernel can free its process table entry. |
| tini | a minimal init program that forwards signals to its child and reaps orphaned processes. |
Every term the collection defines is gathered in the glossary.