TT Lab
Get started
Learn Learning paths Courses

Docker Fundamentals

Why docker stop Takes a Whole Ten Seconds

Continue in TT Lab

One-line summary

A container's first process is PID 1, and the Linux kernel treats PID 1 differently. The 10-second wait of docker stop, Ctrl+C not working, and zombie processes piling up all come from here.

Why this is needed

After adding docker stop to a deployment script, it takes exactly 10 seconds per container. With 20 containers that is 200 seconds. There is no error in the logs; it is just slow.

The cause is this. docker stop first sends SIGTERM, waits, and if the process does not die, sends SIGKILL. The default for that "waiting" time is 10 seconds. So taking 10 seconds means the process received SIGTERM and still did not die.

A timetable comparing docker stop's 10 seconds for four kinds of PID 1 — with the shell form and the exec form without a handler, nobody receives SIGTERM, so they use all 10 seconds and die by SIGKILL, while the exec form with a handler and --init shut down cleanly right away

Why did it not die? For an ordinary process, if it has not registered a signal handler, the kernel performs the default action (termination) on its behalf. But PID 1 does not have that default action. The kernel assumes PID 1 is init and simply discards signals for which no handler is registered. If the last process of the system died by accident, it would be a kernel panic, so this is a protection mechanism.

So if the Python started with CMD ["python", "app.py"] has not registered a SIGTERM handler, that container ignores SIGTERM. After 10 seconds it is forcibly killed with SIGKILL. Open connections and half-written files are cut off just as they are.

How it works

The second trap is the shell form.

CMD python app.py          # 셸 폼 → /bin/sh -c "python app.py"
CMD ["python", "app.py"]   # exec 폼 → python 이 직접 PID 1

If you use the shell form, PID 1 is not Python but /bin/sh. Even when sh receives SIGTERM, it does not forward it to its child. The application never even sees the signal.

Situation PID 1 SIGTERM result
Shell form CMD python app.py /bin/sh Not delivered to the app → KILL after 10 seconds
Exec form, no handler python Discarded by the kernel → KILL after 10 seconds
Exec form, with handler python Clean shutdown immediately ✅
Using --init docker-init Delivered + zombies reaped ✅

The third is zombies. When a child process dies, the parent must collect its exit status with wait() before it disappears from the process table. If the parent does not collect it, it stays as a zombie. This cleanup is originally init's job, but if the container's PID 1 is an application, that app does not reap zombies. A process count that grows over several days in a container that keeps running shell scripts is usually this.

--init solves these two problems at once by inserting a very small init process as PID 1.

exec and attach are different

docker exec docker attach
What it does Runs a new process inside the container Connects to the standard input and output of PID 1
When you leave Only that process ends Pressing Ctrl+C kills the container
Use Debugging, checking files Directly watching the output of the original process

A great many people have attached to a running container with attach, habitually pressed Ctrl+C, and taken the service down. When you connect, use exec.

What it looks like in the field

How to look inside a container you cannot enter

docker exec -it ... sh works only when a shell exists. An image built from distroless or scratch has neither a shell nor ps. You can still look inside — because namespaces can also be inspected from the outside.

Just look at that process from the host. The container's PID 1 is also a single process on the host. Only the namespaces differ.

pid=$(docker inspect -f '{{.State.Pid}}' myapp)
ps -p $pid -o pid,ppid,rss,etime,args
ls -l /proc/$pid/fd            # 열린 파일과 소켓
cat /proc/$pid/limits          # ulimit
cat /proc/$pid/cgroup          # 어느 cgroup 에 매였는지

You can read the filesystem without mounting it. /proc/<pid>/root is the root that the process sees. You can read files inside the container with the host's tools.

cat /proc/$pid/root/etc/app/config.yml

You carry the tools in from outside. nsenter attaches to the namespaces you specify and runs a command. The tool does not have to be in the image.

sudo nsenter -t $pid -n ss -tlnp      # 그 컨테이너의 네트워크에서 포트를 본다
sudo nsenter -t $pid -m ls /tmp       # 그 컨테이너의 마운트에서 파일을 본다

In Kubernetes, an ephemeral container does the same job. You attach a container with tools to the target Pod and have it share the process namespace.

kubectl debug -it pod/myapp --image=nicolaka/netshoot --target=app

If you leave out --target, the processes are not visible. In that case the new container shares only the network and has its own PID namespace. To see the processes, always specify the target.

The reason logs do not appear is usually a buffer. When an application writes stdout to a pipe, libc switches to block buffering, so nothing comes out until 4 KB has accumulated. For Python, use PYTHONUNBUFFERED=1; for ordinary commands, use stdbuf -oL to switch to line buffering. "Logs are not being printed" is often really "they have not come out yet".

What to look for in the next check

In the quiz that follows, based on what you observed in the previous lab, you will distinguish PID 1 signal delivery, zombie reaping, and the conditions under which docker stop kills by force. Answer by citing how the difference between the exec form and the shell form showed up in shutdown time and in the process tree.