Ravindra BagaleCourses & study guides मराठी

Chapter 49: Docker Containers: Lifecycle, docker run Flags, exec, logs, Restart Policies, Limits and Every Container Command

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.

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.

docker stopsends SIGTERM to PID 1PID 1 hears itstops new workfinish current workclose DB connectionsexit 0before the timeoutelse after 10 sSIGKILL, exit 137
Picture 1: graceful stop

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

SIGTERM arriveshandler runsstop acceptingclose the HTTP serverfinish open requestsdrainclose DB and queuesflush dataexit 0clean stop
Picture 2: inside 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.

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.

Lab: try at home

  1. Run sleep as PID 1 and time docker stop.
  2. Run it again with --init and compare.
  3. Build shell form and exec form images and read /proc/1/cmdline.
  4. Add a SIGTERM handler to a Node or Python app and stop it during a request.
  5. 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.

Ravi Sir's Cards for this lesson

मराठी cards