49.10 PID 1, tini (--init) and graceful SIGTERM shutdown
Friends, why does docker stop take 10 seconds? And why are some orders left half done? The answer is PID 1. Today we learn PID 1, signals, tini and graceful shutdown with our own hands.
मित्रांनो, docker stop केलं की container 10 second का थांबतो? आणि कधी कधी orders अर्धवट का राहतात? उत्तर आहे PID 1. आज PID 1, signals, tini आणि graceful shutdown हाताने समजून घेऊ.
दोस्तों, docker stop करने पर container 10 second क्यों रुकता है? और कभी orders आधे क्यों रह जाते हैं? जवाब है PID 1. आज PID 1, signals, tini और graceful shutdown खुद करके समझेंगे.
1. Start from a story
At Zomato, every deploy took 10 extra seconds per container, and a few orders failed during each deploy. The team found the reason: their Node app was started with a shell script. The script was PID 1, and it ignored the stop signal. Docker waited 10 seconds and then killed everything hard, in the middle of orders.
Think of a restaurant at closing time. The manager (PID 1) hears the announcement: we close in 10 minutes. A good manager tells the cooks to finish current orders and stop taking new ones. A manager wearing headphones hears nothing, and at closing time the lights just go off with food still on the stove.
In a container, the first process is PID 1. It must hear SIGTERM, pass it on, finish work and exit. Today we see how.
2. What is PID 1
Step 1: The first process is PID 1
Why: Every container has its own process numbers.
What: Inside a container, the main process gets PID 1. On a normal Linux machine, PID 1 is systemd. In a container, it is your app, or whatever runs first.
Example 1: See PID 1
Type this:
docker run --rm alpine:3.20 ps
You will see:
PID USER COMMAND
1 root ps
What it means: ps itself is PID 1 here.
Example 2: In nginx
Type this:
docker run -d --name ng nginx:1.27; docker exec ng ps -o pid,comm 2>/dev/null || docker exec ng cat /proc/1/comm
You will see:
nginx
What it means: nginx master is PID 1.
Example 3: On the host it is different
Type this:
docker inspect ng --format '{{.State.Pid}}'
You will see:
73311
What it means: The same process has a normal PID on the host.
Step 2: PID 1 is special
Why: Learners expect Ctrl+C or docker stop to work like on a laptop.
What: The Linux kernel does not apply default signal actions to PID 1. If PID 1 has no handler for SIGTERM, the signal is simply ignored.
Example 4: sleep as PID 1
Type this:
docker run -d --name s1 alpine:3.20 sleep 1000
time docker stop s1
You will see:
s1
real 0m10.3s
What it means: sleep has no SIGTERM handler, so Docker waited 10 seconds.
Example 5: The exit code
Type this:
docker inspect s1 --format '{{.State.ExitCode}}'
You will see:
137
What it means: Killed hard with SIGKILL.
Example 6: Same program, not PID 1
Type this:
docker run -d --name s2 alpine:3.20 sh -c 'sleep 1000 & wait'
time docker stop s2
You will see:
s2
real 0m10.2s
What it means: Still slow: now sh is PID 1 and it does not pass the signal on.
3. Shell form vs exec form
Step 3: Shell form hides your app
Why: Most slow stops come from one Dockerfile line.
What: CMD node server.js (shell form) runs /bin/sh -c 'node server.js', so sh is PID 1. CMD ["node","server.js"] (exec form) runs node directly as PID 1.
Example 7: Shell form
Type this:
printf 'FROM node:20-slim\nCMD node -e "setInterval(()=>{},1000)"\n' > Dockerfile.shell
docker build -q -f Dockerfile.shell -t shellform .
docker run -d --name sf shellform; docker exec sf cat /proc/1/cmdline | tr '\0' ' '
You will see:
sha256:...
...
/bin/sh -c node -e "setInterval(()=>{},1000)"
What it means: PID 1 is sh.
Example 8: Exec form
Type this:
printf 'FROM node:20-slim\nCMD ["node","-e","setInterval(()=>{},1000)"]\n' > Dockerfile.exec
docker build -q -f Dockerfile.exec -t execform .
docker run -d --name ef execform; docker exec ef cat /proc/1/cmdline | tr '\0' ' '
You will see:
sha256:...
...
node -e setInterval(()=>{},1000)
What it means: PID 1 is node.
Example 9: Still slow?
Type this:
time docker stop ef
You will see:
ef
real 0m10.2s
What it means: node is PID 1 now, but this code has no SIGTERM handler, so the kernel still ignores it. We need a handler or tini.
Step 4: Entrypoint scripts must use exec
Why: Many images start with docker-entrypoint.sh.
What: If an entrypoint script runs your app as a child, the script stays PID 1. The last line should be exec "$@", which replaces the script with your app.
Example 10: Without exec
Type this:
printf '#!/bin/sh\necho setup done\nnode -e "process.on(\x27SIGTERM\x27,()=>process.exit(0));setInterval(()=>{},1000)"\n' > entry.sh
You will see:
(no output)
What it means: node runs as a child of sh.
Example 11: With exec
Type this:
printf '#!/bin/sh\necho setup done\nexec "$@"\n' > entry.sh
chmod 0755 entry.sh
You will see:
(no output)
What it means: exec replaces sh with the app, so the app becomes PID 1.
Example 12: Check
Type this:
printf 'FROM node:20-slim\nCOPY entry.sh /entry.sh\nENTRYPOINT ["/entry.sh"]\nCMD ["node","-e","process.on(\x27SIGTERM\x27,()=>process.exit(0));setInterval(()=>{},1000)"]\n' > Dockerfile.entry
docker build -q -f Dockerfile.entry -t entry . && docker run -d --name en entry && time docker stop en
You will see:
...
en
real 0m0.4s
What it means: Fast stop: the app is PID 1 and handles SIGTERM.
4. Handling SIGTERM in the app
Step 5: A Node.js handler
Why: The Zomato order API must finish orders before it stops.
What: Listen for SIGTERM, stop the server from taking new requests, wait for open ones, then exit.
Example 13: The code
Type this:
printf 'const http=require("http");\nconst s=http.createServer((q,r)=>setTimeout(()=>r.end("order ok\\n"),2000)).listen(3000);\nprocess.on("SIGTERM",()=>{console.log("SIGTERM: draining");s.close(()=>{console.log("bye");process.exit(0)})});\n' > server.js
You will see:
(no output)
What it means: A slow 2-second order endpoint with a SIGTERM handler.
Example 14: Build and run
Type this:
printf 'FROM node:20-slim\nCOPY server.js /\nCMD ["node","/server.js"]\n' > Dockerfile.node
docker build -q -f Dockerfile.node -t orders . && docker run -d --name ord -p 3000:3000 orders
You will see:
...
What it means: Exec form, so node is PID 1.
Example 15: Stop during a request
Type this:
curl -s localhost:3000 & sleep 0.5; docker stop ord; docker logs ord
You will see:
order ok
ord
SIGTERM: draining
bye
What it means: The order finished, then the app exited cleanly.
Step 6: A Python handler
Why: The same idea in Python, used by teams at Razorpay.
What: Use the signal module to catch SIGTERM and set a flag, then finish the loop and exit.
Example 16: The code
Type this:
printf 'import signal,time,sys\nrun=True\ndef stop(*a):\n global run; print("SIGTERM: finishing",flush=True); run=False\nsignal.signal(signal.SIGTERM,stop)\nwhile run: time.sleep(0.5)\nprint("bye",flush=True); sys.exit(0)\n' > worker.py
You will see:
(no output)
What it means: A worker loop that stops when asked.
Example 17: Run it
Type this:
docker run -d --name wk -v $PWD/worker.py:/w.py:ro python:3.12-slim python /w.py
You will see:
...
What it means: python is PID 1.
Example 18: Stop it
Type this:
time docker stop wk; docker logs wk; docker inspect wk --format '{{.State.ExitCode}}'
You will see:
wk
real 0m0.7s
SIGTERM: finishing
bye
0
What it means: Fast, clean, exit 0.
5. tini and --init
Step 7: --init: a tiny PID 1
Why: You cannot change the app, but you want signals to work.
What: --init puts tini as PID 1. tini passes signals to your app and reaps zombie processes. The app must still exit on SIGTERM, but now it actually receives it.
Example 19: sleep with --init
Type this:
docker run -d --name s3 --init alpine:3.20 sleep 1000
time docker stop s3
You will see:
s3
real 0m0.4s
What it means: tini passed SIGTERM to sleep, which is not PID 1 now, so the default action killed it.
Example 20: See tini
Type this:
docker exec s3 ps -o pid,comm
You will see:
PID COMMAND
1 docker-init
7 sleep
What it means: docker-init is tini.
Example 21: Exit code
Type this:
docker inspect s3 --format '{{.State.ExitCode}}'
You will see:
143
What it means: 143 = 128 + 15 (SIGTERM). A clean signal stop.
Step 8: Zombies and reaping
Why: Apps that start child processes can leave zombies behind.
What: A zombie is a finished child that nobody collected. PID 1 must collect them. tini does it; most apps do not.
Example 22: Make zombies
Type this:
docker run -d --name z1 alpine:3.20 sh -c 'for i in 1 2 3; do (sleep 1 &) ; done; exec sleep 1000'; sleep 3; docker exec z1 ps -o pid,stat,comm
You will see:
PID STAT COMMAND
1 S sleep
9 Z sleep
10 Z sleep
11 Z sleep
What it means: Z means zombie. sleep as PID 1 never reaps them.
Example 23: With --init
Type this:
docker run -d --name z2 --init alpine:3.20 sh -c 'for i in 1 2 3; do (sleep 1 &) ; done; exec sleep 1000'; sleep 3; docker exec z2 ps -o stat | grep -c Z
You will see:
0
What it means: tini collected them.
Example 24: tini in a Dockerfile
Type this:
printf 'FROM alpine:3.20\nRUN apk add --no-cache tini\nENTRYPOINT ["/sbin/tini","--"]\nCMD ["sleep","1000"]\n' > Dockerfile.tini
docker build -q -f Dockerfile.tini -t withtini .
You will see:
sha256:...
What it means: Baked in, so nobody needs to remember --init.
6. Tuning the stop
Step 9: STOPSIGNAL and --stop-timeout
Why: Some apps want a different signal or more time.
What: STOPSIGNAL in a Dockerfile, or --stop-signal at run, changes the signal. --stop-timeout or docker stop -t changes the wait.
Example 25: nginx uses SIGQUIT
Type this:
docker image inspect nginx:1.27 --format '{{.Config.StopSignal}}'
You will see:
SIGQUIT
What it means: SIGQUIT makes nginx finish open connections.
Example 26: More time for a big job
Type this:
docker run -d --name big --stop-timeout 60 orders
You will see:
...
What it means: docker stop now waits up to 60 seconds.
Example 27: Check
Type this:
docker inspect big --format '{{.Config.StopTimeout}}'
You will see:
60
What it means: Saved with the container.
Step 10: The same idea in Compose and Kubernetes
Why: Your app will not run with plain docker run for ever.
What: Compose uses stop_grace_period and init: true. Kubernetes sends SIGTERM, waits terminationGracePeriodSeconds (30 by default), then SIGKILL. A good handler works everywhere.
Example 28: Compose file
Type this:
printf 'services:\n orders:\n image: orders\n init: true\n stop_grace_period: 30s\n' > compose.yaml
docker compose up -d
You will see:
[+] Running 1/1
Container demo-orders-1 Started
What it means: tini and a 30-second wait, in YAML.
Example 29: Check
Type this:
docker inspect demo-orders-1 --format '{{.HostConfig.Init}} {{.Config.StopTimeout}}'
You will see:
true 30
What it means: Both settings applied.
Example 30: Down
Type this:
time docker compose down
You will see:
[+] Running 2/2
Container demo-orders-1 Removed
real 0m0.9s
What it means: Fast, because the app handles SIGTERM.
Step 11: Test it before production
Why: Every service at Swiggy has a stop test in CI.
What: Measure the stop time and the exit code. A good service stops in under a few seconds with exit 0 or 143, never 137.
Example 31: Measure
Type this:
docker run -d --name t0 orders >/dev/null; start=$(date +%s); docker stop t0 >/dev/null 2>&1; echo $(( $(date +%s)-start ))s
You will see:
0s
What it means: Fast.
Example 32: Exit code check
Type this:
docker run -d --name t1 orders; docker stop t1 >/dev/null; docker inspect t1 --format '{{.State.ExitCode}}'
You will see:
0
What it means: Clean.
Example 33: Clean up
Type this:
docker rm -f ng s1 s2 s3 sf ef en ord wk z1 z2 big t0 t1
You will see:
...
What it means: All removed.
Check yourself
Q1. Why does sleep as PID 1 take 10 seconds to stop?
The kernel ignores SIGTERM for PID 1 without a handler.
Q2. Which CMD form keeps the app as PID 1?
The exec form, with JSON brackets.
Q3. What should an entrypoint script end with?
exec "$@".
Q4. What does --init add?
tini as PID 1, which passes signals and reaps zombies.
Q5. What does exit code 143 mean?
Stopped by SIGTERM.
Friends, PID 1 is the restaurant manager. It must hear SIGTERM and finish the orders before closing. Remember three fixes: exec form, exec "$@" and --init. Did you understand? Let us try it once more. Now stop the orders app and check that the order finishes.
मित्रांनो, PID 1 म्हणजे restaurant चा manager. त्याला SIGTERM ऐकायला आलं पाहिजे आणि orders पूर्ण करून बंद करायला पाहिजे. Exec form, exec "$@" आणि --init हे तीन उपाय लक्षात ठेवा. समजलं का? चला, पुन्हा एकदा करून पाहू. आता orders app बंद करून बघा order पूर्ण होतो का.
दोस्तों, PID 1 मतलब restaurant का manager. उसे SIGTERM सुनना चाहिए और orders पूरे करके बंद करना चाहिए. तीन उपाय याद रखिए: exec form, exec "$@" और --init. समझ आया? चलिए, एक बार फिर करके देखते हैं। अब orders app बंद करके देखिए order पूरा होता है या नहीं.
Ravindra Bagale's Tip
Always write CMD in JSON form in the Dockerfile, and add a SIGTERM handler in the app. Then deploys are fast and safe.
Dockerfile मध्ये CMD नेहमी JSON form मध्ये लिहा, आणि app मध्ये SIGTERM handler लावा. मग deploy fast आणि safe होतो.
Dockerfile में CMD हमेशा JSON form में लिखिए, और app में SIGTERM handler लगाइए. तब deploy तेज़ और सुरक्षित होता है.
Lab: try at home
- Run sleep as PID 1 and time docker stop.
- Run it again with --init and compare.
- Build shell form and exec form images and read /proc/1/cmdline.
- Add a SIGTERM handler to a Node or Python app and stop it during a request.
- Make zombies with and without --init.
Recap
- The first process is PID 1 and ignores SIGTERM without a handler.
- Use the exec form of CMD and exec "$@" in scripts.
- Handle SIGTERM: stop, drain, close, exit.
- --init adds tini to pass signals and reap zombies.
- Exit 0 or 143 is good; 137 means the stop was too slow.
Revision card
Interview card