その原因の多くは、
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問題を根本解消できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜgraceful shutdownが必要なのか
SIGKILLによる強制終了では、アプリはシャットダウン処理を一切実行できません。現場でよく見る被害は次のとおりです。
・HTTPリクエストの強制切断:応答を送る直前のリクエストがドロップされ、クライアントに502エラーが返ります・DBコネクションの不正切断:コネクションプールが正常にクローズされず、PostgreSQL側に残存接続が積み上がります
・ログ・一時ファイルの破損:バッファにたまったログが書き込まれないまま終了し、次回起動時にファイルが壊れた状態で残ります
・キャッシュフラッシュの省略:Redisのような非同期永続化を使うストアがデータを書き出す前に終了します
graceful shutdownとは、SIGTERMを受け取ったアプリが新規リクエストの受付を止め、処理中のものだけを完了してから正常終了する動作です。これを実現するには「SIGTERMがアプリのPID 1に届いていること」「アプリがSIGTERMハンドラを実装していること」の2つが揃っていなければなりません。
Dockerのシグナル終了フローを理解する
1. docker stopの動作シーケンス
docker stop・docker compose down・docker 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日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:docker compose secretsでパスワードを安全に渡す方法|environmentを使わないシークレット設計
- この記事の属するカテゴリ:Dockerへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます