本ページはプロモーションが含まれています

開発ツール

Docker Compose healthcheckの書き方と設定例

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

depends_on を書いたのに、アプリが「DBに接続できません」で落ちる。

原因は、depends_on がDBコンテナの起動しか待たないことです。

接続できる状態まで待たせるには、healthcheck と condition: service_healthy を組み合わせます。

ただ、よく見る書き方のままだと待ち時間が30秒に延びたり、初期化の途中で「準備完了」と誤判定されたりします。

2026年10月3日にDocker Engine 26.1.5 / Compose v2.32.1で計測した結果をもとに、正しい書き方をまとめました。

この記事でわかること

  • healthcheck の書き方と5つのパラメータの既定値
  • start_interval で起動待ちを31秒から2秒以内に縮める方法(実測)
  • MySQL・PostgreSQLが初期化中に healthy になる落とし穴と対処
  • curlがないイメージでの書き方と、unhealthy の調べ方(docker compose ps・docker inspect)

Docker Compose healthcheckとは?depends_onだけでは足りない理由

Docker Composeの healthcheck とは、コンテナの中でコマンドを定期的に実行し、その結果でサービスが正常に動いているかを判定する仕組みです。

判定結果は starting・healthy・unhealthy の3つの状態で表されます。

depends_on と組み合わせると「依存先が healthy になってから起動する」という起動順の制御ができます。

逆に言えば、Composeファイルにもイメージ(Dockerfile の HEALTHCHECK)にも healthcheck がなければ、Composeはサービスの中身が使える状態かどうかを知る手段がありません。

「起動した」と「接続できる」は別物

depends_on の短い書き方は、依存先のコンテナが起動した時点で次のサービスを起動します。

ところがDBは、コンテナが起動してからも初期化に数秒〜数十秒かかります。

MySQLなら、データファイルの作成、初期化用の一時サーバーの起動、/docker-entrypoint-initdb.d のSQL実行と続き、最後に本番のサーバーが立ち上がります。

私が計測した MySQL 8.4 では、コンテナの起動から本番サーバーが接続を受け付けるまで32秒かかりました(初期化SQLに20秒かかる設定)。

アプリがこの32秒の間に接続しにいくと、当然エラーになります。

depends_on だけ

DBコンテナが起動した瞬間にアプリが起動する。初期化が終わっていないので接続エラーになりやすい

healthcheck + service_healthy

DBが healthy になるまでアプリの起動を待つ。判定コマンドが正しければ、接続エラーは起きない

healthcheckは終了コード0か1かで判定する

healthcheck の判定は、指定したコマンドの終了コードだけで決まります。

Docker公式リファレンスによると、意味は次の3つです。

終了コード意味
0成功(healthy)
1失敗(unhealthy に向けてカウント)
2予約済み(使わない)

コンテナの状態は最初 starting で、チェックが1回成功すると healthy になります。

失敗が retries 回続くと unhealthy です。

コマンドの標準出力・標準エラーは先頭4,096バイトまで保存され、あとで docker inspect から読めます。

Composeのhealthcheckで状態が移り変わる流れは次のとおりです。

flowchart LR A["starting"] -->|"チェック成功"| B["healthy"] A -->|"start_period中の失敗<br>(カウントしない)"| A A -->|"start_period後に<br>retries回連続で失敗"| C["unhealthy"] B -->|"retries回連続で失敗"| C C -->|"チェック成功"| B

healthcheckの書き方と5つのパラメータ

healthcheck は、サービス定義の下に test と4〜5個の時間・回数のパラメータを書くだけです。

迷うのは test の書き方と、各パラメータに何秒を入れるかの2点でしょう。

先に、この記事でおすすめする形を載せておきます。

既定値のままにすると起動待ちが無駄に長くなるので、start_period と start_interval は書いておいたほうが得です。

yaml
services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER:-postgres}"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
      start_interval: 1s

testの書き方|CMDとCMD-SHELLの違い

test は配列か文字列で書きます。

配列の1つ目の要素で、コマンドの実行方法が変わります。

書き方動き向いている場面
["CMD", "コマンド", "引数"]シェルを通さず直接実行環境変数やパイプを使わない単純なコマンド
["CMD-SHELL", "コマンド文字列"]/bin/sh -c で実行環境変数・パイプ・OR条件を使うとき
文字列で書くCMD-SHELL と同じ短く書きたいとき
["NONE"]healthcheck を無効化イメージ側の healthcheck を止めたいとき

CMD-SHELL の中で環境変数を使うときは、$ を $$ と2つ重ねます。

Composeはファイルを読み込む時点で ${...} を自分で展開してしまうため、$$ にしてコンテナ内のシェルまで $ を届ける必要があります。

$ 1つで書いて、ホスト側に同じ名前の環境変数がないと、空文字に置き換わってチェックが失敗し続けます。

interval・timeout・retriesの既定値と決め方

5つのパラメータには、Docker公式リファレンスで既定値が決まっています。

パラメータ意味既定値おすすめ
intervalチェックの間隔30s10s〜30s
timeout1回のチェックの制限時間30s3s〜5s
retriesunhealthy と判定する連続失敗回数33〜5
start_period起動直後の猶予期間0s30s〜60s(DB)
start_interval猶予期間中のチェック間隔5s1s〜2s

timeout の既定値30秒は長すぎます。

DBへのpingは通常1秒もかからないので、5秒もあれば足ります。

timeout を超えたチェックは SIGKILL で止められ、失敗として数えられます。

interval は「起動後に壊れたことを何秒で検知したいか」で決めます。

開発環境なら30秒のままでも困りません。

start_intervalで起動待ちを31秒→2秒に短縮する

既定値のままだと、最初のチェックはコンテナ起動の30秒後まで実行されません。

公式リファレンスに「最初のチェックは起動から interval 秒後に実行される」と書かれているとおりで、DBの準備が1秒で終わっても、healthy になるのは30秒後です。

PostgreSQL 17で実際に測った結果が次の表です。

設定healthy までの時間
interval だけ(既定値30s)31秒
start_period: 30s + start_interval: 1s1〜2秒

start_period の間は、チェックが interval ではなく start_interval の間隔で実行されます。

猶予期間中の失敗は retries に数えられないので、1秒間隔で何回失敗しても unhealthy にはなりません。

start_interval は Docker Engine 25.0 以降、Compose 2.20.2 以降で使えます。

古い環境で書くとエラーになるので、docker version で確認してから使ってください。

DockerfileのHEALTHCHECKとの関係|Composeは項目ごとに上書きする

イメージの Dockerfile に HEALTHCHECK が書かれている場合、Compose の healthcheck はそれを丸ごと置き換えるのではなく、書いた項目だけを上書きします。

書かなかった項目は、Dockerfile 側の値がそのまま残ります。

HEALTHCHECK --interval=7s --timeout=3s を書いたイメージで、Compose 側の書き方を変えて確かめた結果が次の表です(Docker Engine 26.1.5 / Compose v2.32.1)。

Composeの書き方実際に使われた設定
healthcheck を書かないDockerfile の設定がそのまま使われる
test と interval を書くtest・interval は Compose の値、timeout は Dockerfile の3秒が残る
interval だけ書くtest は Dockerfile のコマンドのまま、interval だけ変わる
disable: truetest が ["NONE"] になり、チェックされない

つまり、イメージ付属のチェックコマンドはそのまま使い、間隔だけ縮めたいときは次のように書けます。

yaml
services:
  web:
    build: .   # DockerfileにHEALTHCHECKがあるイメージ
    healthcheck:
      interval: 2s   # testを書かなければDockerfileのコマンドが使われる

どの設定が効いているか分からなくなったら、コンテナの設定を直接見ると確実です。

bash
docker inspect --format '{{json .Config.Healthcheck}}' "$(docker compose ps -q web)"

timeout を書いていないのに短い値になっている、といった食い違いは、たいてい Dockerfile 側の設定が残っているのが原因です。

depends_onとservice_healthyで起動順を制御する

healthcheck を書いただけでは、起動順は変わりません。

依存する側のサービスに、depends_on の長い書き方で condition: service_healthy を書いて初めて「healthy になるまで待つ」動きになります。

DBとアプリの2サービス構成なら、次の形が基本です。

yaml
services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER:-postgres}"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
      start_interval: 1s
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
        restart: true

conditionの3種類と使い分け

condition には3つの値があります。

condition待つ条件使う場面
service_started依存先のコンテナが起動した短い書き方と同じ。起動順だけ決めたいとき
service_healthy依存先が healthy になったDB・キャッシュ・APIなど、準備完了を待ちたいとき
service_completed_successfully依存先が終了コード0で終わったマイグレーションなど1回きりの処理の完了を待つとき

DB → マイグレーション → アプリの順に起動したい場合を考えます。

マイグレーション用サービスには service_healthy、アプリには service_completed_successfully を書けば、この順に並びます。

依存先が unhealthy のまま終わると、Composeは依存する側を起動しません。

画面には dependency failed to start: container ○○ is unhealthy と表示されます。

restart: trueとdocker compose up --wait

restart: true を付けると、Composeの操作で依存先を再起動・更新したときに、依存する側も自動で再起動されます。

DBを作り直したあと、アプリが古い接続を握ったままになる事故を防げます。

ただし対象は docker compose restart などの操作で起きた再起動だけで、コンテナが勝手に落ちて再起動した場合は含まれません。

docker compose up --wait は、全サービスが running か healthy になるまでコマンドを返さないオプションです。

CIで「起動しきってからテストを流す」用途に向いています。

待ち時間の上限は --wait-timeout で秒数を指定できます。

DB・サービス別のhealthcheck設定例

DB向けの healthcheck は、各DBのイメージに最初から入っている確認用コマンドを使うのが定番です。

curlを入れる必要がなく、DBのプロトコルで本当に応答するかを確かめられます。

ただし、ホストを指定しない書き方だと初期化中に healthy と誤判定されます。

ここは見落としやすい落とし穴なので、計測結果とあわせて説明します。

PostgreSQLはpg_isreadyで判定する

PostgreSQLは pg_isready で判定します。

ポイントは -h 127.0.0.1 を付けて、TCPで接続させることです。

yaml
healthcheck:
  test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER:-postgres}"]
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 30s
  start_interval: 1s

公式イメージは初回起動時、初期化用の一時サーバーをUNIXソケットだけで立ち上げます。

ホストを省略した pg_isready はソケットにつなぐので、一時サーバーに応答して healthy になってしまいます。

初期化SQLに20秒かかる設定で比べた結果は次のとおりです。

testhealthy になった時間判定
pg_isready -U postgres2秒(初期化の途中)× 早すぎる
pg_isready -h 127.0.0.1 -U postgres23秒(初期化完了後)◎ 正しい

Docker公式ドキュメントの例もホストを省略した形なので、初期化SQLを置いている人は特に注意が必要です。

MySQLはmysqladmin pingを127.0.0.1で使う

MySQLも同じ構造で、mysqladmin ping に -h localhost を付けるとソケット接続になります。

MySQLクライアントは localhost をソケット接続の意味で扱うため、TCPにしたいなら 127.0.0.1 と書きます。

yaml
healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD}"]
  interval: 30s
  timeout: 5s
  retries: 5
  start_period: 60s
  start_interval: 1s

MySQL 8.4で、初期化SQLに20秒かかる設定にして比べました。

testhealthy になった時間判定
mysqladmin ping -h localhost11秒(初期化SQLの実行中)× 早すぎる
mysqladmin ping -h 127.0.0.134秒(本番サーバー起動後)◎ 正しい

ログを見ると、本番サーバーが接続を受け付け始めたのは32秒時点でした。

localhost 版は、その21秒前に「準備完了」を出していたわけです。

初期化SQLが一瞬で終わる環境では差が見えないため、テーブル作成やデータ投入を足した日に突然エラーが出始める、という形で表面化します。

なお、コマンドラインにパスワードを書くと警告が出ます。

開発環境なら実害はありませんが、本番では MariaDB 公式イメージに入っている healthcheck.sh のような専用スクリプトを検討してください。

RedisとSpring Bootアプリの設定例

Redisは redis-cli ping が PONG を返すかで判定します。

grep PONG を挟むと、PONG 以外の応答が返ったときも確実に失敗として扱えます。

yaml
healthcheck:
  test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
  interval: 30s
  timeout: 3s
  retries: 3
  start_period: 10s
  start_interval: 1s

アプリ側にも healthcheck を書いておくと、docker compose up --wait やフロントのコンテナの起動待ちに使えます。

Spring Bootなら、spring-boot-starter-actuator を入れて /actuator/health を叩くのが手軽です。

DBに接続できないときは503が返るので、curl -f が失敗を返してくれます。

yaml
healthcheck:
  test: ["CMD", "curl", "-fsS", "http://localhost:8080/actuator/health"]
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 60s
  start_interval: 2s

eclipse-temurin:21-jre にはcurlが入っているので、ベースイメージがこれならそのまま書けます。

curlがないイメージでhealthcheckを書く方法

Webサーバーの healthcheck は curl -f http://localhost/ で書かれている例が多いものの、curlが入っていないイメージも珍しくありません。

2026年10月3日時点で、よく使うイメージにcurl・wget・ncが入っているかを確かめました。

イメージcurlwgetnc
postgres:17-alpine×○○
redis:7-alpine×○○
mysql:8.4○××
eclipse-temurin:21-jre○○×
alpine:3.20×○○

curlがないのに ["CMD", "curl", ...] と書くと、チェックのたびに「executable file not found」で失敗し、永遠に healthy になりません。

wgetやncで代用する

Alpine系のイメージなら、BusyBox の wget か nc で代用できます。

HTTPで応答を確かめたいなら wget、ポートが開いているかだけでよければ nc です。

yaml
# HTTPの応答を確かめる(wget)
healthcheck:
  test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]

# ポートが開いているかだけ確かめる(nc)
healthcheck:
  test: ["CMD", "nc", "-z", "localhost", "8080"]

nc はポートが開いた瞬間に成功するので、アプリの初期化が終わったかまでは分かりません。

アプリにヘルスチェック用のURLがあるなら、wget で叩くほうが確実です。

シェルもないイメージ(distroless)の対処

distroless のようにシェルもcurlもないイメージでは、CMD-SHELL 自体が動きません。

対処は2つあります。

  1. アプリ自身が持っているヘルスチェック用のサブコマンドを ["CMD", ...] で直接呼ぶ
  2. マルチステージビルドで、静的リンクの BusyBox だけをコピーして使う

2つ目は、次のように busybox:1.37-musl から1ファイルだけ持ってきます。

gcr.io/distroless/static-debian12 に入れて、wget が動くことを確認しました。

dockerfile
FROM busybox:1.37-musl AS bb
FROM gcr.io/distroless/static-debian12
COPY --from=bb /bin/busybox /busybox
yaml
healthcheck:
  test: ["CMD", "/busybox", "wget", "-q", "--spider", "http://localhost:8080/health"]

足されるのは約1.3MBのBusyBox 1ファイルだけです。

イメージを小さく保つために distroless を選んでいる場合でも、healthcheck が書けない不便さと比べれば許容しやすいサイズでしょう。

healthcheckがunhealthyになる原因と調べ方

healthcheck が通らないとき、Composeの画面には「unhealthy」としか出ません。

原因を突き止めるには、チェックの実行結果そのものを読みにいく必要があります。

調べ方を知っていれば、大半は数分で片づきます。

docker compose psで今の状態を確認する

まずは、各サービスがいまどの状態にあるかを確認します。

bash
docker compose ps --format 'table {{.Service}}\t{{.Status}}'

STATUS 列の末尾に、healthcheck の状態がカッコ書きで表示されます。

text
SERVICE   STATUS
app       Up 4 seconds (health: starting)
db        Up 12 seconds (healthy)
redis     Up 12 seconds (unhealthy)

(health: starting) は最初のチェックが通る前、(healthy) は正常、(unhealthy) は失敗が retries 回続いた状態です。

カッコ書きが出ないサービスは、healthcheck が設定されていないか、disable: true で無効になっています。

starting のまま進まない、unhealthy になった、という場合は次のヘルスログで中身を読みます。

docker inspectでヘルスログを読む

チェックの結果は、直近の数回分がコンテナに保存されています。

次のコマンドで、終了コードと出力を確認できます。

bash
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"

curlがないイメージで試すと、Output に次のメッセージが入っていました。

text
OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown

チェックのコマンドを、コンテナの中で手で実行してみるのも有効です。

docker compose exec db pg_isready -h 127.0.0.1 -U postgres; echo "exit=$?" のように終了コードまで表示すると、どこで失敗しているかがすぐ分かります。

よくある原因4つと対処

unhealthy になる原因は、だいたい次の4つに収まります。

症状原因対処
executable file not foundイメージにcurl等がないwget・nc・DB付属のコマンドに替える
認証エラーで失敗し続ける$ 1つで環境変数を書いて空になった$$ に重ねる
起動直後に unhealthy になるstart_period が短く、初期化中の失敗を数えたstart_period を延ばす
timeout で失敗する(ExitCode が -1、Output が Health check exceeded timeout)チェック自体が重い軽いコマンドに替えるか timeout を延ばす

逆に「早く healthy になりすぎて、アプリが接続エラーになる」場合は、前の章のソケット接続の問題を疑ってください。

unhealthyでもコンテナは再起動されない

意外と知られていませんが、unhealthy になってもDockerはコンテナを再起動しません。

restart: always を付けたコンテナで healthcheck を失敗させ続けても、再起動回数は0のままでした。

healthcheck はあくまで状態を判定する仕組みで、復旧までは面倒を見てくれません。

自動で再起動させたいなら、アプリ側で異常を検知したらプロセスを終了させ、restart ポリシーに再起動させる設計が素直です。

Docker Compose healthcheckのよくある質問

Docker Compose の healthcheck について、検索でよく調べられている疑問をまとめました。

起動順の制御、パラメータの決め方、curlがないときの対処が質問の中心です。

詳しい手順や計測結果は、各見出しの本文もあわせて確認してください。

healthcheckとdepends_onの違いは?

healthcheck はサービスが正常かを判定する仕組みで、depends_on は起動順を決める仕組みです。

depends_on に condition: service_healthy を書くと、healthcheck の判定結果を使って「healthy になるまで待つ」動きになります。

interval・timeoutは何秒にすればいい?

開発環境なら interval 30秒、timeout 5秒、retries 3回で足ります。

起動待ちを短くしたいなら、start_period を30〜60秒、start_interval を1〜2秒にしてください。

既定値の timeout 30秒は長すぎるので、短くしておくのがおすすめです。

unhealthyになったら自動で再起動される?

されません。

Docker Composeで動かすコンテナは、unhealthy になっても restart ポリシーによる再起動の対象になりません。

再起動させたい場合は、アプリ側で異常時にプロセスを終了させる設計にします。

curlが入っていないイメージではどうする?

Alpine系なら BusyBox の wget か nc で代用できます。

DBなら pg_isready や mysqladmin ping のように、イメージに付属するコマンドを使うのが確実です。

healthcheckを無効にするには?

disable: true を書くか、test を ["NONE"] にします。

ベースイメージの Dockerfile に HEALTHCHECK が書かれていて、それを止めたいときに使います。

condition: service_healthyが使えないと言われる

docker-compose(v1)の 1.27.0 より前で version: '3' 系のファイル形式を指定していると、condition の書き方がエラーになります。3系のファイル形式から condition が一度削除されていたためで、1.27.0 以降は使えるようになっています。

現在の Docker Compose v2 では、トップレベルの version は不要で、condition もそのまま使えます。

Docker Compose healthcheckの要点まとめ

healthcheck は数行書くだけの設定ですが、既定値とホスト指定の2か所で結果が大きく変わります。

最後に、書くときに押さえる点を整理します。

【要点まとめ】

  • 起動順を待たせるには depends_on の condition: service_healthy が必要
  • 既定値のままだと初回チェックは30秒後。start_interval で1〜2秒に縮む
  • DBのチェックは -h 127.0.0.1 でTCP接続にする(ソケットだと初期化中に healthy になる)
  • 環境変数は $$ で書く。curlがなければ wget・nc・DB付属コマンドで代用する
  • unhealthy になっても再起動はされない。原因は docker inspect のヘルスログで読む

今回の計測でいちばん意外だった結果は、PostgreSQL の healthcheck が初期化の途中で healthy を返したことです。

Docker公式ドキュメントの例と同じく、ホストを省略した書き方でした。

初期化SQLを置いていないうちは表に出ません。

手元の compose.yaml が pg_isready や mysqladmin ping をホストなしで呼んでいないか、一度見直してみてください。

Composeファイル全体の書き方は「Docker Compose入門|yml書き方と使い方」で解説しています。

コンテナ同士の通信やポートの考え方は「Dockerネットワークとポート設定」が参考になります。

REVISIONS / 改訂履歴
REV.DATE内容
22026-10-04記載内容の事実確認と修正
12026-10-03初版
  • この記事を書いた人
  • 最新記事

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

-開発ツール