Docker Composeのhealthcheckを正しく設定する方法
Docker Composeのhealthcheckは何を判定し、depends_onだけではなぜ待機できないのでしょうか。Postgresとアプリのreadiness checkを、実行例とともに解説します。
Docker Compose の healthcheck が実際に行うこと
Docker Compose の healthcheck は、Docker が一定間隔でコンテナ内で実行する1つのコマンドです。Docker はログを読み取らず、ポートを監視せず、プロセス一覧も確認しません。コマンドを実行し、終了コードを読み取り、コンテナに1つの状態として starting、healthy、または unhealthy を保存します。終了コード 0 は正常を意味します。その他の終了コードは異常を意味します。終了コード 2 は Docker 用に予約されているため、意図的に返さないでください。
仕組みはこれだけです。healthcheck の問題のほとんどは、同じ原因です。記述したコマンドが、確認したかった内容とは別の質問に答えています。このガイドでは、すでに VPS で compose ファイルを記述する方法を理解しているものとし、スタックが誤った順序で起動する段階から説明します。
services:
api:
image: ghcr.io/example/api:1.4.0
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30stest の値には、役立つ形式が2つあります。CMD で始まるリストはシェルを介さずにコマンドを直接実行するため、パイプ、&&、変数展開は機能しません。CMD-SHELL で始まるリストは、残りの部分を1つの文字列としてコンテナ内の /bin/sh -c に渡します。チェックでシェル構文が必要な場合は、この形式を使用します。通常の文字列は CMD-SHELL として扱われます。["NONE"] だけで構成されたリストは、イメージの Dockerfile に組み込まれた healthcheck を削除します。
チェックはコンテナ内で実行されるため、指定するすべてのバイナリがそのイメージに存在している必要があります。まず確認してください。curl がない slim image では、アプリケーションログに一切現れない理由で、コンテナが常に異常な状態になることがあります。手動でテストします。
docker compose exec api curl --versionバイナリがない場合は OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown が返ります。Alpine ベースのイメージでは、通常は代わりに BusyBox の wget が含まれているため、チェックは ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"] になります。
interval、retries、start_periodの組み合わせ
5つの設定でタイミングを制御します。デフォルト値はComposeではなくDocker Engineから取得されます。
interval: コンテナがstart periodを過ぎた後、2回のチェック間に置く時間です。デフォルトは30sです。timeout: 1回のチェックに許可される実行時間です。この時間を超えるとDockerは実行を停止し、その実行を失敗としてカウントします。デフォルトは30sです。retries: 状態がunhealthyに変わるまでに必要な連続失敗回数です。デフォルトは3です。start_period: コンテナの起動後に設ける猶予期間です。デフォルトは0sです。start_interval: start period中にチェックを実行する間隔です。デフォルトは5sで、Docker Engine 25.0以降が必要です。
重要なルールは、start period中の失敗はretriesにカウントされず、コンテナはstartingのままになることです。チェックが初めて成功すると、コンテナはhealthyになり、start periodは未使用の時間が残っていても直ちに終了します。チェックが失敗したままstart periodが終了すると、通常のカウントダウンが始まります。コンテナはretries回連続で失敗してからunhealthyとしてマークされます。
したがって、コンテナの起動からunhealthyになるまでの最長時間は、start_periodにretriesとintervalの積を加え、さらにtimeoutを加えた時間です。上のファイルの値では、30に5と13の積を加えるため、95秒です。deploy timeoutを設定する前に、この数値を書き留めてください。60秒後に諦めるrolloutでは、このコンテナが最終状態になるのを待てません。
ここでよくある誤りは、起動の遅さに対応するためにretriesを増やすことです。一度は機能しますが、その後は常に悪影響が出ます。起動に8回の再試行が必要だったサービスは、本番環境で8回連続して失敗しても、何も検知されなくなります。代わりにstart_periodを使用してください。これは最初の成功前にのみ適用されます。
depends_on だけでは何も保証されない理由
depends_on の短縮形式が、混乱の主な原因です。
api:
depends_on:
- dbこれは1つのことだけを意味します。api コンテナーより前に db コンテナーを起動します。Compose はコンテナーの作成と起動が完了するまで待機します。ただし、PostgreSQL の初回初期化が完了するまで待機するわけではありません。また、port 5432 が接続を受け付けるまで待機するわけでもありません。アプリは約1秒後に起動し、まだ何も listening していない port に接続して終了します。ログには Connection refused、またはサーバーが起動していてもまだ復旧中の場合は FATAL: the database system is starting up と表示されます。
実際に必要なのは長い形式です。
api:
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullycondition には3つの値があります。service_started は短縮形式と同じです。service_healthy は、依存先が healthy であると報告するまで依存サービスを保留します。これは、依存先が compose file またはその image で healthcheck を定義している場合にのみ意味があります。service_completed_successfully は、database migration などの one shot container が status 0 で終了するまで待機します。
condition の横には追加のフィールドが2つあります。restart: true を指定すると、依存サービスの更新後にこのサービスを再起動するよう Compose に指示します。required: false を指定すると、依存先がない場合のエラーを警告に変更します。
ここで、問題になりやすい制限があります。これらの条件は stack の起動時に評価されます。これは起動順序を定めるものであり、監視ルールではありません。午前3時に database が再起動しても、service_healthy が再評価されることはなく、条件を再度満たすためにアプリが再起動されることもありません。アプリケーションコードは、自身で再接続する必要があります。docker compose up --no-deps api は設計上、この仕組み全体をスキップします。docker start で container を直接起動する場合も同様です。
プロセスの存在ではなく、準備状態をテストするチェックを作成する
pgrep nginx のようなチェックで確認できるのは、プロセステーブルのエントリが存在することだけです。サービスがリクエストに応答できるかどうかは確認できません。Webアプリケーションは、データベース接続プールが停止した後もリスニングソケットを開いたままにすることがあり、その間ずっとプロセスチェックは正常のままです。
コンテナが本来の処理を実行できるか確認します。
- HTTPサービスでは、実際のエンドポイントにリクエストを送信します。
curl -fsSは-fにより、400以上のステータスで non zero を返すため、アプリケーションの障害による500もチェック失敗になります。 - PostgreSQLでは
pg_isreadyを使用します。サーバーが接続を受け入れている場合は0、拒否している場合は1、まったく応答しない場合は2、渡したパラメーターが正しくない場合は3で終了します。 - Redisでは
redis-cli pingを使用します。PONGを出力し、0で終了します。 - MariaDBでは、公式イメージに
healthcheck.shスクリプトが含まれており、healthcheck.sh --connect --innodb_initializedがメンテナーの文書に記載されている形式です。
pg_isready には、知っておくべき注意点が1つあります。空のデータディレクトリで初回起動すると、公式の postgres イメージは、一時サーバーに対して初期化を実行します。このサーバーはUnixソケットでのみリッスンします。ホスト引数なしの pg_isready はこのソケットを使用するため、TCPポート5432がアプリケーションに対してまだ閉じていても、「接続を受け入れている」と応答できます。チェックの接続先を明示的にTCPに指定すると、この問題は解消します。一時サーバーはTCPでは応答しないためです。
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30sドル記号を2つ重ねるのは誤記ではありません。Composeはファイルの読み込み時に $VAR 自体を展開するため、ホスト環境の値がチェックに埋め込まれます。$$ はこれを単一の $ にエスケープします。そのため、コンテナ内のシェルがコンテナ独自の環境に対して展開します。
正しい順序で起動する postgres とアプリのスタック
services:
db:
image: postgres:17.5
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
restart: unless-stopped
api:
image: ghcr.io/example/api:1.4.0
environment:
DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
volumes:
pgdata:起動して、状態の変化を確認します。
docker compose up -d
docker compose psSTATUS 列には、角括弧内にヘルス状態が表示されます。正常なペアでは、両方の行に Up 41 seconds (healthy) と表示されます。データベースの初期化中は、db が Up 4 seconds (health: starting) と表示され、api は一覧にありません。Compose がまだ作成していないためです。
チェックが成功または失敗した理由を確認するには、ヘルスログを読み取ります。
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"Docker は直近の結果をいくつか保持します。各結果には開始時刻、終了時刻、ExitCode、およびコマンドの Output が含まれます。保存される出力は切り詰められます。そのため、大きなページ本文を出力するチェックでは、役に立たないログエントリになります。チェックでは出力を抑えてください。
コンテナが unhealthy になったときに Docker が行うこと
何も行いません。これが、最も意外に思われる点です。
単一ホスト上の Docker Engine は、unhealthy のコンテナを再起動しません。restart: unless-stopped ポリシーが反応するのはメインプロセスが終了した場合であり、unhealthy のコンテナは終了していないためです。Compose がそのまま放置している間、コンテナは unhealthy の状態で1週間とどまることがあります。Swarm mode は unhealthy のタスクを置き換えますが、1台のサーバー上で動作する通常の Compose スタックは置き換えません。
そのため、現実的な選択肢は2つです。プロセスが自身の異常を検知したら終了するようにし、再起動ポリシーが処理できる状態にします。または、外部から状態を監視してアラートを送ります。Uptime Kuma の監視を healthcheck と同じエンドポイントに設定すると、依存関係の障害が両方に現れます。そのため、ユーザーからではなく監視システムから障害を知ることができます。アプリへのトラフィックが Traefik reverse proxy 経由で到達する場合は、バックエンドに対するプロキシ自身の認識と Docker の health 状態は別であることに注意してください。一方だけでは、もう一方を監視できません。
常に healthy にならないチェックのデバッグ
同じコンテナ内でコマンドを正確に実行し、終了コードを確認します。
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"コンテナがまだ unhealthy と報告している状態で exit=0 となる場合は、compose test が直前に入力した内容と異なります。通常は、シェル構文が必要な箇所で CMD が使用されています。
残りの大半は、2つの誤りが原因です。1つ目はポートの誤りです。healthcheck はコンテナ内で実行されるため、公開されたホスト側のポートではなく、必ずコンテナ側のポートを使用します。ports: - "8080:3000" ではアプリケーションが 3000 で待ち受けるため、http://localhost:8080 に対するチェックは、ブラウザーではサイトが正常に動作していても、永続的に失敗します。2つ目はホストの誤りです。チェック内では、localhost は同じコンテナを指します。自身を確認する場合は正しい値ですが、隣接するコンテナを確認する場合は誤りです。その場合は、たとえば db のようにサービス名を使用します。
最後に、名前を付けておくべきケースが1つあります。healthcheck は成功しているのに、ユーザーにはエラーが表示される場合です。これは、エンドポイントが実際の処理を行わず、固定の 200 を返す場合に発生します。データベースに一度もクエリを実行しない readiness エンドポイントでは、データベースが停止したことを検出できません。低コストで実行できる実際のクエリを1つ実行するようにします。
FAQ
depends_on でデータベースが healthy と示されているのに、アプリケーションの接続が失敗し続けるのはなぜですか?
condition: service_healthy は、スタックの起動時に1回だけ評価されるためです。その後の状態は監視しません。後でデータベースコンテナが再起動しても、Compose は条件を再び満たすためにアプリケーションを再起動しません。そのため、アプリケーションコードに独自の再接続とリトライのロジックが必要です。docker start または docker compose up --no-deps で単一のコンテナを起動した場合も、この条件は機能しません。
イメージにすでに healthcheck が定義されている場合、独自の healthcheck は必要ですか?
通常は必要ありません。上書きすると、かえって後退することがよくあります。そのソフトウェアで readiness が何を意味するかは、イメージのメンテナーが把握しているためです。イメージのチェックが環境に合わない場合にだけ、独自のチェックを追加してください。たとえば、移動したポートを検査している場合です。イメージの healthcheck を無効にするには、サービスで test: ["NONE"] または disable: true を設定します。
healthcheck では curl と wget のどちらを使用すべきですか?
イメージにすでに存在する方を使用し、依存する前に docker compose exec <service> curl --version で確認してください。Debian ベースのイメージの多くには、どちらもありません。Alpine ベースのイメージには BusyBox wget があります。ソフトウェアに pg_isready や redis-cli などの独自クライアントが付属している場合は、healthcheck のためだけにイメージへパッケージを追加しないでください。
unhealthy のコンテナは自動的に再起動されますか?
単一ホスト上の Docker Engine では、再起動されません。再起動ポリシーが反応するのはプロセスの終了であり、health 状態ではありません。そのため、unhealthy のコンテナは起動したまま、障害が解消されないままになります。障害を検出したらプロセスを終了するか、状態を監視してアラートを送る外部モニターを実行してください。
start_period はどの程度に設定すべきですか?
測定した、正当な初回起動時間のうち最も長い時間に、余裕を加えた長さにしてください。空のボリュームに対して docker compose up で計測してください。データベースの初回起動は、その後の起動より大幅に遅いためです。start_period が長すぎると、最初の unhealthy 判定が遅れるだけです。retries が多すぎると、コンテナの稼働期間全体でチェックが弱くなります。こちらの方が深刻な障害です。