When stopping a container one way or another, like by Ctrl+C or
docker stop, Docker sends SIGTERM to the process by default. If the first process (PID = 1) on the container is not a proper init process, the termination may take a long time to be done. It's because processes running as PID 1 are treated differently than usual ones.
In Linux, a process running as PID 1 ignores any signal unless the process implements signal handlers on its own. While some programs like
ruby trap signals, many others like
tail do not.
# this will treat signals as usually expected docker run --rm -it ruby:slim ruby -e STDIN.read # this will ignore signals and take longer time to terminate docker run --rm -it ruby:slim tail -f /dev/stdin
The first one (
ruby) in the example above can be stopped by either Ctrl+C or
docker stop because
ruby comes with the signal handlers for SIGINT/SIGTERM out of the box. On the other hand, the second one (
tail) will ignore Ctrl+C and
docker stop will take a much longer time to terminate than the other since it does not handle these signals. By default,
docker stop sends SIGTERM to the process and waits 10 seconds. If the process does not stop in the grace period, then Docker sends SIGKILL to terminate no matter what. In this example, the second one will be killed that way.
The same thing goes on Docker Compose. Sometimes I intentionally run a command that does nothing and not consume many resources like
tail -f /dev/null to keep the container idle for some reasons such as providing a place to run one-off commands. I have written something like that several times for Docker Compose and Kubernetes. These containers never handle signals without a proper init process. It can be reproduced with the following
services: idler: image: debian:bullseye-slim command: ['tail', '-f', '/dev/null']
If you run
docker-compose up with this file and then hit Ctrl+C, you can see the container takes a certain amount of time to stop.
To fix this problem, we have to use a proper init process as the entry point and run the original process on top of that. One of the easiest way to do that is to pass
--init option to
docker. It tells Docker to run the original entry point on a built-in init process that handles the signals as expected.
# this will handle Ctrl+C and `docker stop` immediately docker run --rm -it --init ruby:slim tail -f /dev/stdin
docker-compose.yml, you can pass
init: true to do that.
services: idler: image: debian:bullseye-slim command: ['tail', '-f', '/dev/null'] init: true