If you use exec in your container script, then the container or Kubernetes pod might exit after the command that is exec-ed into has exited. But if that’s what you wanted, then it’s okay. This blog tries to explain how to pass the signals to the applications, how they work differently when invoked uniquely and what to do if the application does handle them.

What are the “Signals”?

Signals are messages one process can send to another process, mostly used in UNIX like operating systems.

How exec works in Linux?

This is copied from the man page:

Exec Man Page

An application spawned by a shell script.

To ensure that the signals are passed effectively to the real application, spawned by a shell script, you can do something similar to what is done in this snippet. Use trap to call a function which does the cleanup after receiving any of the registered signals like SIGHUP, SIGTERM or SIGINT.

#!/bin/bash

function cleanup() {
  kill "${pid}"
  exit
}

trap cleanup SIGHUP SIGTERM SIGINT

echo "sleeping"
sleep infinity &
pid="${!}"

wait "${!}"

If a process runs in foreground spawned by a bash script, then the bash script does not respond to the signals, so the trap is rendered useless. Hence run it in the background and wait on it using wait. Make sure that the script is run via ENTRYPOINT the way it is done in this Dockerfile:

FROM quay.io/surajd/fedora-networking

COPY ./cleanup.sh /cleanup.sh

ENTRYPOINT /cleanup.sh

Since the above script, cleanup.sh is listening on three types of signals: SIGHUP, SIGTERM, SIGINT if any of these is sent to the script the container will stop working. So the following commands will work just fine:

Docker commands

Here is a video that shows how the above commands works:

An application spawned directly.

Here is a golang application that handles signals. This golang app can be run directly via ENTRYPOINT.

Golang Signals App

FROM quay.io/surajd/fedora-networking

COPY ./signals /signals

ENTRYPOINT /signals

This golang code is also listening on the same signals: SIGHUP, SIGTERM, SIGINT. The code has a main goroutine which spawns another goroutine. The background goroutine is listening to the aforementioned signals. While the main goroutine is waiting (←done) for the background goroutine to receive signal and finally exit. In the Dockerfile, we have simply copied the binary directly and spawn it using ENTRYPOINT, this ensures that the application receives the signals directly. ENTRYPOINT is actually starting the given process using exec.

Watch the video below, which shows how the signals are passed to the application:

When should you use exec?

Now consider that you have an application which listens to those signals, but for some reason, you need to spawn that application via a shell script. This is when you use exec. We are using the same golang application as before but spawning it from a bash script. Notice that this script is invoking that golang binary using exec, this replaces the bash script with golang binary as PID 1.

#!/bin/bash

echo "spawning the golang app"

# This app can handle the signals so no need to handle them on
# behalf of the application here in the bash script.
exec ./signals
FROM quay.io/surajd/fedora-networking

COPY ./signals /signals
COPY ./startup.sh /startup.sh

ENTRYPOINT /startup.sh

If we get shell access of the container, you will see that the golang signals app has become PID 1.

Console output

Here is the video that shows these signals passing in action:

Conclusion

I hope this gives you some clearer picture on how to use exec sanely and make sure that the applications are spawned correctly to get the signals sent by their environment be it systemd, docker or Kubernetes.

References