Handling docker stop signals properly

Methods

If you are starting your dockerized application from an ENTRYPOINT or a CMD you might have noticed that the application simply gets killed after 10 seconds of issuing a docker stop command on the container. This happens because your application does not get a chance to process the SIGTERM signal issued by docker.

There are two reasons this could be happening.

  1. You application is getting the signal but it does not know how to process it.
  2. You application knows how to process the signal, but never gets it.

In the first case you have two options:

  1. Modify your application to handle the SIGTERM. For example a node server would do something like this: process.on('SIGTERM', (signal) => { serverEx.close(() => { process.exit(128) }) })

  2. Have the shell wrapper handle SIGTERM on behalf of the application. More on how to do this later.

In the second case the fix it to make sure your ENTRYPOINT or CMD script forward the signals correctly. There are also two options

  1. Replace your script with the application process by invoking it via exec application bash command. Docker sends signals to PID 1. After exec the application will be running as PID 1 and will get the signal

  2. Catch the signal in the script and re-send it to the application using trap 'kill -SIGTERM $(pidof app)' SIGTERM. This has an advantage of processing the signal inside the script before or after sending it to the application.

Using the shell wrapper to handle docker signals

This is useful when you want to issue additional commands to the application before stopping it. To do this you might need the application to run in the background while still getting STDIN from the script.

  • First create a named pipe to route STDIN through: mkfifo /data/in.

  • Then block it for writing, so it does not get closed when your process read all of the current contents: sleep infinity > /data/in &. Sleeping forever is better than tailf -f /dev/null because tailf uses inotify resources and will be triggered each time some app sends data to /dev/null. You can see this by running strace on it. It is also better than cat > /dev/null & because inside of docker cat will be itself disconnected from STDIN, which in turn will close /data/in.

  • Start your process in the background with the /data/in providing STDIN: application < /data/in &. This works better than using piping like this tail -f /data/in | application & because the pipe will only get terminated if the tail stops, but if your application crashes the pipe will keep running.

  • Halt waiting for the application to finish. wait $(pidof application). This will ensure that the docker container is running as long as the applicaiton is running. If the application crashed, the container started with -d --restart unless-stopped will be automatically restarted.

  • Handle your signals. Somewhere before the wait command put in the signal handling routines:

trap 'server_shutdown' SIGTERM

function server_shutdown() {
    pid=$(pidof applicaiton)
    if [ -z "$pid" ]; then echo "Already stopped"; exit 1; fi
    echo "stop" > /proc/$pid/fd/0
    # Wait for it to finish saving and exit
    while [ -e /proc/$pid ]; do
        sleep 1
    done
    echo "done"
    exit 0
}

Note: an alternative way to the named pipe that’s held open may be bash v4 coprocesses using the ‘coproc’ command. A coprocess has a two-way pipe established between the executing shell and runs in the background.

Based off the Stack theme.