Introduction
Some of you might have encountered the issue that it takes some time (ca 10 seconds) before a container stops after you issue docker stop
or docker-compose stop
. This is happening either because the script you use in your ENTRYPOINT is trap
:ing the wrong kill
signal, or you are not properly dealing with signals your init script/command. I always use a bash script, so this post is mainly focusing on that. If you use other scripting languages for your ENTRYPOINT script, you might have to solve this issue differently.
Stopping a container is dependent on a few things:
- The
ENTRYPOINT
in your Dockerfile, and how it behaves when receiving a signal - The
STOPSIGNAL
in your Dockerfile (default:SIGTERM
, but this is not always used in all base containers, see php:8.0-fpm and nginx). - The
stop_signal
in your docker-compose.yml file
Bonus items:
- The
stop_grace_period
in your docker-compose.yml file
How docker stops your containers
The default behavior is that docker sends the SIGTERM
signal to the entry point process (normally with process id 1 in the container). If the container is still running after 10 seconds, docker stop
and docker-compose down
will send the SIGKILL
signal, which will remove the process from the OS scheduler. This can be overridden:
- In the Dockerfile
- In the docker-compose.yml
- In the
docker stop
command
For example, the php-fpm and nginx containers does not use the default SIGTERM
as stop signal.
$ docker inspect nginx:latest | jq '.[].Config.StopSignal'
"SIGQUIT"
$ docker inspect php:7.4-fpm | jq '.[].Config.StopSignal'
"SIGQUIT"
In the case of nginx
container, it has to do with how the nginx
process itself deals with signals. SIGTERM
is used to quickly stop an nginx
process, which might cut off a request and leave some garbage behind. The signal SIGQUIT
is used instead, which within the nginx
process is interpreted as “we will slowly shut down, and the users will not be affected”.
Start/stop containers
I have a very simple example, to show a few ways to test this.
#--- create the init.sh script
cat<<EOT > init.sh
#!/bin/bash
echo "This container will not stop immediately after SIGTERM"
sleep infinity
EOT
chmod 755 init.sh
#--- create the Dockerfile
cat<<EOT > Dockerfile
from php:8.0-fpm
COPY . /
ENTRYPOINT ["/init.sh"]
EOT
#--- build the container
docker build -t stop-container-demo:latest .
#--- run the container (in one terminal window)
docker run --name=stop-demo --rm -it stop-container-demo
#--- stop the container (in a different terminal window), this will take 10 seconds
docker stop stop-demo
The container will stop after 10 seconds, as we have multiple issues with the init.sh script.
- php:8.0-fpm does not use the default
SIGTERM
signal (it usesSIGQUIT
), so the init.sh script does not receive the expected signal - We don’t
trap
the signal, so that we can exit the script - We
sleep infinity
in a way so that our bash script can’ttrap
the signal
We have to add a trap
to the init.sh
script, and we have to send sleep
to the background:
cat<<EOT > init.sh
#!/bin/bash
#--- we add a function to exit nicely (perhaps kill a few processes and remove some temp files)
function exit_container_SIGTERM(){
echo "Caught SIGTERM"
exit 0
}
#--- trap the SIGTERM signal
trap exit_container_SIGTERM SIGTERM
echo "This container will stop immediately after SIGTERM"
sleep infinity &
wait
EOT
chmod 755 init.sh
Now you can test again:
#--- build the container
docker build -t stop-container-demo:latest .
#--- run the container (in one terminal window)
docker run --name=stop-demo --rm -it stop-container-demo
#--- stop the container (in a different terminal window), this will take 10 seconds
docker stop stop-demo
Well, our container still did not exit immediately. This is because the php:8.0-fpm container is built to use SIGQUIT
instead of SIGTERM
. We must either build our container and select the SIGTERM
as the stop signal, or we have to rewrite our init.sh
script to trap
the SIGQUIT
signal.
In your Dockerfile, you select which signal to use with the STOPSIGNAL
keyword:
cat<<EOT > Dockerfile
from php:8.0-fpm
COPY . /
#--- override the SIGQUIT used in php:8.0-fpm
STOPSIGNAL SIGTERM
ENTRYPOINT ["/init.sh"]
EOT
#--- build the container
docker build -t stop-container-demo:latest .
#--- run the container (in one terminal window)
docker run --name=stop-demo --rm -it stop-container-demo
#--- stop the container (in a different terminal window), the container will terminate immediately.
docker stop stop-demo
Bash - sleep, wait, trap
This section is basically repeating what is already written above to some extent. Your ENTRYPOINT script will not react on a signal if you don’t take care of how you sleep
at the end of the script (bash). Many write their entry point script such, that they are using sleep infinity
to make the script “blocking forever”. If this is not done properly, your script will not catch any signals sent to it, even if you have a trap
in your bash script.
This does not work:
function exit_script(){
echo "Caught SIGTERM"
exit 0
}
trap exit_script SIGTERM
#--- my init.sh script
./start/my/program &
sleep infinity
This works:
function exit_script(){
echo "Caught SIGTERM"
exit 0
}
trap exit_script SIGTERM
#--- my init.sh script
./start/my/program &
#--- send sleep into the background, then wait for it.
sleep infinity &
#--- "wait" will wait until the command you sent to the background terminates, which will be never.
#--- "wait" is a bash built-in, so bash can now handle the signals sent by "docker stop"
wait
docker-compose.yml
You can override the STOPSIGNAL in a container by using the attribute stop_signal
in your docker-compose.yml file:
version: '2'
services:
demo:
stop_signal: SIGTERM
image: stop-container-demo:latest
References
- https://docs.docker.com/engine/reference/builder/#stopsignal
- https://stackoverflow.com/questions/27694818/interrupt-sleep-in-bash-with-a-signal-trap
- https://docs.docker.com/compose/compose-file/#stop_signal
- https://docs.docker.com/compose/compose-file/#stop_grace_period
- https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/