Shutdown Signals with Docker Entrypoint Scripts
--
One of the goals of Docker is to simplify what it takes to start and run applications. A way Docker tries to achieve this goal is by allowing users to create an isolated runtime environment where they don’t need complex startup scripts.
For the most part, it works. Docker is simple enough that an average application can be started directly with the ENTRYPOINT
instructions within the Dockerfile
.
ENTRYPOINT ["./my-app"]
However, not every app can start up so simple.
It is not uncommon to require specific tasks to execute within the container environment before the application starts. These tasks could be as simple as managing secrets like Certificates/Passwords, or highly complex like an orchestrated multi-step start process.
The reasons are numerous, and they typically all depend on both the application and the hosting environment it’s running in. The typical answer to this issue is to create ENTRYPOINT
scripts.
These scripts are custom start scripts the replace the application in the ENTRYPOINT
. Below is an example of a Dockerfile that uses a ENTRYPOINT
script.
One common issue with these scripts is that many times, users find it challenging to pass shutdown signals to the running application. That is what this article is going to cover, how best to write scripts that don’t break shutdown signals, and why it’s so easy to get it wrong.
Writing ENTRYPOINT scripts the right way
Before we start delving into how to write ENTRYPOINT
scripts the right way, let’s first look at how easy it is to write one the wrong way.
On the surface, the above script looks reasonably good, it… “should” work. But it doesn’t.
Our script is a pretty good example of what a ENTRYPOINT
script is. It first checks for a secret file, loads the contents of that file into an environment variable defined at runtime, and then starts our application. So, where does it go wrong?
It goes wrong with how it starts the application. Currently, our script is starting our service by only running the binary. What this does, is it creates a child process of our running app.
It is easier to explain if we login to our running container and run ps -elf
to see our processes.
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 0 80 0 - 934 - 06:43 ? 00:00:00 /bin/bash ../../docker-entrypoint.sh
4 S root 11 1 0 80 0 - 269652 - 06:43 ? 00:00:00 healthchecks-example
Within our container, we have two processes; PID 1 (parent), which is our actual entry point script, and PID 11 (child), which is our running service.
When Docker attempts to stop a container, it will send the specified signal to PID 1, the process that Docker starts. Docker will completely ignore any other process running within this container. So that means when we issue a docker stop
, the Docker daemon will send a SIGTERM signal to the docker-entrypoint.sh
process, not to our running service.
What is also important to note is that in Unix & Linux systems, a signal sent to the parent process will never pass to the child processes. What this means is, our script will receive a signal, but our running service will not. The only reason our process stops is that when the primary process stops executing (because it received a SIGTERM), Docker will teardown the container forcefully reaping any other running processes inside of it.
So how do we modify our script to work with signals to shutdown our apps gracefully? Simple, we use the exec
command.
What makes the exec
command special is that when used to execute a command like running our service. This command will replace the parent process with the new process. We can see this in action if we once again look at the process list from inside our container.
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 0 80 0 - 288149 - 06:56 ? 00:00:00 healthchecks-example
Notice the difference? When using exec
the only process running is our service. It has completely replaced our script, including taking over the process id.
Now when Docker sends the SIGTERM signal to process id 1, our service will trap the SIGTERM gracefully shutting down.
That’s it; that is the secret to writing ENTRYPOINT
scripts that allow the service to shutdown gracefully. However, you may still find that even with a well-written ENTRYPOINT
script, signals are still not working. The most likely cause is not within the script but the Dockerfile
.
Making sure the Dockerfile is correct
While our ENTRYPOINT
script is now working; there is another prevalent mistake that occurs. It’s straightforward, but it all revolves around how we use the ENTRYPOINT
instruction within the Dockerfile
.
Docker allows for two methods of defining ENTRYPOINT
instructions.
ENTRYPOINT ../../docker-entrypoint.sh
The above is called the “shell” form, where the command is specified. The second form is the “exec” form, where the command and arguments are in a JSON format.
ENTRYPOINT ["../../docker-entrypoint.sh"]
The difference is that when using the “shell” form, Docker will run the specified command within a sub-shell utilizing the sh -c "command"
method. We can once again see this in action by looking at the process list within the running container.
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 0 80 0 - 597 - 07:13 ? 00:00:00 /bin/sh -c ../../docker-entrypoint.sh
4 S root 6 1 0 80 0 - 269652 - 07:13 ? 00:00:00 healthchecks-example
With the above, we can see that there are two processes once again.
It is important to note that even though our script is correct, using the “shell” form changes behavior. For signals to work correctly, it is vital to use the “exec” ENTRYPOINT
format.
Summary
In this article, we explored two common mistakes people make when using ENTRYPOINT
scripts with Docker. They both come down to the use of using sub-processes to start the application. If you take away nothing else, remember this. Sub-processes do not receive shutdown signals and based on how you write the ENTRYPOINT
scripts and Dockerfile
instruction; determines if the application starts as a sub-process.
To learn more about signals, check out my article; Signal Traps: What are they, and how to use them.