1. Overview
A Docker container is a process that runs in isolation from its host. The Docker engine uses Linux namespaces to isolate container processes from the host. As a result, each container has its process table. Processes running in the host aren’t visible from within the container.
In this tutorial, we’ll learn how to send signals to processes that run in a Docker container.
2. The Test Case
We’ll start a Docker container running a Bash shell to create our test case. There, we’ll run a small shell script that traps a signal and prints a message to the standard output:
$ sudo docker run -it ubuntu:latest /bin/bash
# /bin/bash -c "trap 'echo signal caught' SIGALRM;read"
As we can see, we started a new container using the Ubuntu image. Also, we ran the container interactively with a pseudo-tty allocated, using the -i and -t options. Finally, we set the container to run a new Bash session.
At this point, we’re in a terminal inside the container. Next, we create a Bash sub-shell using the bash command with the -c option. This option makes Bash execute commands from the first non-option argument. Here, we run two commands in the sub-shell:
- a trap command that will print a message upon receiving a SIGALRM signal
- a read command that blocks the execution waiting for a user input
Of course, our goal here isn’t to provide input but to send a SIGALRM signal to the sub-process. Also, the sub-shell runs in a separate process in the container.
3. Sending a Signal From the Host
First, we’ll try to send a signal to the sub-process from the host. We should keep in mind that the container is a process that runs in a child PID namespace. There’s a parent-child relationship between the host’s and the container’s namespaces.
An important property of this relationship is that the processes of the child namespace are visible from the parent namespace. This means that processes that run in parent namespaces can see processes running in child namespaces but not the other way around. As a result, we can see the sub-process that runs in the container with the ps command. Since the terminal session that we created in the previous section is blocked, we can open a new terminal session and run the ps command:
$ ps aux
...
237362 root /bin/bash -c trap 'echo signal caught' SIGALRM;read
...
As expected, running ps on the host returned the sub-process running in the container. Furthermore, our sub-process has a different process ID in the container. We can find all process IDs in the /proc directory of the host:
$ cat /proc/237362/status | grep NSpid
NSpid: 237362 8
Indeed, the NSpid property of the status file holds the process ids of the process both in the parent and the child namespace.
As a result, we can use the kill command to send a signal to the 237362 process:
$ sudo kill -SIGALRM 237362
Next, if we return to the container, we’ll see that the trap command indeed printed the signal caught message. This means that the sub-process effectively received the signal that we sent it.
4. Using Docker’s exec Command
Docker’s exec command runs a command in a running container. So, we can open a new terminal session, run a ps command to find the process id of our sub-process in the container, and then run the kill command to send the signal. First, let’s list running containers to find the name of the container that we’re interested in:
$ sudo docker container list
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c87dea2c9f5c ubuntu:latest "/bin/bash" About an hour ago Up About an hour upbeat_leakey
Here, our container’s name is upbeat_leakey. Next, let’s use Docker’s exec command to run ps:
$ sudo docker container exec upbeat_leakey ps ax
PID TTY STAT TIME COMMAND
1 pts/0 Ss 0:00 /bin/bash
8 pts/0 S+ 0:00 /bin/bash -c trap 'echo signal caught' SIGALRM;read
34 ? Rs 0:00 ps ax
As we expected, Docker ran the ps command in the upbeat_leakey container and returned the output. Indeed, we can see our sub-process with process ID 8 running in the container. In addition, we used options a and x of the ps command so that we get all processes, even those without an assigned tty.
So, we’re ready to send the signal with the kill command:
$ sudo docker container exec upbeat_leakey kill -SIGALRM 8
We can go back to the container’s terminal to verify that the relevant message is printed. Indeed, we managed to send the SIGALRM signal with the help of Docker’s exec command.
5. Using the nsenter Command
The nsenter command runs a program using the namespaces of another process. So, we can find the process id of a Docker container process and enter its namespace to send a signal. First, we can find the container’s process ID using Docker’s inspect command:
$ sudo docker inspect --format='{{.State.Pid}}' upbeat_leakey
237208
The inspect command with no arguments prints all the container’s configuration in JSON format. We can filter the output with the –format option. This option accepts a template that will select a specific property of the JSON configuration. Here, we select the Pid property of the State construct. As a result, we get the process ID of the container.
Next, we can run the ps command through nsenter to find the process id of our sub-process in the container:
$ sudo nsenter --target 237208 --mount --pid ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 4620 3784 pts/0 Ss 04:17 0:00 /bin/bash
root 8 0.0 0.0 4352 1456 pts/0 S+ 05:07 0:00 /bin/bash -c trap 'echo signal caught' SIGALRM;read
root 76 0.0 0.1 7472 3248 ? R+ 07:04 0:00 ps aux
Indeed, we can see that our sub-process runs with a process ID value of 8. In addition, we used the –target, –mount, and –pid options of the nsenter command. The –target option is used to define the process whose namespace we want to enter.
The –mount and –pid options define the type of namespaces we want to enter. This means that out of the eight namespaces available, we enter only the mount and PID namespaces. Both of these namespaces are necessary to get the processes running in the container displaying the container’s process IDs. The mount namespace is necessary so that the ps command uses the /proc filesystem of the container.
So, at this point, we’re ready to send a signal using the nsenter command:
$ sudo nsenter --target 237208 --mount --pid kill -SIGALRM 8
Here, we see that the syntax of the nsenter command is similar to the previous example, meaning that we enter the PID and mount namespaces of the target process. If we return to the container’s terminal session, we’ll verify that the signal was received successfully by the sub-process.
6. Conclusion
In this article, we learned how we can send a signal to a process running in a Docker container. First, we created a test scenario and described three ways to send a signal. The first way was directly from the host, the second used Docker’s exec command, and the third was again from the host using the nsenter command.
Finally, we should keep in mind that a container is a process that runs in a child namespace and that processes running in a child namespace are visible from the parent.