Shutdown Signals with Docker Entrypoint Scripts

Benjamin Cane
Aug 17 · 5 min read

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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store