In an earlier post of mine “Let it explode!”, we explored “handling” certain exceptions via
premature termination. Now, it is time to solve the mystery of process termination in Docker.
Nowadays, Docker containers have become the de facto standard of shipping applications. While running docker
containers, I noticed an unexpected exit code when a contained application crashed.
Out of curiosity and while I had
a few minutes left for Futurama to finish downloading, I created a sample application in order to investigate this
sorcery:
1
2
3
4
5
6
#include <cstdlib>
int main() {
std::abort();
return 0;
}
Dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM ubuntu:18.04
# Install tools
RUN apt-get update \
&& apt-get -y install \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Build the application
COPY ./ /src/
WORKDIR /src/
RUN g++ main.cpp -o app
WORKDIR /
CMD ["/src/app"]
The result once executed:
$ docker build -f ./Dockerfile -t sigabort_test:latest .
$ docker run --name test sigabort_test:latest ; echo $?
139
In Unix-like operating systems, if a process is terminated with a signal, the exit code is the result of 128
+ the
signal number[1]. In the example above, the exit code is 139 = 128 +
11
, where 11
represents SIGSEGV
(segmentation fault) instead of 134 = 128 + 6
which is SIGABRT
(abort).
Most users are generally only interested in knowing whether the application works, which they check and confirm when the
exit code is 0
. For debugging purposes, however, it is very useful to understand the cause of an unexpected
termination.
During my research, I was able to find an open issue on the SIGABRT
dilemma and a comment with the following workaround using bash:
1
CMD ["bash", "-c", "/src/app ; exit $(echo $?)"]
Now, the container returns the correct exit code:
$ docker run --name test sigabort_test:latest ; echo $?
bash: line 1: 6 Aborted /src/app
134
Another way, which can be considered the correct method, is to solve the problem by adding the --init
flag. This indicates that an init
process should be used as the PID 1 in the container. Specifying the init process
ensures that the usual responsibilities of an init system, such as reaping zombie processes and default signal handling,
are performed inside of the created container.
The following is the result of running the container with the --init
flag and the original Dockerfile command CMD
["/src/app"]
:
$ docker run --init --name test sigabort_test:latest ; echo $?
134
P.S. docker-compose
also supports the init flag
from version 2.4 and onward.
For a detailed explanation on signal handling in docker, please have a look at my next article How signals are handled in a docker container.