Claude Codeでdocker composeの起動競合をhealthcheckで直す
DBより先にアプリが立って落ちる起動競合を、healthcheckとdepends_onのservice_healthyで直す手順です。失敗ログをClaude Codeに渡して切り分ける進め方も載せます。
docker compose up でアプリが「データベースに接続できません」と落ちるのに、少し待って再実行すると通る。この症状の多くは、DBが接続を受け付けられる状態になる前にアプリが起動している起動競合です。直し方は healthcheck で「準備完了」を定義し、depends_on の condition: service_healthy でアプリの起動を待たせることです。
Claude Codeには、composeファイルの書き換えと、失敗ログからの原因の切り分けを任せられます。ただし何をもって「準備完了」とするかは、サービスごとに違います。ここでは公式の仕様を押さえたうえで、ログの渡し方と、任せる範囲の決め方まで書きます。
起動競合はなぜ起きるのか
Composeは depends_on を見て、コンテナを依存の順に作ります。順序は守られますが、待つのは「コンテナがrunningになるまで」です。Docker公式の説明では、Composeはコンテナが「ready」になるまでは待たないとされています。リレーショナルデータベースのように、起動後に自前の初期化を済ませてから接続を受け付けるサービスでは、この差が競合になります。
short syntaxの depends_on: [db] も同じです。公式は、依存サービスが「started」であることは保証するが、「healthy」になるまでは待たないと書いています。つまり次の2つは別物です。
- コンテナが起動した(running)
- サービスが接続を受け付けられる(healthy)
後者をComposeに伝える手段が healthcheck です。
healthcheckとdepends_onの組み方
公式のスタートアップ順序のページに載っている例が、組み方の基本形です。PostgreSQLの例を、役割ごとに分けて見ます。
services:
web:
build: .
depends_on:
db:
condition: service_healthy
restart: true
redis:
condition: service_started
redis:
image: redis
db:
image: postgres:18
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
retries: 5
start_period: 30s
timeout: 10sdb の healthcheck が「準備完了」の定義で、web の depends_on がそれを待つ側です。redis は service_started のままです。待たせる条件はサービスごとに選べるので、全部を service_healthy にする必要はありません。
condition に書ける値は3つあります。
| condition | 待つ内容 | 向く場面 |
|---|---|---|
service_started | 待つ内容コンテナの起動(short syntaxと同じ) | 向く場面起動直後から使えるサービス |
service_healthy | 待つ内容healthcheck が通ること | 向く場面DBなど、初期化に時間がかかるサービス |
service_completed_successfully | 待つ内容依存側が正常終了すること | 向く場面マイグレーションなど、一度走って終わるジョブ |
restart: true は、db が docker compose restart のようなComposeの操作で更新・再起動されたとき、web も自動で再起動させる指定です。公式は、ランタイムによる自動再起動(コンテナが落ちて立ち上がり直す場合)は対象外と明記しています。このキーはDocker Compose 2.17.0で入りました。
healthcheckの各値は何を決めるのか
healthcheck の既定値と挙動はDockerfileの HEALTHCHECK 命令と同じで、composeファイル側から上書きできます。
| キー | 役割 |
|---|---|
test | 役割健全性を調べるコマンド。リストの先頭は NONE / CMD / CMD-SHELL |
interval | 役割チェックの間隔(Dockerfileの既定は30秒) |
timeout | 役割1回のチェックの制限時間(同じく30秒)。超えると失敗扱い |
retries | 役割連続で何回失敗したら unhealthy にするか(同じく3回) |
start_period | 役割起動に時間がかかるコンテナの猶予(同じく0秒) |
start_interval | 役割猶予期間中のチェック間隔(同じく5秒) |
start_period の間の失敗は、retries に数えられません。ただし猶予中でもチェックが一度成功すると、そこからの連続失敗が数えられます。重いDBでは、start_period を長めに取ると猶予のうちに unhealthy と判定されるのを避けられます。
start_interval はDocker Engine 25.0以降が前提です。期間を 10s のように書く形式は、Compose 2.20.2から使えます。手元のDockerが古いときは、この2つが原因でファイルが読めない可能性があります。
test を文字列で書くと CMD-SHELL と同じ扱いになります。CMD-SHELL はコンテナの既定のシェル(Linuxでは /bin/sh)で実行されるため、パイプや || も使えます。公式の例が $${POSTGRES_USER} と $ を2つ重ねているのは、変数をコンテナ内のシェル側で展開させる書き方です。
healthcheckの状態と終了コードの意味
healthcheck を持つコンテナには、通常の状態とは別にヘルス状態が付きます。最初は starting で、チェックが通るたびに healthy になります。連続で retries 回失敗すると unhealthy です。
test のコマンドは、終了コードで結果を返します。
| 終了コード | 意味 |
|---|---|
| 0 | 意味成功。コンテナは健全で、使える状態 |
| 1 | 意味失敗。コンテナが正しく動いていない |
| 2 | 意味予約済み。使わない |
自作のチェックスクリプトを書くなら、終了コードは0か1に揃えます。また、チェックが標準出力や標準エラーに書いた文字列は、ヘルス状態に保存されて docker inspect で読めます。保存されるのは先頭4096バイトまでなので、出力は短く保ちます。失敗の理由を1行で出すスクリプトにしておくと、後からClaude Codeに docker inspect の結果を読ませるときにも手がかりになります。
状態が変わるたびに health_status イベントも発行されます。起動の待ち合わせを外部ツールで見たい場合は、このイベントが使えます。
イメージ側のHEALTHCHECKと上書きの関係
イメージのDockerfileに HEALTHCHECK が書かれていれば、composeの healthcheck を書かなくても、その定義が使われます。compose側に書いた値は、その定義を上書きします。
反対に、イメージのチェックを止めたいときは test: ["NONE"] か disable: true を使います。公式は、NONE はイメージ側の HEALTHCHECK を無効にする用途が中心だと説明しています。
自分のアプリのイメージに HEALTHCHECK を足すなら、その命令はDockerfileに1つしか有効になりません。複数書くと最後の1つだけが効きます。チェックの置き場所をDockerfileとcomposeのどちらにするかは、環境ごとに値を変えたいかで決めます。開発用とCI用で interval を変えたいなら、compose側に置くほうが差し替えやすくなります。
失敗ログをClaude Codeに渡して切り分ける
起動競合は、ログに出ているエラーの文面から原因を絞れます。Claude Codeの公式ワークフローも、エラーを共有する際は再現コマンド・再現手順・毎回起きるか断続的かを伝えるよう勧めています。起動競合は断続的に起きやすいので、この3点がそのまま効きます。
まず、失敗時のログをファイルに残します。
docker compose up -d
docker compose ps -a
docker compose logs --no-color > compose.log 2>&1compose.log とcomposeファイルをClaude Codeに見せて、次のように頼みます。
docker compose up で web が起動直後に落ちます。
compose.log と docker-compose.yml を読み、原因を切り分けてください。
- 再現コマンド: docker compose up -d
- 頻度: 5回に2回ほど。db を先に起動しておくと通る
- まだファイルは変更せず、原因の候補を根拠の行付きで挙げてください「まだ変更せず」と入れるのは、候補の確認と修正を分けるためです。Claude Codeには、実行前に計画を立てさせる運用もあります。原因が複数あり得る起動競合では、いきなり直させるより、根拠の行を並べさせたほうが後で見直せます。
症状から原因を絞る
ログの様子で、見る場所が変わります。次の表は、起動順の問題とそうでない問題を分けるための目安です。
| ログの様子 | 疑う点 | 見る場所 |
|---|---|---|
web の接続エラーのあと、db の「ready」が出る | 疑う点起動順(競合) | 見る場所depends_on の condition |
db が unhealthy のまま進まない | 疑う点healthcheck自体の失敗 | 見る場所test のコマンド、start_period |
db が再起動を繰り返す | 疑う点DB側の起動失敗 | 見る場所docker compose logs db |
service_healthy にしたのに競合が残る | 疑う点別の依存(キャッシュ、マイグレーション)が漏れている | 見る場所redis など他サービスの condition |
2行目の unhealthy は、競合の対処が裏目に出た形です。test が通らないと、web はいつまでも作られません。その場合は、test のコマンドをコンテナ内で手で実行し、終了コードを見るのが近道です。Claude Codeに「このコマンドを docker compose exec db で実行して結果を読んで」と頼めば、検証と読み取りを任せられます。
直したあとに設定を検証する
修正が入ったら、起動する前にcomposeファイルの書式を確かめます。
docker compose config
docker compose up -d
docker compose psdocker compose config は、変数展開後の設定を出力します。$${...} のエスケープが意図どおりに働いているかも、ここで見られます。起動後は docker compose ps で各サービスの状態を確認します。Claude Codeにはこの3コマンドを順に実行させ、ps の出力を読ませて、web が作られたか、db が healthy かを答えさせます。
1回通っただけで成功と決めないことが大切です。競合は断続的に起きるので、down してから up し直す起動を数回繰り返し、毎回通るかを見ます。その繰り返しも、1回目の結果を貼って「同じ条件でもう一度」と頼めば、Claude Codeに任せられます。ただし down はボリュームを残す形(-v なし)で使います。
Claude Codeに任せる範囲の決め方
composeの操作は、止める・消すを含みます。読み取りと再起動は任せ、データを消す操作は手元で承認する、と分けておくと安全です。Claude Codeの権限ルールは Bash(コマンド *) の形で書けます。ワイルドカードはサブコマンドの後ろに置くのが公式の書き方です。
{
"permissions": {
"allow": [
"Bash(docker compose ps *)",
"Bash(docker compose logs *)",
"Bash(docker compose config *)"
],
"deny": [
"Bash(docker compose down -v *)",
"Bash(docker compose down --volumes *)"
]
}
}down -v はボリュームも消すので、DBの中身が失われます。この書き方に一致するコマンドは、Claude Codeが試みても実行されません。-f x.yml を挟んだ書き方や docker volume rm は別のルールになるので、データを消す操作はdenyに並べて書くか、down 自体を許可リストに入れず承認制にします。docker compose up を許可に入れるかは、環境の重さ次第です。
CLAUDE.mdには、チームの約束を短く書いておきます。
## docker compose の起動順
- DB・キャッシュなど接続待ちが要るサービスには healthcheck を付ける
- 依存側は depends_on の condition: service_healthy で待たせる
- 起動競合を疑うときは、まず docker compose logs を読み、変更はその後この3行を置いておくと、修正の方針が揃います。固定の待ち時間の sleep は、環境が遅いと間に合わなくなるため、書かない方針にしておきます。
組んだあとに残りやすい落とし穴
落とし穴
healthcheck が通らず web がいつまでも作られない
test に書いたコマンドがイメージ内に無いと、チェックは成功しません。pg_isready はPostgreSQLのイメージに入っていますが、他のイメージでは同じコマンドが無いこともあります。curl -f を使う例では、curl が入っているかを先に確かめます。
もう1つは、短い retries と interval の組み合わせです。公式の例は interval: 10s、retries: 5、start_period: 30s です。初期化に数分かかるDBでは、この値のままだと unhealthy と判定される可能性があります。DBの実際の起動時間をログで測り、start_period を決めます。
依存が1つ欠けるだけの競合もあります。マイグレーションを別サービスに分けているなら、web は service_completed_successfully でそのジョブを待たせます。required: false を付けると、依存サービスが起動していない場合に警告だけで進みます(Compose 2.20.0以降)。必須の依存には付けません。
Dockerfile側の HEALTHCHECK や、ビルドの最適化はClaude CodeでDockerfileを最適化する手順が扱っています。Claude Code自体をコンテナで動かすcompose構成はClaude Code Docker実行ガイドにあります。コンテナを立てるテストなら、Testcontainersの手順で待機条件をテスト側に持たせる方法もあります。
まとめ
起動競合は、depends_on が「起動」までしか待たないことから生まれます。DBには healthcheck を付け、依存側で service_healthy を指定すれば、待つ対象が「接続を受け付ける状態」に変わります。
Claude Codeには、失敗ログと再現条件を渡し、原因の候補を根拠付きで挙げさせるところから始めます。修正の前に unhealthy か競合かを分けておけば、直す場所を間違えません。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。