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.