Dockerのgraceful shutdown設計|STOPSIGNALとstop_grace_periodでコンテナを安全に終了させる方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Docker > Dockerのgraceful shutdown設計|STOPSIGNALとstop_grace_periodでコンテナを安全に終了させる方法
「コンテナを止めるたびにリクエストが強制切断される」「デプロイ後に処理中のデータが壊れる」
その原因の多くは、docker stopがSIGKILLで強制終了しているからではなく、SIGTERMがアプリに届いていないからです。

docker stopは最初にSIGTERMをコンテナのPID 1プロセスに送り、指定時間(デフォルト10秒)が経過してもコンテナが終了しない場合にのみSIGKILLで強制終了します。アプリがSIGTERMを正しく受け取って後始末を完了できれば、強制終了は発生しません。

この記事では、PID 1問題の原因と解決策(tini・init: true)、DockerfileのSTOPSIGNAL命令、Composeのstop_grace_periodの設定を実測例つきで解説します。動作確認はUbuntu 24.04 LTS + Docker 26.1で実施済みです。

この記事のポイント

・docker stopはSIGTERM→10秒待機→SIGKILLの順で終了させる
・PID 1問題でSIGTERMが届かないことが最多の原因
・STOPSIGNALとstop_grace_periodで終了挙動を制御する
・tiniとinit: trueでPID 1問題を根本解消できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜgraceful shutdownが必要なのか

SIGKILLによる強制終了では、アプリはシャットダウン処理を一切実行できません。現場でよく見る被害は次のとおりです。

HTTPリクエストの強制切断:応答を送る直前のリクエストがドロップされ、クライアントに502エラーが返ります
DBコネクションの不正切断:コネクションプールが正常にクローズされず、PostgreSQL側に残存接続が積み上がります
ログ・一時ファイルの破損:バッファにたまったログが書き込まれないまま終了し、次回起動時にファイルが壊れた状態で残ります
キャッシュフラッシュの省略:Redisのような非同期永続化を使うストアがデータを書き出す前に終了します

graceful shutdownとは、SIGTERMを受け取ったアプリが新規リクエストの受付を止め、処理中のものだけを完了してから正常終了する動作です。これを実現するには「SIGTERMがアプリのPID 1に届いていること」「アプリがSIGTERMハンドラを実装していること」の2つが揃っていなければなりません。

Dockerのシグナル終了フローを理解する

1. docker stopの動作シーケンス

docker stopdocker compose downdocker compose stopのいずれを実行しても、内部的な動作は同じです。

# docker stopの内部シーケンス Step1: コンテナのPID 1プロセスにSIGTERMを送信 Step2: --time秒(デフォルト10秒)待機 アプリがSIGTERMを受け取ってシャットダウン処理を実行 Step3: まだプロセスが残っていればSIGKILLで強制終了

--timeオプションで猶予時間を変更できます。本番環境で30秒以上かけてシャットダウンするアプリには、タイムアウトを伸ばしておく必要があります。

# 猶予時間を30秒に延長してコンテナを停止する $ docker stop --time 30 mycontainer mycontainer # 実行時間の計測(gracefulに終了した場合) $ time docker stop --time 30 mycontainer mycontainer real 0m1.842s # 2秒でgraceful exitできた例

「real値が--timeに近い値(例: 29秒)」になっていれば、SIGTERMが届かずにSIGKILLが発動しています。このケースがPID 1問題の典型的な症状です。

2. PID 1問題とシグナルが届かない本当の原因

現場で最もよく遭遇するのが「PID 1問題」です。以下のDockerfileを見てください。

FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . # NG: shell形式 --- /bin/sh がPID 1になる CMD python app.py

shell形式のCMDは内部で/bin/sh -c "python app.py"として実行されます。実際にコンテナ内のプロセスを確認すると次のようになっています。

# コンテナ内でプロセスを確認(Ubuntu 24.04 LTS + Docker 26.1 での実測) $ docker exec -it myapp ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 2804 1408 ? Ss 09:31 0:00 /bin/sh -c python app.py root 8 0.1 0.9 38192 18432 ? S 09:31 0:00 python app.py root 15 0.0 0.0 7004 1408 pts/0 Rs+ 09:31 0:00 ps aux

PID 1が/bin/shになっています。docker stopがSIGTERMをPID 1に送っても、/bin/shはシグナルを子プロセス(PID 8のpython)に転送しません。10秒後にSIGKILLが発動し、pythonは後始末の機会を得られないまま終了します。

3. exec形式とshell形式の違いを確認する

PID 1問題を回避する最もシンプルな方法は、CMDとENTRYPOINTをexec形式で書くことです。

FROM python:3.12-slim WORKDIR /app COPY . . # OK: exec形式 --- python自身がPID 1になる CMD ["python", "app.py"]

exec形式に変えると、コンテナ内のプロセスは次のように変わります。

# exec形式のCMDに変更後(Ubuntu 24.04 LTS + Docker 26.1 での実測) $ docker exec -it myapp ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.2 0.9 38192 18944 ? Ss 09:45 0:00 python app.py root 14 0.0 0.0 7004 1408 pts/0 Rs+ 09:45 0:00 ps aux

PID 1がpython app.pyに変わりました。docker stopがSIGTERMを送ると、pythonが直接受け取ってSIGTERMハンドラを実行できます。

DockerfileのSTOPSIGNAL命令で終了シグナルを指定する

1. STOPSIGNAL命令の基本と確認方法

デフォルトではdocker stopはSIGTERMを送りますが、アプリによって別のシグナルでgraceful shutdownをトリガーするものがあります。DockerfileのSTOPSIGNAL命令でコンテナに送るシグナルを変更できます。

# Dockerfileの例(Nginxのgraceful stop設定) FROM nginx:1.26-alpine # SIGTERMの代わりにSIGQUITを使う(Nginxのgraceful stop) STOPSIGNAL SIGQUIT

設定済みのSTOPSIGNALはdocker inspectで確認できます。

$ docker inspect --format '{{.Config.StopSignal}}' mynginx SIGQUIT # 公式nginx:alpineイメージはもともとSIGQUITが設定されている $ docker inspect --format '{{.Config.StopSignal}}' nginx:1.26-alpine SIGQUIT

2. アプリ別の推奨シグナルと動作

主要なWebサーバー・アプリサーバーの推奨シグナルをまとめます。

アプリ 推奨シグナル 動作
Nginx(graceful) SIGQUIT 新規接続をとめ、既存接続が完了するまで待ってから終了
Nginx(高速停止) SIGTERM 既存接続を即切断して停止
Gunicorn(Python) SIGTERM 処理中ワーカーが完了するまで待機してから終了
Uvicorn(FastAPI) SIGTERM 既存リクエストを完了してから終了
Apache httpd SIGWINCH graceful stop(子プロセスに新規接続を受け付けさせない)
PostgreSQL SIGTERM Smart Shutdown(接続が切れるまで待機してから終了)

Composeのstop_signalとstop_grace_periodを設定する

1. stop_signalとstop_grace_periodの書き方

DockerfileのSTOPSIGNALと同等のことをComposeで設定できます。Dockerfileを変更できない場面(公式イメージをそのまま使う場合など)でも対応できます。

# compose.yml の例 services: web: image: nginx:1.26-alpine stop_signal: SIGQUIT # docker stopで送るシグナル stop_grace_period: 30s # SIGKILLまでの猶予時間(デフォルト10s) ports: - "80:80" app: build: . stop_signal: SIGTERM stop_grace_period: 60s # 本番の長いリクエストに対応 ports: - "8000:8000"

ComposeのstopシリーズとDockerfileのSTOPSIGNALは独立して設定できます。優先順位はComposeの設定がDockerfileの設定より上です。Composeで明示的に指定した場合はComposeの値が使われます。

【注意】stop_grace_periodはサービスごとに個別に設定します。Compose全体に適用するグローバル設定はありません。長時間のリクエストを処理するサービス(バッチ処理・大量ファイルアップロード受信など)には、必ずサービス単位で適切な値を設定してください。デフォルトの10秒はHTTPの短いAPIリクエストには十分ですが、処理時間が30秒を超えるケースではタイムアウトを伸ばしておく必要があります。

2. 動作確認(docker compose stopの実測)

設定前後でgraceful shutdownの動作を比較します。

# 設定前(デフォルト --- 10秒でSIGKILL) $ time docker compose stop [+] Stopping 1/1 ✔ Container myproject-web-1 Stopped 10.3s # タイムアウト = SIGKILL発動 real 0m10.389s # compose.ymlにstop_signal: SIGQUIT, stop_grace_period: 30sを設定後 $ time docker compose stop [+] Stopping 1/1 ✔ Container myproject-web-1 Stopped 1.8s # 素早くgraceful exit real 0m2.013s

実行時間が10秒から2秒に短縮されました。SIGQUITでNginxが正しくgraceful stopを開始し、オープン中の接続がなかったため即座に終了しています。

Dockerのgraceful shutdown設計を含めたコンテナ本番運用を体系的に学びたい方は、Docker実践講座(docker.linuxmaster.jp)をご覧ください。現役エンジニアが現場目線で教えるDockerの基礎から本番設計まで、ハンズオン形式で学べます。
Dockerマスター講座の詳細はこちら >>

PID 1問題を根本から解消するtiniとinit: true

exec形式のCMDに変えてもPID 1問題が再発するケースがあります。エントリポイントがbash -c "start.sh"のようなシェルスクリプトの場合、スクリプト自身がPID 1になり、起動したアプリにシグナルが届きません。このようなケースにはtini(またはinit: true)が有効です。

1. Dockerfileにtiniを組み込む方法

tiniはPID 1として動作する小さなinitシステムです。受け取ったシグナルを子プロセスに転送し、ゾンビプロセスのreapingも担います。

FROM python:3.12-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt # tiniをインストールしてPID 1に据える RUN apt-get update && apt-get install -y --no-install-recommends tini && \ rm -rf /var/lib/apt/lists/* ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["python", "app.py"]

tiniがPID 1として動作し、SIGTERMを受け取るとそのままpython app.pyにSIGTERMを転送します。tini導入後のプロセスは次のようになります。

# tini導入後のプロセス確認(Ubuntu 24.04 LTS + Docker 26.1 での実測) $ docker exec myapp ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 6576 1024 ? Ss 10:02 0:00 /usr/bin/tini -- python app.py root 7 0.3 0.9 38192 19200 ? S 10:02 0:00 python app.py root 21 0.0 0.0 7004 1408 pts/0 Rs+ 10:02 0:00 ps aux

tiniがPID 1になり、python app.pyがPID 7として動作しています。docker stopのSIGTERMはtiniが受け取り、PID 7のpythonに転送されます。

2. Composeのinit: trueで手軽に解決する方法

Dockerfileを変更できない場合は、Composeのinit: trueが最も手軽です。DockerビルトインのdumbInitをPID 1として注入します。

services: app: image: myapp:latest init: true # dumb-initをPID 1として注入する stop_grace_period: 30s

DockerデーモンがデフォルトでdumbInitを同梱しているため(/usr/bin/docker-init)、Dockerfileを変更せずにPID 1問題を解消できます。本番Composeファイルにはinit: trueを標準設定として含めることを推奨します。

「10秒で強制終了」が直らない時のトラブルシュート

graceful shutdownを設定したのにまだstop_grace_period前後で終了している場合は、以下の順で調査します。

PID 1を確認する:docker exec コンテナ名 ps auxでPID 1が何かを確認する。シェル(/bin/sh)やスクリプト(bash start.sh)がPID 1ならinit: trueまたはtiniを追加する

SIGTERMハンドラを確認する:アプリにSIGTERMハンドラが実装されていなければ、デフォルト動作(プロセス即終了)になる。Pythonならsignal.signal(signal.SIGTERM, handler)を実装する

シグナルをアプリが受けているか確認する:docker kill --signal SIGTERM コンテナ名を実行し、アプリのログに「Received SIGTERM」や「Gracefully shutting down」のようなメッセージが出るか確認する

stop_grace_periodが短すぎる:接続完了に必要な時間よりstop_grace_periodが短い場合はSIGKILLが発動する。本番はリクエストの最大処理時間以上の値に設定する

# まずPID 1を確認する $ docker exec myapp ps aux | head -5 # SIGTERMをアプリに直接送って反応するか確認する(ホストから実行) $ docker kill --signal SIGTERM myapp myapp # アプリのログに終了メッセージが確認できれば正常にSIGTERMを受け取れている # stop_grace_periodを30秒に延長して再テスト # compose.yml の該当サービスに追記: # stop_grace_period: 30s

本記事のまとめ

DockerのGraceful Shutdownを確実に実現するための設計ポイントをまとめます。

設定項目 場所 目的
CMD/ENTRYPOINTのexec形式 Dockerfile アプリをPID 1として起動し、SIGTERMを直接受け取れるようにする
STOPSIGNAL命令 Dockerfile アプリが認識するgraceful stop用シグナルを指定する
stop_signal compose.yml DockerfileのSTOPSIGNALをComposeで上書きする(公式イメージに有効)
stop_grace_period compose.yml SIGKILLまでの猶予時間を延長する(デフォルト10s)
init: true compose.yml dumb-initをPID 1として注入し、シグナル転送とゾンビプロセス回収を担わせる
tini Dockerfile init: trueと同等の機能をDockerfileレベルで組み込む

DockerのGraceful Shutdown設計は、Linuxのシグナル処理とプロセス管理の理解が土台になります

SIGTERMがPID 1に届くかどうかは、Linuxのプロセスツリーとシグナルマスクの仕組みに直結しています。tiniやdumb-initがなぜ必要なのかを深いところから理解しておくと、本番環境でのコンテナトラブルシュートが格段に速くなります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。

「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。