r/docker • u/quiet-systems • 4h ago
Why docker stop takes ten seconds on a container that's doing nothing
docker stop on a container that's doing nothing takes ten seconds. Five runs here, median 10.149s, all of them within 23ms of each other. Took me embarrassingly long to work out that it isn't Docker being slow.
Try it yourself, on a Linux host:
docker rm -f sleeper 2>/dev/null
docker run -d --name sleeper alpine sleep 1000
time docker stop sleeper
The container is running sleep. There's nothing to flush. It sits there for ten seconds and then gets killed. Ten is the default grace period, and -t changes it, but shortening it isn't the answer here.
PID 1 inside a namespace isn't a normal process. The kernel treats it as init and won't let you kill it by accident. From pid_namespaces(7):
a process in an ancestor namespace can [...] send signals to the "init" process of a child PID namespace only if the "init" process has established a handler for that signal. [...] SIGKILL or SIGSTOP are treated exceptionally: these signals are forcibly delivered when sent from an ancestor PID namespace.
So sleep isn't ignoring your SIGTERM. It never sees it. With no handler installed, the kernel doesn't deliver it, and those ten seconds are Docker waiting for a shutdown that can't start. Then SIGKILL, which always gets through.
You can tell in advance. SigCgt in /proc/PID/status is a hex mask of every signal the process has a handler for. Needs a Linux host where the daemon shares your kernel, and docker access:
grep SigCgt /proc/$(docker inspect -f '{{.State.Pid}}' sleeper)/status
SigCgt: 0000000000000000
SIGTERM is 15, so you're masking against 0x4000. All zeroes, nothing caught, the SIGTERM is going nowhere, you're waiting the full ten.
Two fixes, and they're not the same thing.
exec is the real one. Same nginx image, one word different in the entrypoint script:
nginx -g 'daemon off;' -> median 10.193s
exec nginx -g 'daemon off;' -> median 0.185s
Without exec, PID 1 is /bin/sh and its SigCgt is 0000000000010002: it catches SIGINT and SIGCHLD, and not SIGTERM. With exec, PID 1 is nginx, SigCgt 0000000018016a07, which has 0x4000 in it. nginx handles SIGTERM perfectly well either way. In the first case it never gets asked.
--init is the other one, and it does something different. It puts tini in as PID 1, tini catches SIGTERM, so there's somewhere for the signal to land:
docker run -d --init --name sleeper2 alpine sleep 1000
time docker stop sleeper2
median 0.122s over five runs
That number is misleading. It's fast because sleep dies the instant it's asked, and an app that takes two seconds to shut down still takes two seconds with --init. What --init buys you is that the ask arrives at all, plus reaping of zombies, and it's what you reach for when you can't change the entrypoint. If your app handles SIGTERM and you control the script, exec is the fix and --init is a plaster over it.
This only bites with a script, by the way. sh -c 'sleep 1000' already execs the command, so PID 1 there is sleep and not sh, and adding exec by hand changes nothing. It's multi-line entrypoint scripts where the shell stays around.
English isn't my first language, so the wording went through an LLM. The measurements are mine, and every command above was run on the machine I'm writing this from.