Millet Porridge

English version of https://corvo.myseu.cn

0%

Docker Series 4: Exit

I’ve refered the Trapping signals in Docker containers, and the offical blogs about stop and kill.

how stop and kill works

stop: The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL. The first signal can be changed with the STOPSIGNAL instruction in the container’s Dockerfile, or the –stop-signal option to docker run.

kill: The main process inside the container is sent SIGKILL signal (default), or the signal that is specified with the –signal option. You can reference a container by its ID, ID-prefix, or name. When you want to send SIGHUP, there is the command docker kill --signal=SIGHUP my_container.

Why bothers?

You may notice that The main process inside the container in both descriptions. What if we have multiprocess in the container?

Let’s create a container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.js
'use strict';
var http = require('http');
var server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(3000, '0.0.0.0');
console.log('server started');
var signals = {
'SIGINT': 2,
'SIGTERM': 15
};
function shutdown(signal, value) {
server.close(function () {
console.log('server stopped by ' + signal);
process.exit(128 + value);
});
}
Object.keys(signals).forEach(function (signal) {
process.on(signal, function () {
shutdown(signal, signals[signal]);
});
});
1
2
3
#!/bin/sh

node main.js
1
2
3
4
5
6
7
8
9
10
11
12
FROM node:10.17.0-alpine

COPY main.js main.js
COPY run.sh /run.sh
RUN chmod +x /run.sh

# ENTRYPOINT ["node", "main.js"]
ENTRYPOINT ["/run.sh"]

# docker build -t test .
# docker run --name my-kill -ti test
# docker stop my-kill

We could exec to watch the process.

1
2
3
4
5
6
7
docker exec -ti my-kill /bin/sh
/ # ps
PID USER TIME COMMAND
1 root 0:00 {run.sh} /bin/sh /run.sh
6 root 0:00 node main.js
18 root 0:00 /bin/sh
23 root 0:00 ps

We can get different results if we modify the entrypoint:

1
2
3
# Use `ENTRYPOINT ["node", "main.js"]`
server started
server stopped by SIGTERM
1
2
3
# Use `ENTRYPOINT ["/run.sh"]`
server started
# stop but wait for 10s

When we use ENTRYPOINT ["node", "main.js"], the node process will receive the SIGTERM signal and close itself.

When we use ENTRYPOINT ["/run.sh"], the node process doesn’t get the proper signal, and it was forcibly shut down.

How to close the container gracefully

Well, we know what the offical description really means. It will only send SIGTERM to the main process. If we have to use the script, we need to pass the signal to the child process.

Using trap to enhance shell scripts

Copy from program.sh(One one process):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env bash
set -x

pid=0

# SIGUSR1 -handler
my_handler() {
echo "my_handler"
}

# SIGTERM -handler
term_handler() {
if [ $pid -ne 0 ]; then
kill -SIGTERM "$pid"
wait "$pid"
fi
exit 143; # 128 + 15 -- SIGTERM
}

# setup handlers
# on callback, kill the last background process, which is `tail -f /dev/null` and execute the specified handler
trap 'kill ${!}; my_handler' SIGUSR1
trap 'kill ${!}; term_handler' SIGTERM

# run application
node main.js &
pid="$!"

# wait forever
while true
do
tail -f /dev/null & wait ${!}
done

Use supervisor or S6

Please refer to S6.

Conclution

I want you to know that Docker doesn’t expected to run multiple processes within a single container, and I don’t encourage reader to write some weird scripts.

If you really need multiprocess support, please consider to use Kubernetes or Docker Dompose.