問題:Docker上ではコンテナのステータスが Up と表示されているのに、Webサイトにアクセスすると 502 Bad Gateway が表示される。これは非常にストレスのたまる状況です。ログを確認すると、アプリケーションがずっと前にフリーズしていたことが分かります。
問題は、Dockerがメインのプロセス(process)のみを監視している点にあります。メモリ不足(OOM)、デッドロック、データベース接続の切断などが発生しても, プロセス自体が終了していなければ、Dockerは「正常」と判断し続けます。この時、システムは「植物状態」、つまり完全には死んでいないものの、機能もしない状態に陥ります。
私も最初のプロジェクトでは、安易に restart: always だけを使っていました。サーバーの負荷が高まりデータベースのレスポンスが遅くなった際、Node.jsアプリの接続がハングしてしまいましたが、コンテナの表示は正常なままでした。自分では安定していると思い込んでいた一方で、顧客からは不満の声が絶えませんでした。これを根本的に解決するには、Restart Policies と Healthcheck の組み合わせが必要です。
1. Restart Policiesによる自動復旧
Restart Policies(再起動ポリシー)は、コンテナが予期せず停止したりクラッシュしたりした際に、自動的に再起動させる機能です。Dockerには主に4つのオプションがあります。
- no: デフォルト。コンテナが停止しても、Dockerは何もしません。
- always: 停止した理由に関わらず、常に再起動します。サーバーを再起動した場合も、Dockerデーモンに合わせてこのコンテナも自動的に起動します。
- on-failure: 終了コード(exit code)が0以外の場合のみ再起動します。処理が終われば終了するデータ処理ジョブなどに適しています。
- unless-stopped:
alwaysと似ていますが、一つ利点があります。明示的にdocker stopコマンドで停止させた場合は、手動で起動するまで停止したままになります。
<a href="https://itfromzero.com/ja/docker-ja/docker-compose%ef%bc%9a%e5%ae%9f%e8%b7%b5%e3%81%a7%e5%ad%a6%e3%81%b6%e3%83%9e%e3%83%ab%e3%83%81%e3%82%b3%e3%83%b3%e3%83%86%e3%83%8a%e3%82%a2%e3%83%97%e3%83%aa%e3%82%b1%e3%83%bc%e3%82%b7%e3%83%a7.html">docker-compose.yml</a> での設定は非常にシンプルです:
services:
web-app:
image: nginx:1.25-alpine
restart: unless-stopped
私は通常、unless-stopped を優先して使います。これはサーバーのメンテナンス後にアプリが自動復旧するのを助ける一方で、デバッグのために意図的にアプリを停止させている時に、コンテナが勝手に再起動して邪魔をすることを防げるからです。
2. Healthcheck:コンテナ専用の「主治医」
Restart Policiesがコンテナの「生か死か」しか判断できないのに対し、Healthcheckはアプリケーションが 効果的に動作しているか を把握します。これは、定期的に「ねえ、まだ応答できる?」と信号を送って確認するようなものです。
把握しておくべき主要なパラメータ
- test: チェック用のコマンド(通常は
curlやpg_isreadyを使用)。 - interval: チェックの間隔(例:30秒ごと)。
- timeout: 応答がない場合に失敗とみなすまでの時間。
- retries:
unhealthy(異常)と判断するまでの連続失敗回数(例:3回)。 - start_period: アプリの起動を待つ時間。Java Spring Bootのようなアプリは起動に45秒ほどかかる場合があるため、チェックを開始する前に準備期間を設けます。
3. Node.jsアプリケーションでの実践的な設定
例えば、ポート3000で動作するアプリケーションがあるとします。単に動くことを祈るのではなく、Dockerに強制的にチェックさせましょう。
services:
my-api:
image: node-app:v1
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
この設定では、エンドポイント /health が500エラーを返したりタイムアウトしたりすると、curl -f コマンドがエラーを返します。start_period: 40s のおかげで、Dockerはアプリがライブラリの読み込みを完了するまで辛抱強く待ち、その後に健康状態の判定を開始します。
DockerfileにHealthcheckを組み込む
最善の方法は、このメカニズムをイメージ自体にパッケージ化して、どの環境でも保護されるようにすることです:
FROM node:18-alpine
RUN apk add --no-cache curl
# ... アプリのセットアップ ...
HEALTHCHECK --interval=1m --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/ || exit 1
CMD ["npm", "start"]
4. サービス間のスムーズな連携
よくあるミスは、アプリケーションがデータベースよりも早く起動してしまい、起動直後に接続エラーが発生することです。複雑な待機スクリプトを使う代わりに、depends_on と健康状態の条件(condition)を組み合わせて使いましょう:
services:
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
app:
image: my-node-app
depends_on:
db:
condition: service_healthy
これで、app コンテナは db が実際に接続を受け入れられる状態になるまで待機するようになります。この方法は、単に sleep 10 で盲目的に待つよりも、はるかにプロフェッショナルで信頼性が高いです。
5. リソースに関する注意点:過度なチェックは禁物
Healthcheckは無料ではありません。実行されるたびに、少量のCPUとメモリを消費します。もし20個のコンテナすべてに interval: 1s を設定すると、サーバーのリソースが相互チェックのためだけに浪費されてしまいます。
通常のサービスであれば、30秒から1分程度が妥当な数値です。データベースの生存確認のために重いSQLクエリを実行するようなことは避け、軽量なチェックコマンドを優先しましょう。
まとめ
Restart PoliciesとHealthcheckを組み合わせることで、自己修復(self-healing)システムを構築できます。もう docker restart コマンドを打つためだけに、午前2時に目を覚ます必要はありません。
覚えておくべき3つのルール:
- ほとんどのWebサービスには
unless-stoppedを使用する. - アプリが起動完了前に強制終了されないよう、必ず
start_periodを設定する。 - 依存関係のあるサービスの起動順序を管理するために
service_healthyを使用する。
これらのテクニックを今すぐ取り入れて、より堅牢で安定したアプリケーション運用を実現しましょう。
