tl;dr
Running Docker containers spawns processes with the PID of 1. If you run your container process wrapped in a shell script, this shell script will be PID 1 and will not pass along any signals to your child process. This means that SIGTERM
, the signal used for graceful shutdown, will be ignored by your process.
To avoid this problem, you can use the exec
from within your wrapper shell script along with a custom signal handler.
Alternatively, you can use an init-like process such as dumb-init with signal-proxying capabilities.
Background
Recently I encountered some unexpected behavior when working on an application deployed in Kubernetes. When pods were rotated during deployments they were being abruptly stopped with a SIGKILL
and ignoring the SIGTERM
signal sent at the start of the pod termination process.
After looking at the logs it was clear that the issue wasn’t with our signal handling code, which looked something like this:
import sys
import signal
import time
def signal_handler(signum, frame):
print(f"Gracefully shutting down after receiving signal {signum}")
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
while True:
time.sleep(0.5) # simulate work
print("Interrupt me")
Based on our logs, we weren’t hitting the signal handler function at all.
We were running the application from a shell script that looked like this:
#!/bin/sh
./main.py
This script was being run from a Docker container that looked something like this:
FROM python:3.9-alpine3.12
COPY main.py .
COPY main .
CMD ["./main"]
The Problem
If we take a look at the running processes in our container, we’ll see what the main
shell script has PID 1, and our main.py
Python program will have another PID:
$ docker exec pedantic_matsumoto ps aux
PID USER TIME COMMAND
1 root 0:00 {main} /bin/sh ./main &
6 root 0:00 python ./main.py
12 root 0:00 ps aux
$ docker exec pedantic_matsumoto pstree -p
main(1)---python(6)
PID 1 processes in Linux do not have any default signal handlers and as a result will not receive and propogate signals. They are also expected to take on certain responsibilities, such as adopting orphaned processes, and reaping zombie processes.
Potential Solutions
DIY Signal Handling and exec
The first way to get around this issue is to install custom signal handlers for SIGTERM
and other signals you need directly in your application code, and then run exec
in your wrapper shell script. This replaces the running process with your application.
For the application I gave as an example above, using exec
means doing this:
#!/bin/sh
exec ./main.py
Your application still wouldn’t be able to reap zombie processes or adopt orphaned processes, but it would be able to catch signals and handle them gracefully.
dumb-init
dumb-init is a simple init process which does everything an init
process is supposed to do. If you install it in your Docker container and use it as your entrypoint, you’ll be able to handle signals just fine.
tini
If you run your Docker container with --init
, Docker will automatically start its own init process as PID 1. The problem with using tini
is that container orchestrators, such as Kubernetes, can’t start your Docker container with the --init
flag.
If you want to use tini
, you’ll have to download and install it in your Dockerfile
and pass along the -g
option for signal forwarding.