Memo:

アイデアや気づきとかが雑に書き殴られる

docker container のとめかた

このような Dockerfile を $docker build . -t webapp:latest でビルドした後、$ docker run webapp:latest でコンテナを立ち上げることができる。

# Dockerfile
FROM python

WORKDIR /app

COPY . /app
RUN pip install flask

ENTRYPOINT ["bash", "entorypoint.sh"]
# entorypoint.sh
echo "Starting server..."
python3 webapp.py
# webapp.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

app.run(host="0.0.0.0")

そして、その後停止するためには、$ docker stop をすれば良い。

$ docker stop b42e16eddd7d

kill-docker-container_webapp-exec_1 exited with code 137

... はずだが、すぐ終了せず、10秒経ってから強制終了(exited 137)された。

docker stop とは?

公式ドキュメントによると、以下のように説明されている。

1つまたは複数の実行中コンテナを 停止stop します。
コンテナ内のメイン・プロセスが SIGTERM を受信し、一定期間の経過後、 SIGKILL を送信します。.... (docker stop — Docker-docs-ja 24.0 ドキュメント

SIGTERM, SIGKILL は Unix / Linux でプロセスに対して送られる signal 。

SIGTERM を受け取ったプロセスは、ハンドラがあれば正常終了のための処理を行って停止という動きをできる。
プロセスに signal を送るには、kill コマンドを利用できる。

$ kill -15 { PID (Process ID) }

SIGKILL を受け取ったプロセスは、強制的に終了させられる。ちなみに、ゾンビプロセスは既に終了していることになっているため効かないらしい。もちろん、これも kill コマンドで送信できる。

$ kill -9 { PID (Process ID) }

つまり、exited 137 となったということは、SIGTERM では止まらず 10s 後、SIGKILL によって強制的に終了させられたことを表している。

コンテナ内のメイン・プロセスが SIGTERM を受信し...

docker stop コマンドの説明で引っ掛かるのはここ。docker stop コマンドを実行すると、コンテナの「メイン・プロセス」に対して SIGTERM が送られるらしい。

コンテナのメインプロセスとは何だろう?

コンテナのメインプロセス

コンテナでメインとして実行するプロセスは、 Dockerfile の最後に書かれている ENTRYPOINT か CMD か、あるいは両方によって指定します。 (コンテナ内で複数のサービスを実行 — Docker-docs-ja 19.03 ドキュメント

なるほど、 Dockerfile の一番最後のコマンドがメインプロセスとして扱われるらしい。今回作成した dockerfile では bash entorypoint.sh がメインプロセスとなるはず。

docker container 内でプロセスを見てみると、bash entorypoint.sh は PID 1 を割り当てられていた。

root@5c63c147bb2e:/app# ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 bash entorypoint.sh
root@5c63c147bb2e:/app#

--init プラグを使うと、コンテナ内で PID 1 として使われるべき init プロセスを指定できます。(Docker run リファレンス — Docker-docs-ja 24.0 ドキュメント

リファレンスを漁ってみると、このような説明があった。

メインプロセスは、init プロセスで、PID 1 が割り当てられているということだと思う。

init プロセスは UNIX / Linux のシステムで、ブートローダーによって起動したカーネルが起動するプログラムで、init が全てのプロセスを起動する。そして PID 1 が付与される。

init - Wikipedia

手元にある ubuntu で PID 1 を見ると、確かに init だった。そして /sbin/init は systemd へのシンボリックリンクだった。

$ ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 /sbin/init splash
$ ll /sbin/init 
lrwxrwxrwx 1 root root 20  110  2022 /sbin/init -> /lib/systemd/systemd*

ちなみに、このプロセスを殺そうとしても無駄だ(PID 1 は特別に保護されているらしい)

$  kill -9 1
bash: kill: (1) - Operation not permitted

$ sudo kill -9 1
(何も起きない)

container を止める

最初の docker container を止めるためには、python3 webapp.py のコマンドをメインプロセスにする必要がある。(bash が実行したコマンドは子プロセスになる)

そのためには、bash から別のコマンドを実行する際に exec を付けて実行すれば OK。

# entorypoint.sh
echo "Starting server..."
exec python3 webapp.py

確認してみると、ちゃんと PID 1 が python のコマンドになっている。

root@382c9f96490b:/app# ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 python3 webapp.py

しかし、まだちゃんと停止してくれない。
これは、前述の PID が保護されていることによるものだろう。

$ docker stop cea116b31db2

kill-docker-container_webapp-exec_1 exited with code 137

SIGTERM で終了させるためには、--init で init プロセス(メインプロセス)を PID 1 以外にしたり、SIGTERM 用のハンドラを用意したりする。

# webapp.py
from flask import Flask
import signal

def handler(signum, frame):
    print(f"signal {signum} received!")
    exit(0)
    
signal.signal(signal.SIGTERM, handler)

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

app.run(host="0.0.0.0")

このように SIGTERM のハンドラを追加して、docker stop を実行してみるとすぐに終了した。

やったね。

$ docker stop 78a6563659b5
webapp-exec_1  | signal 15 received!
kill-docker-container_webapp-exec_1 exited with code 0

疑問

無事 docker container を止めることができた。しかし、何故メインプロセスが停止した時にコンテナが止まるのか(逆にコンテナを止めるためにメインプロセスを停止させている?メインプロセス自体がコンテナの実態?多分どちらも違う)。

逆にメインプロセスがメモリ不足で終了した際に、コンテナが止まるのはどういう仕組みなのか....

そもそもコンテナを止めるとはどういうことなのか?1
メインプロセスとコンテナの関係とは?

その謎を解明するため、我々調査隊はアマゾンの奥地へと向かった――


  1. もしかすると、私は本当の意味でコンテナを止められていないのかもしれません。