Skip to content

Commit

Permalink
Merge branch 'ckuehl_exit_status_better_help'
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskuehl committed Aug 6, 2015
2 parents 9e11023 + 2c3b93a commit b535104
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 10 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
DOCKER_TEST := sh -c 'dpkg -i /mnt/dist/*.deb && cd /mnt/test && ./test'
DOCKER_TEST := sh -c 'dpkg -i /mnt/dist/*.deb && cd /mnt && ./test'

.PHONY: build
build:
Expand Down
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
dumb-init
========

`dumb-init` is a simple process designed to run as PID 1 inside Docker
containers and proxy signals to a single child process.

In Docker containers, a process typically runs as PID 1, which means that
signals like TERM will just bounce off your process unless it goes out of its
way to handle them (see the "Why" section below). This is a big problem with
scripts in languages like Python, Bash, or Ruby, and can lead to leaking Docker
containers if you're not careful.


## Why you need a signal proxy

Normally, when processes are sent a signal like `TERM`, the Linux kernel will
try to trigger any custom handlers the process has registered for that signal.

If the process hasn't registered custom handlers, the kernel will fall back to
default behavior for that signal (such as killing the process, in the case of
`TERM`).

However, processes which run as PID 1 get special treatment by the kernel, and
default signal handlers won't be applied. If your process doesn't explicitly
handle these signals, a `TERM` will have no effect at all.

For example, if you have Jenkins jobs that do `docker run my-container script`,
sending TERM to the `docker run` process will typically kill the `docker run`
command, but leave the container running in the background.


## What `dumb-init` does

`dumb-init` runs as PID 1, acting like a simple init system. It launches a
single process, and then proxies all received signals to that child process.

Since your actual process is no longer PID 1, when it receives signals from
`dumb-init`, the default signal handlers will be applied, and your process will
behave as you would expect.

If your process dies, `dumb-init` will also die.


## Installing inside Docker containers

You have a few options for using `dumb-init`:


### Option 1: Installing via an internal apt server

If you have an internal apt server, uploading the `.deb` to your server is the
recommended way to use `dumb-init`. In your Dockerfiles, you can simply
`apt-get install dumb-init` and it will be available.


### Option 2: Installing the `.deb` package manually

If you don't have an internal apt server, you can use `dpkg -i` to install the
`.deb` package. You can choose how you get the `.deb` onto your container
(mounting a directory or `wget`-ing it are some options).


## Usage

Once installed inside your Docker container, simply prefix your commands with
`dumb-init`. For example:

$ docker run my_container dumb-init python -c 'while True: pass'

Running this same command without `dumb-init` would result in being unable to
stop the container without SIGKILL, but with `dumb-init`, you can send it more
humane signals like TERM.


## See also

* [Docker and the PID 1 zombie reaping problem (Phusion Blog)](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/)
* [Trapping signals in Docker containers (@gchudnov)](https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86)
* [pgctl](https://github.com/Yelp/pgctl)
9 changes: 8 additions & 1 deletion debian/changelog
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
dumb-init (0.0.2) unstable; urgency=low

* Exit with the same exit status as the process we call.
* Print a more useful help message when called with no arguments.

-- Chris Kuehl <ckuehl@yelp.com> Thu, 06 Aug 2015 13:51:38 -0700

dumb-init (0.0.1) unstable; urgency=low

* Initial release.

-- Chris Kuehl <ckuehl@yelp.com> Wed, 29 Jul 2015 15:39:11 -0700
-- Chris Kuehl <ckuehl@yelp.com> Thu, 06 Aug 2015 13:51:38 -0700
36 changes: 31 additions & 5 deletions dumb-init.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,35 @@ void signal_handler(int signum) {
}
}

void print_help(char *argv[]) {
fprintf(stderr,
"Usage: %s COMMAND [[ARG] ...]\n"
"\n"
"Docker runs your processes as PID1. The kernel doesn't apply default signal\n"
"handling to PID1 processes, so if your process doesn't register a custom\n"
"signal handler, signals like TERM will just bounce off your process.\n"
"\n"
"This can result in cases where sending signals to a `docker run` process\n"
"results in the run process exiting, but the container continuing in the\n"
"background.\n"
"\n"
"A workaround is to wrap your script in this proxy, which runs as PID1. Your\n"
"process then runs as some other PID, and the kernel won't treat the signals\n"
"that are proxied to them specially.\n"
"\n"
"The proxy dies when your process dies, so it must not double-fork or do other\n"
"weird things (this is basically a requirement for doing things sanely in\n"
"Docker anyway).\n",
argv[0]
);
}

int main(int argc, char *argv[]) {
int signum;
char* debug_env;
int signum, exit_status, status = 0;
char *debug_env;

if (argc < 2) {
fprintf(stderr, "Try providing some arguments.\n");
print_help(argv);
return 1;
}

Expand Down Expand Up @@ -70,10 +93,13 @@ int main(int argc, char *argv[]) {
if (debug)
fprintf(stderr, "Child spawned with PID %d.\n", child);

waitpid(child, NULL, 0);
waitpid(child, &status, 0);
exit_status = WEXITSTATUS(status);

if (debug)
fprintf(stderr, "Child exited, goodbye.\n");
fprintf(stderr, "Child exited with status %d, goodbye.\n", exit_status);

return exit_status;
}

return 0;
Expand Down
15 changes: 15 additions & 0 deletions test
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/sh -eux
run_tests() {
./test-proxies-signals
./test-exit-status
./test-help-message
}

cd tests

echo "Running tests in normal mode."
run_tests

echo "Running tests in debug mode."
export DUMB_INIT_DEBUG=1
run_tests
2 changes: 1 addition & 1 deletion test/print-signals → tests/lib/print-signals
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Print received signals into a file, one per line
file="$1"

. ./testlib.sh
. ./lib/testlib.sh

for i in $(catchable_signals); do
trap "echo $i > \"$file\"" "$i"
Expand Down
File renamed without changes.
10 changes: 10 additions & 0 deletions tests/test-exit-status
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh -u
# dumb-init should exit with the same exit status as the process it launches.
for i in $(seq 0 255); do
status=$(dumb-init sh -c "exit $i"; echo $?)

if [ "$status" -ne "$i" ]; then
echo "Error: Expected exit status $i, got $status."
exit 1
fi
done
18 changes: 18 additions & 0 deletions tests/test-help-message
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh -u
# dumb-init should say something useful when called with no arguments, and exit
# nonzero.

status=$(dumb-init > /dev/null 2>&1; echo $?)

if [ "$status" -ne 0 ]; then
msg=$(dumb-init 2>&1 || true)
msg_len=${#msg}

if [ "$msg_len" -le 50 ]; then
echo "Error: Expected dumb-init with no arguments to print a useful message, but it was only ${msg_len} chars long."
exit 1
fi
else
echo "Error: Expected dumb-init with no arguments to return nonzero, but it returned ${status}."
exit 1
fi
6 changes: 4 additions & 2 deletions test/test → tests/test-proxies-signals
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
#!/bin/sh -eum
# dumb-init should proxy all possible signals to the child process.

# Try sending all signals via dumb-init to our `print-signals` script, ensure
# they were all received.
. ./testlib.sh
. ./lib/testlib.sh

# The easiest way to communicate with the background process is with a FIFO.
# (piping spawns additional subshells and makes it hard to get the right PID)
fifo=$(mktemp -u)
mkfifo -m 600 "$fifo"
read_cmd="timeout 1 head -n1 $fifo"

dumb-init ./print-signals "$fifo" &
dumb-init ./lib/print-signals "$fifo" &
pid="$!"

# Wait for `print-signals` to indicate it's ready.
Expand Down

0 comments on commit b535104

Please sign in to comment.