Edited at

Docker Containerの終了時にExit Code 137 (SIGKILL)が出る時の対処法


概要

これの日本語版のようなもの


問題

複数のコマンドを扱うDockerを使うとき、こんな感じのスクリプトになりませんか?

run.sh

#!/bin/sh -e

pip install --upgrade poetry
poetry config settings.virtualenvs.create false
poetry install
python ./manage.py runserver 0.0.0.0:8000

backend.dockerfile

from python:3.6

env PYTHONUNBUFFERED 1
run mkdir /code
workdir /code

docker-compose.yml

version: '3'

services:
db:
image: postgres:alpine
environment:
POSTGRES_PASSWORD: fingine
POSTGRES_USER: fingine
POSTGRES_DB: fingine
backend:
build:
context: ./docker
dockerfile: backend.dockerfile
command: bash -c ./run.sh
stop_signal: SIGINT
env_file: ./docker/dev.env
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- db

これ、docker-compose upした後にdocker-compose stopすると、fingine_backend_1 exited with code 137と、エラーコード137、つまりSIGKILLが返された上で終了する。つまり、終了まで10秒ほど待たされる上に強制終了を食らう

で、調べるとやれ、メモリが足りないだのtrapコマンドを仕掛けろだのと的外れな回答ばかりで嫌気が差した。だから書いてやった。


解決法

注目すべき点はexecビルトインユーティリティコマンドにあります。通常、shell芸で書かれたコマンドは新しいプロセスを生成してそこでコマンドを実行します。この新しく生成されたプロセスでは、後述する問題によってシグナルを受信することができません。

しかし、execにコマンドライン引数としてコマンドとその引数を指定すると、コマンドのプロセスを生成せずに、シェルのプロセスとそのコマンドのプロセスを置き換えます。つまり、先程のrun.shを次のように書き換えると解決するのでは・・・と考えますよね?

run.sh

#!/bin/sh -e

pip install --upgrade poetry
poetry config settings.virtualenvs.create false
poetry install
exec
python ./manage.py runserver 0.0.0.0:8000 # <-- これ。

実はこれだと不十分っす。 というのも、Dockerの終了シグナルはコンテナ内のプロセスIDが1のプロセスに対してのみ発火します。さあ、docker-compose.ymlを見てみましょう。

docker-compose.yml

version: '3'

services:
db:
image: postgres:alpine
environment:
POSTGRES_PASSWORD: fingine
POSTGRES_USER: fingine
POSTGRES_DB: fingine
backend:
build:
context: ./docker
dockerfile: backend.dockerfile
command: bash -c ./run.sh # <-- これ
stop_signal: SIGINT
env_file: ./docker/dev.env
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- db

要するに、bashコマンドをrun.shを引数にとって実行している、つまり、run.shの実行には新しいプロセスが生成される、と。 そういうことですね。

というわけで、このdocker-compose.ymlに記述されているrun.shの呼び出しをbashではなく、execを使って呼び出されるようにする必要があります。

幸いなことに、dockerには手軽にexecを使ったコマンドを記述する方法があり、今回はこれを用いることにします。

docker-compose.yml

version: '3'

services:
db:
image: postgres:alpine
environment:
POSTGRES_PASSWORD: fingine
POSTGRES_USER: fingine
POSTGRES_DB: fingine
backend:
build:
context: ./docker
dockerfile: backend.dockerfile
command: ['./run.sh'] # <-- これ。配列として書き直すとexecに続くコマンドとして認識される。
stop_signal: SIGINT
env_file: ./docker/dev.env
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- db

さあこれでdocker-compose upしてdocker-compose stopしてみましょう。コンテナを10秒も待つことなく、fingine_backend_1 exited with code 0、つまりコード0、正常終了させる事ができます。素晴らしい!!


最後に

Schlawackさんの記事によって筆者のdockerの問題が解決した事に謝辞を申し上げます。


参考記事

Hynek Schlawack, Why Your Dockerized Application Isn’t Receiving Signals, 19 June 2017