Docker Containers Without a Proper Init Process May Take a Long Time to Terminate

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

# 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 docker-compose.yml.

    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

In docker-compose.yml, you can pass init: true to do that.

    image: debian:bullseye-slim
    command: ['tail', '-f', '/dev/null']
    init: true

See Also

Gentaro "hibariya" Terada

Otaka-no-mori, Chiba, Japan
Email me

Likes Ruby, Internet, and Programming.