再起動後だけ落ちるComposeサービスの話
普段は問題なく動いているDocker Composeのサービスが、ホストの再起動や更新の直後にだけDB接続エラーで止まる。ログを見ると Connection refused や Name or service not known が一度出て、その後は手で起動し直すと何事もなかったように動く。こうした「再起動後だけ落ちる」構成を抱えている運用者の多くは、depends_on を書けば依存サービスの準備まで待ってもらえる、と理解しています。
そこで使い捨てのVM内で、PostgreSQLと簡単なwebコンテナを用意し、depends_on・healthcheck・service_healthy・アプリ側リトライをそれぞれ実際に壊して観測しました。便利な書式を並べるのではなく、「何が起動順だけを保証し、何が準備完了を待ち、どこからはアプリの責任になるのか」を、docker compose logs と docker inspect の実出力で切り分けます。
検証環境
- 実行場所: Dockerを入れた使い捨てVM(検証後に破棄)
- Docker: server 29.6.0
- Docker Compose: v5.2.0
- 使用イメージ: postgres:16(DB役・client役の両方に同一イメージを流用)
DB役とweb役の両方に同じ postgres:16 を使っているのは、psql クライアントを別途用意せずに接続テストを実行できるためです。本番のwebアプリではアプリ本体のイメージに置き換えてください。なお、Composeの依存制御はDockerが提供する実機能であり、ここでは事実として記録します。
depends_onだけでは「接続可能」を待たない
まずDocker本体とComposeの版を確認し、作業ディレクトリを作ります。
実行コマンド
$ docker version --format 'server={{.Server.Version}}'
$ docker compose version
$ mkdir -p ~/compose-lab && cd ~/compose-lab
結果
server=29.6.0
Docker Compose version v5.2.0
次に、condition を指定しない depends_on だけの構成を置きます。
compose.yml
services:
db:
image: postgres:16
environment:
設定項目: app
設定項目: example_password
設定項目: app
web:
image: postgres:16
depends_on:
- db
environment:
設定項目: example_password
command: sh -c "psql -h db -U app -d app -c 'select 1' && echo 設定項目 || echo 設定項目"
実行コマンド
$ docker compose up -d
$ sleep 3
$ docker compose logs web
$ docker compose down -v
結果
web-1 | psql: error: connection to server at "db" (...), port 5432 failed: Connection refused
web-1 | Is the server running on that host and accepting TCP/IP connections?
web-1 | 設定項目
dbコンテナは起動しています。それでもwebは Connection refused で 設定項目 になりました。depends_on(condition なし)が保証するのは「dbの起動を先に始める」ことだけで、「PostgreSQLが接続を受け付けられる状態になるまで待つ」ことではありません。これが、再起動直後だけ失敗する構成の典型的な原因です。
healthcheckを誤設定するとunhealthyになる
準備完了を待たせるには、まずdb側に「準備できたか」を判定するhealthcheckが要ります。ここではあえて存在しないコマンドを指定して、誤設定がどう見えるかを観測します。
compose.yml(db部分)
services:
db:
image: postgres:16
environment:
設定項目: app
設定項目: example_password
設定項目: app
healthcheck:
test: ["CMD", "this-check-does-not-exist"]
interval: 2s
timeout: 2s
retries: 3
実行コマンド
$ docker compose up -d
$ sleep 12
$ CID=$(docker compose ps -q db)
$ docker inspect --format '{{json .State.Health}}' "$CID"
$ docker compose down -v
結果(抜粋)
{"Status":"unhealthy","FailingStreak":5,"Log":[
{"ExitCode":-1,"Output":"OCI runtime exec failed: exec failed: unable to start container process: exec: \"this-check-does-not-exist\": executable file not found in $設定項目"}
]}
unhealthy のときに原因を見るコマンドは docker inspect --format '{{json .State.Health}}' です。Status が unhealthy、Log[].Output に executable file not found in $設定項目 と出ており、これは「DBが壊れている」のではなく「healthcheckのコマンド自体が存在しない」誤設定だと分かります。healthcheckを足したのに状況が悪化したように見える場合、まずこのLogを確認してください。
service_healthyで準備完了まで待たせる
healthcheckを正しい判定コマンドに直し、webの depends_on に condition: service_healthy を付けます。PostgreSQLには接続受付の判定に使える pg_isready があります。
compose.yml
services:
db:
image: postgres:16
environment:
設定項目: app
設定項目: example_password
設定項目: app
healthcheck:
test: ["CMD-設定項目", "pg_isready -U app -d app"]
interval: 2s
timeout: 2s
retries: 10
web:
image: postgres:16
depends_on:
db:
condition: service_healthy
environment:
設定項目: example_password
command: sh -c "psql -h db -U app -d app -c 'select 1' && echo 設定項目 || echo 設定項目"
実行コマンド
$ docker compose up -d
$ sleep 20
$ docker compose logs web
$ docker compose down -v
結果
web-1 | ?column?
web-1 | ----------
web-1 | 1
web-1 | (1 row)
web-1 |
web-1 | 設定項目
healthy
今度はwebがdbのhealthyを待ってから起動し、select 1 が通って 設定項目 になりました。depends_on(condition なし)との差は、待っている対象が「起動開始」から「healthcheckのhealthy」に変わった点です。初回起動の競合は、これで吸収できます。
再起動の瞬断ではアプリ側リトライが効く
ここで「では再起動後も service_healthy だけで大丈夫か」を確かめます。一度起動して安定させたあと、docker compose restart を実行し、リトライなしのwebとリトライありのwebを比較しました。
リトライなし(NO-設定項目)
--- NO-設定項目 web logs after restart ---
web-1 | (起動直後はselect 1が成功し 設定項目)
web-1 | psql: error: could not translate host name "db" to address: Name or service not known
web-1 | 設定項目
リトライあり(設定項目-設定項目)
--- 設定項目-設定項目 web logs after restart ---
web-1 | (起動直後はselect 1が成功し 設定項目)
web-1 | psql: error: could not translate host name "db" to address: Name or service not known
web-1 | 設定項目...
web-1 | (再試行後 select 1 が成功し 設定項目)
再起動直後は、service_healthy を指定していても一度 Name or service not known(名前解決の瞬断)が起きました。リトライなしの構成はそこで 設定項目 のまま落ち、リトライありの構成は数回の再試行で接続を取り戻して 設定項目 に回復しています。
境界はここです。service_healthy は「初回起動でdbがhealthyになるまで待つ」ことは保証しますが、稼働中の再起動でネットワークやDNSが一瞬切れる場面までは肩代わりしません。その瞬断を吸収するのは、アプリ側(またはエントリポイント)のリトライです。再起動後だけ落ちる構成を本当に安定させたいなら、service_healthy とアプリ側リトライの両方が要ります。
誤設定からの切り戻しは設定差分で完結する
運用で怖いのは、healthcheckを直したつもりが余計に壊すことです。誤設定版(compose-broken.yml)と正常版(compose-fixed.yml)を用意し、状態と差分を確認しました。
実行コマンド
$ docker compose -f compose-broken.yml up -d
$ sleep 10
$ docker inspect --format '{{.State.Health.Status}}' "$(docker compose -f compose-broken.yml ps -q db)"
$ docker compose -f compose-broken.yml down -v
$ docker compose -f compose-fixed.yml up -d
$ sleep 10
$ docker inspect --format '{{.State.Health.Status}}' "$(docker compose -f compose-fixed.yml ps -q db)"
$ diff compose-broken.yml compose-fixed.yml
$ docker compose -f compose-fixed.yml down -v
結果
--- broken status ---
unhealthy
--- fixed status ---
healthy
--- healthcheck diff ---
9c9
< test: ["CMD", "this-check-does-not-exist"]
---
> test: ["CMD-設定項目", "pg_isready -U app -d app"]
12c12
< retries: 3
---
> retries: 10
壊れた状態と直った状態の差は、healthcheckの test 行と retries 行の2か所だけでした。切り戻しは別ファイルへ -f で切り替えるか、この2行を戻すだけで完結します。healthcheckの変更は影響範囲が小さく、設定差分として追える点は実務上の安心材料です。
比較:この読者が選べる3つの構成
| 観点 | depends_onのみ | healthcheckなし+アプリ側リトライ | healthcheck+service_healthy(+リトライ) |
|---|---|---|---|
| 起動直後の失敗ログの分かりやすさ | 起動順は守られるが接続失敗が必ず出る | 失敗が再試行に紛れて見えにくい | healthy待ちで初回失敗が減り、起きてもinspectで原因を追える |
| 再起動時の安定性 | 低い(接続可能を待たない) | 中(瞬断は吸収できるが準備完了は待たない) | 高い(初回はservice_healthy、瞬断はリトライ) |
| 設定ミス時の切り戻しやすさ | 設定が少なく戻しやすい | アプリ側の挙動に依存し追いにくい | healthcheck2行差分で戻せる |
| Composeファイルの複雑さ | 最小 | 小(Compose側は単純、責任はアプリ) | 中(healthcheckとconditionが増える) |
判断の目安
- 初回起動の競合を消したいだけなら、
healthcheck+condition: service_healthyを入れる価値があります。depends_on単独では接続可能を待てません。 - ホスト再起動や更新後の瞬断まで安定させたいなら、
service_healthyに加えてアプリ側(エントリポイント)のリトライまで必須として扱ってください。どちらか片方では再現する失敗があります。 - 常用のComposeに反映する前に、最低でも「depends_onのみで
Connection refused」「healthcheck誤設定でunhealthy」「restart直後の名前解決瞬断」の3つを使い捨て環境で一度観測しておくと、本番でログを見たときに原因の切り分けが速くなります。
失敗・制約・再現条件
- 設定項目-RETRYのサンプルスクリプトのログに
sh: 1: [: -ge: unexpected operatorが1行混じりました。これは検証用シェルスクリプトの記法差(算術比較の書き方)によるもので、再試行による回復という本筋には影響しません。実アプリではリトライ処理を各言語のクライアント側で実装してください。 - 切り戻し手順のステップで最終的に exit code 1 を記録しましたが、これは初回SSH接続時のホスト鍵警告と teardown(ネットワーク削除)中のログによるもので、healthcheckの broken→fixed 判定自体は
unhealthy→healthyと期待どおりに観測できています。実害のある失敗ではありません。 - 後片付け後に
docker ps -aとdocker volume lsを確認し、残コンテナ・残ボリュームがないことを確認しました。 - 再現方法: 同じ手順を、Docker入りの使い捨てVMで再実行すれば同様の切り分けが得られます。本記事のパスワード値やネットワークアドレスは検証用のもので、公開向けにプレースホルダへ置き換えています。
まとめ
depends_on は起動順、healthcheck+service_healthy は初回の準備完了待ち、アプリ側リトライは稼働中の瞬断吸収。再起動後だけ落ちる構成は、この3層のどこに穴があるかをログで切り分けると原因が見えます。便利な書式を信じる前に、小さく壊して docker compose logs と docker inspect の出力を一度自分の目で見ておくことを勧めます。
この検証を回している環境
この検証は、自宅の常設ラボ(使い捨てVM/LXCを回す母艦+GPU+VLAN分離ネットワーク)で動かしています。使っている機材と選定理由、全体構成は1本にまとめています。
ラボ構成のまとめを見る →