
この現象は、Docker Composeが起動順序を間違えているというより、コンテナの起動とサービスの準備完了を同じものとして扱っていることから起きる。depends_onでデータベースを先に起動しても、その中のデータベースサーバーがすぐ接続を受け付けられるとは限らない。
[Docker Composeで depends](/articles/docker-composenoprofilesji-neng-kai-fa/)_on と healthcheck を組み合わせると、単にコンテナを先に作るだけでなく、依存先が一定の状態になるまでアプリケーションの起動を待たせられる。さらに、アプリケーション側のリトライ処理も加えれば、初回起動だけでなく、データベースの再起動や一時的なネットワーク障害にも対応しやすくなる。
depends_onだけでは不十分である理由
まず、最も単純なCompose設定を考える。dbを先に起動し、その後にappを起動する構成だ。
services.db.imageにPostgreSQLのイメージを指定し、services.app.depends_onにdbを指定すると、Composeはdbがappより先に起動を開始するように処理する。しかし、この設定だけでは、PostgreSQLが接続可能な状態になるまで待機することはない。
ここでいう「起動」は、コンテナのプロセスが実行状態になったという意味に近い。コンテナが実行中でも、内部では次のような処理が残っていることがある。
1. 初回起動時のデータディレクトリ作成
2. データベースやユーザーの初期化
3. 既存データがある場合のリカバリ
4. 設定ファイルの読み込み
5. ソケットやポートの開放
6. クライアントからの接続受付
このうち、コンテナの起動と、クライアントからの接続受付は同時に完了するとは限らない。特に名前付きボリュームを初めて使う場合や、データベースの初期化が発生する場合は、コンテナが先に実行状態になっても、内部の準備が続いていることがある。
depends_onの短縮記法は、あくまでサービス間の依存関係を示すものだ。次のような設定では、dbの起動開始後にappの起動が進むだけで、dbがSQLを実行できる状態かどうかまでは確認しない。
appのdepends_onにdbを指定するdbのコンテナ作成を先に行うdbのプロセスが動き始めたらappも起動する
そのため、アプリケーションが起動時にすぐデータベースへ接続する設計だと、タイミング次第で接続エラーになる。再起動すると成功するのは、最初の試行でデータベース側の初期化が進み、2回目には接続可能な状態になっているからだ。
depends_onが保証するのは、依存先のコンテナが先に動き始めることまで。サービスが仕事を始められる状態かどうかは、別に確認する必要がある。コンテナの状態とサービスの状態は違う
Dockerでは、コンテナがrunningであることと、アプリケーションが正常に利用できることは別の情報だ。
例えば、PostgreSQLのプロセスが起動していても、まだ初期化処理中ならSQL接続は失敗する可能性がある。Redisでも、プロセスが存在することと、PINGに応答できることは同じではない。Webアプリケーションなら、プロセスが起動していても、設定の読み込みや外部サービスとの接続が終わっていなければ、実際のリクエストを処理できない場合がある。
この差をComposeに伝える役割がhealthcheckだ。ヘルスチェックでサービス固有の確認処理を実行し、その結果をservice_healthyという条件から参照する。
condition: service_healthyによる起動制御
Composeでは、依存関係を長い記法で書くと、依存先がどの状態になるまで待つかを指定できる。基本形は、appのdepends_onでdbをマッピング形式にし、その中にcondition: service_healthyを書く構成だ。
この設定にすると、appはdbのコンテナ作成を待つだけでなく、dbに定義されたヘルスチェックが成功し、正常状態と判定されるまで起動を待機する。
利用できる条件は、役割によって次のように分かれる。
| 条件 | Composeが待つ状態 | 向いている用途 |
|---|---|---|
service_started | 依存先コンテナが起動した状態 | 単純な起動順序だけが必要な場合 |
service_healthy | 依存先のヘルスチェックが成功した状態 | DBやキャッシュなど、利用可能になるまで時間がかかるサービス |
service_completed_successfully | 依存先の処理が正常終了した状態 | マイグレーションや初期データ投入などの一度限りの処理 |
短縮記法で指定するdepends_on: - dbは、実質的に起動開始を基準にした依存関係だ。接続待ちを目的にするなら、長い記法でservice_healthyを明示する。
ただし、service_healthyは魔法の待機命令ではない。依存先に適切なhealthcheckがなければ、Composeは何をもって正常と判断すればよいのか分からない。サービスが実際に使えるかどうかを確認できるコマンドを、依存先コンテナ側に定義する必要がある。
service_healthyが適している場面
この条件は、次のような依存関係で使いやすい。
- Laravelなどのアプリケーションが、起動時にデータベースへ接続する
- ワーカーがRedisやメッセージブローカーへ接続してから処理を始める
- 管理画面やAPIが、依存するバックエンドの準備完了後に起動する
- 開発環境で、複数コンテナを毎回まとめて立ち上げる
反対に、常に再起動や障害復旧までComposeだけで制御できるわけではない。すでに起動したコンテナの依存先が後から落ちた場合、depends_onがアプリケーションを自動的に停止・再接続させる仕組みではないからだ。起動時の順序と、稼働中の耐障害性は分けて考える必要がある。
依存先コンテナにhealthcheckを定義する
healthcheckは、一定の間隔でコンテナ内のコマンドを実行し、その終了コードによって状態を判定する。コマンドが成功すれば正常、失敗が続けば異常という形だ。
設定する主な項目は次の5つである。
test:実行する確認コマンドinterval:確認を繰り返す間隔timeout:1回の確認を待つ上限時間retries:失敗を何回続けると異常と判定するかstart_period:起動直後に失敗を大きく扱わない猶予期間
PostgreSQLの確認
PostgreSQLではpg_isreadyを使う構成が一般的だ。例えば、dbサービスのhealthcheck.testを["CMD-SHELL", "pg_isready -U postgres"]とし、intervalを5秒、timeoutを3秒、retriesを5回、start_periodを10秒程度に設定する。
アプリケーション側では、depends_on.db.conditionをservice_healthyにする。これで、PostgreSQLが接続を受け付けられる状態になるまで、アプリケーションの起動を待機させられる。
pg_isreadyは、データベースに対して実際のアプリケーション処理を行うコマンドではない。認証してクエリを実行するところまで確認するものでもないため、ヘルスチェックが成功しても、ユーザー名やパスワード、データベース名が間違っていればアプリケーションの接続は失敗する。
ここは見落としやすい。ヘルスチェックは「サービスが応答可能か」を確認するものであり、「アプリケーションの設定が完全に正しいか」を保証するものではない。必要に応じて、接続先のデータベース名やユーザーを明示し、アプリケーションが実際に使う条件に近づける。
MySQLの確認
MySQLではmysqladmin pingを利用できる。healthcheck.testを["CMD", "mysqladmin", "ping", "-h", "localhost"]とし、数秒間隔で確認する形だ。
MySQLの初回起動では、データディレクトリや指定したデータベースの準備に時間がかかることがある。そのため、コンテナが起動した直後から失敗回数を数え始めると、準備が終わる前にunhealthyと判定される可能性がある。初期化の時間を見込んでstart_periodを設定しておく。
なお、mysqladmin pingが応答していても、アプリケーション用ユーザーの権限や接続先スキーマまで検証できるとは限らない。運用上、権限設定の誤りを早く検出したい場合は、専用ユーザーでの接続確認や、軽い読み取りクエリを使う方法も検討できる。ただし、ヘルスチェックで毎回重い処理を実行するのは避けるべきだ。
Redisの確認
Redisはredis-cli pingで応答を確認できる。PONGが返り、コマンドの終了コードが成功になれば、少なくともRedisがコマンドを受け付けられる状態だと判断できる。
Redisに認証を設定している場合は、ヘルスチェックにも認証情報が必要になる。認証なしの環境で使えるコマンドを、そのまま本番用設定へ持ち込まないようにする。認証情報をComposeファイルへ直接書く場合は、リポジトリへの登録やログへの露出にも注意が必要だ。
メッセージブローカーの場合
RabbitMQなどのメッセージブローカーは、プロセスが起動していても、ポート、ノード、キュー管理などの初期化が完了していないことがある。利用するイメージが提供している診断コマンドを使い、単にTCPポートが開いているかだけでなく、可能ならサービス自身が正常と判断している状態を確認する。
ヘルスチェックのコマンドは、イメージの種類やタグによって利用できる場所が変わる。開発マシンで動いた設定を、別のイメージタグや本番環境へそのままコピーできるとは限らない。コンテナ内でコマンドが存在するか、終了コードが期待どおりかを確認してから採用する。
healthcheckのパラメータをどう調整するか
ヘルスチェックの値は、短ければよいというものではない。頻度を上げすぎると不要な負荷になり、猶予を長くしすぎると異常の検知が遅れる。
start_periodは初期化時間のために使う
start_periodは、サービスが起動した直後の準備時間を確保するための項目だ。データベースの初期化中に確認が失敗しても、その失敗を通常の連続失敗としてすぐに扱わないようにできる。
ここで大切なのは、start_periodを固定の正解として扱わないことだ。データ量、ストレージ性能、初回初期化の有無によって、必要な時間は変わる。普段は短時間で起動していても、空のボリュームを作った初回だけ時間が延びるケースもある。
intervalとtimeoutのバランス
intervalを短くすると、準備完了を早く検知できる。一方で、1秒未満のような極端に短い間隔にすると、起動時だけでなく稼働中も確認処理が頻繁に走る。データベースやブローカーが小さな開発用コンテナであれば、数秒程度の間隔から始めるのが扱いやすい。
timeoutは、確認コマンドが応答しない場合の上限だ。ネットワーク越しの重い確認を使う場合は長めに必要になるが、同じコンテナ内で単純な応答を確認するだけなら、長すぎる値にする必要はない。
retriesは一時的な揺らぎを吸収する
起動直後や負荷がかかった瞬間に、1回だけヘルスチェックが失敗することはある。retriesを1回にすると、こうした一時的な失敗でもすぐに異常判定になる。逆に大きくしすぎると、本当に停止しているサービスの検知が遅れる。
設定値は、確認間隔と合わせて考える。例えば、5秒間隔で複数回の失敗を許容するなら、異常判定までの時間はその分だけ延びる。retriesだけを見て調整するのではなく、実際にどれくらいの時間で異常と見なしたいのかを決めてから設定する。
起動順序だけに頼らず、アプリケーション側でも待つ
depends_onとhealthcheckを設定しても、アプリケーション側のリトライ処理は不要にならない。理由は、ヘルスチェックが成功した後にデータベースが再起動することもあれば、ネットワークや認証設定の問題で接続が一時的に失敗することもあるからだ。
Composeは、コンテナの初回起動順序を制御するのは得意だ。しかし、稼働中のサービス同士が常に接続できることまでは保証しない。アプリケーションは、依存サービスが一時的に利用できない状況を前提にしたほうがよい。
Laravelで注意したい接続設定
Laravelのデータベース設定では、接続先ホストにComposeのサービス名を使う。例えば、データベースサービス名がdbなら、アプリケーションコンテナから見たホストはlocalhostではなくdbになる。
config/database.phpでは、接続先、データベース名、ユーザー名、パスワードを環境変数から読み込む構成にしておく。retry_on_timeoutのような設定を利用できる場合でも、それがすべての接続拒否を自動的に再試行するわけではない点には注意したい。
タイムアウトは、接続後の処理が一定時間返ってこない場合に関係する。データベースがまだポートを開いていない、名前解決が一時的に失敗した、接続が拒否された、といった起動時のエラーをすべて吸収する設定ではない。
Laravelの起動処理でマイグレーションや設定キャッシュの生成を行う場合は、コマンド全体をリトライできるようにする方法もある。例えば、エントリーポイントでデータベース接続を確認し、成功するまで短い待機を挟んでからphp artisan migrateやアプリケーションサーバーを実行する。
ただし、マイグレーションを複数のコンテナが同時に実行する構成では、別の競合が発生する可能性がある。アプリケーションコンテナを複数起動する本番環境では、マイグレーション専用の一度限りのジョブに分けるなど、起動待ちとは別の設計が必要になる。
待機スクリプトを使う場合
wait-for-it.shのような待機スクリプトをエントリーポイントから呼び出す方法もある。db:5432のようにホストとポートを指定し、接続可能になるまで待ってからphp artisan serveなどの本来のプロセスを起動する。
この方法は、Composeのservice_healthyに対応していない環境でも使いやすい。起動条件をアプリケーションコンテナ内で完結させられる点も利点だ。
一方で、ポートが開いていることと、アプリケーションが正常に使えることは同じではない。TCP接続が成功しても、データベースの初期化や認証、必要なスキーマの作成が終わっていない可能性がある。可能なら、単なるポート確認ではなく、対象サービスが提供する固有の確認コマンドを使うほうが判断として正確だ。
待機スクリプトを使う場合は、次の点も確認しておきたい。
- スクリプトが実行権限を持っているか
ENTRYPOINTやCMDで本来のプロセスへ正しく処理を引き継ぐか- 待機が無限に続かないよう、タイムアウトを設定しているか
- 待機中の理由がログから分かるか
- シェルの終了コードを握りつぶしていないか
待機処理が失敗したのに、最後にアプリケーションを起動してしまうスクリプトは危険だ。接続できていない状態を、起動成功に見せてしまうからである。
リトライは「接続」だけでなく「処理」の単位で考える
接続を再試行できても、処理そのものが安全に繰り返せるとは限らない。例えば、キューからメッセージを取り出した後に接続が切れた場合、同じ処理が再実行される可能性がある。外部APIへの送信や決済のような処理では、単純なリトライが二重実行につながる。
Dockerの起動順序制御を考えるときも、単に「何回接続するか」だけで終わらせないことが重要だ。失敗した処理を繰り返しても問題ないか、タイムアウト後にどの状態へ戻るか、ログと監視で異常を追えるかまで設計に含める。
Composeのヘルスチェックは入口を整える仕組みで、アプリケーションのリトライは運用中の揺らぎに備える仕組みだ。役割が違うので、どちらか一方だけで済ませない。
複数の依存サービスを待つときの注意点
実際のWebアプリケーションは、データベースだけに依存するとは限らない。Laravelの構成でも、PostgreSQLやMySQLに加えて、Redis、キュー、メール送信サービス、検索エンジンなどを使うことがある。
appがdb、cache、queueのすべてに依存するなら、それぞれのサービスにヘルスチェックを定義し、app.depends_onで各サービスの条件をservice_healthyにする。
この場合、appの起動は3つのサービスがすべて正常になるまで待機する。依存関係が並列に準備されるなら、全体の待ち時間はおおむね最も遅いサービスに左右される。データベースだけが遅いと思っていたら、実際にはRabbitMQの初期化がボトルネックになっていた、ということもある。
すべてを1つのヘルスチェックに詰め込まない
複数の依存サービスを確認するために、アプリケーション側で1本の巨大なスクリプトを作る方法もある。しかし、そのスクリプトが失敗したとき、どの依存先に問題があるのか分かりにくくなる。
サービスごとにヘルスチェックを持たせると、Composeの状態から原因を切り分けやすい。
dbだけがunhealthyなら、データベースの初期化や認証を確認するcacheだけが失敗するなら、Redisのコマンドや認証を確認する- すべてが正常なのに
appが起動しないなら、アプリケーションの環境変数やマイグレーションを確認する
ヘルスチェックは、起動を遅らせるためだけの設定ではない。障害時に、どのサービスが利用不能なのかを判断する材料でもある。
service_completed_successfullyの使いどころ
service_completed_successfullyは、常駐するサービスではなく、一度実行して終了する処理に向いている。例えば、データベースのマイグレーションや初期データの投入を専用サービスとして実行し、その処理が正常終了してからアプリケーションを起動する構成だ。
ただし、マイグレーションを常にアプリケーション起動の前提にすると、失敗時にWebコンテナも起動しなくなる。これは意図した動作である場合もあるが、開発環境と本番環境で同じ扱いにするとは限らない。
また、常駐プロセスにこの条件を使うと、処理が終了しないため、後続サービスがいつまでも起動しない。データベースやRedisのようなサービスにはservice_healthyを使い、一度限りの処理にはservice_completed_successfullyを使うという切り分けが基本になる。
起動順序制御のベストプラクティス
Docker Composeで起動順序を設計するときは、次のような考え方が安定しやすい。
1. depends_onをサービスの準備完了と解釈しない
depends_onは依存関係を表すが、デフォルトの指定だけでは接続可能性を確認しない。アプリケーションがデータベースやキャッシュを必要とするなら、healthcheckとservice_healthyを組み合わせる。
2. ヘルスチェックは実際の利用条件に近づける
データベースなら、単にプロセスが存在するかではなく、接続を受け付けられるかを確認する。Redisなら応答コマンドを実行する。メッセージブローカーなら、利用するイメージが提供する診断機能を使う。
ただし、ヘルスチェックで重いクエリや副作用のある処理を実行するのは避ける。何度も繰り返される処理なので、短時間で終わり、状態を変更しない確認が基本だ。
3. 起動直後の失敗を許容する
初期化に時間がかかるサービスでは、start_periodを設定する。interval、timeout、retriesの組み合わせで、どの程度の時間で正常・異常を判定するのかを決める。
設定値を極端に短くすると、起動処理の途中で異常判定になりやすい。逆に長すぎると、設定ミスやクラッシュに気付くまで時間がかかる。開発環境では待ち時間を短くしたくなるが、初回起動やボリューム再作成時の挙動も確認しておく。
4. ヘルスチェックの失敗理由をログに残す
終了コードだけでなく、確認コマンドの出力も調査できるようにする。docker compose psで状態を確認し、必要ならdocker inspectでヘルスチェックの履歴を見ると、接続拒否なのか、認証失敗なのか、コマンド不足なのかを切り分けやすい。
ヘルスチェックのコマンドがコンテナ内に存在しないケースもある。軽量イメージでは、クライアントツールが最初から含まれていないことがあるため、設定を書いたら実際にコンテナ内で実行できるか確認する。
5. 本番環境ではアプリケーションの再接続を前提にする
healthcheckは、起動時の制御と状態表示には役立つ。しかし、データベースのフェイルオーバーや再起動、ネットワークの一時断を解決する機能ではない。
本番環境では、アプリケーションやワーカーが依存サービスの一時的な停止を検知し、適切に再接続できるようにする。接続失敗時に即座に終了してコンテナの再起動へ任せる構成もあれば、一定回数の指数バックオフを行ってから再試行する構成もある。サービスの性質に応じて選ぶべきだ。
6. restart設定とリトライを混同しない
コンテナの再起動ポリシーは、プロセスが終了した場合の復旧に関係する。プロセスが動き続けているものの、内部でデータベース接続を失っている場合、再起動ポリシーだけでは問題は解決しない。
逆に、アプリケーションが接続不能を検知して終了する設計なら、再起動ポリシーが復旧に役立つことがある。ただし、再起動を繰り返すだけでは原因を隠してしまうため、ログや監視と組み合わせて使う必要がある。
7. 開発環境と本番環境の責務を分ける
開発環境では、docker compose upだけでDB、キャッシュ、アプリケーションが順番に立ち上がると便利だ。service_healthyを設定しておけば、手動で何度もアプリケーションを再起動する必要が減る。
一方、本番環境では、Composeの起動順序だけを信頼しないほうがよい。オーケストレーション基盤を使う場合は、その基盤の readiness や liveness に相当する仕組み、アプリケーションの再接続処理、データベースの可用性設計を組み合わせる。Composeファイルを本番へ持ち込む場合も、開発時の便利な待機設定が、そのまま障害復旧の仕組みになるわけではない。
それでも接続エラーが出るときの確認箇所
service_healthyを設定したのに接続エラーが残る場合は、起動順序以外の問題を疑う。
まず確認したいのは、アプリケーションから見たホスト名だ。Composeのサービス間通信では、接続先にサービス名を使う。ホストマシンから接続するときのlocalhostと、アプリケーションコンテナから見たlocalhostは意味が違う。コンテナ内のlocalhostは、そのアプリケーション自身を指す。
次に、ポート番号を確認する。portsでホストへ公開するポートと、コンテナ間通信で使うポートは別だ。アプリケーションコンテナからDBへ接続するときは、通常、DBコンテナが内部で待ち受けているポートを指定する。ホスト側へ割り当てた番号を、そのままコンテナ間接続に使うとは限らない。
さらに、環境変数の読み込みタイミングにも注意する。Laravelでは設定キャッシュが残っていると、.envを変更しても実行時に古い接続情報が使われることがある。Composeの依存関係が正しくても、アプリケーションが別のホスト名やデータベース名を参照していれば接続は成功しない。
確認の順番としては、次の流れが分かりやすい。
1. dbコンテナが実行中か確認する
2. dbのヘルスチェックが成功しているか確認する
3. アプリケーションコンテナからサービス名が名前解決できるか確認する
4. コンテナ間のポート番号が正しいか確認する
5. ユーザー名、パスワード、データベース名を確認する
6. Laravelの設定キャッシュや起動スクリプトを確認する
7. 接続後のマイグレーションや権限エラーを確認する
この順序で見ていくと、「DBがまだ起動していない問題」と「DBは起動しているが設定が間違っている問題」を分離できる。
まとめ
Docker Composeのdepends_onは、依存先コンテナの起動開始を基準にした制御であり、内部サービスの準備完了までは待たない。データベースやキャッシュが利用可能になるまで時間を要する構成では、アプリケーションが先に接続を試み、起動に失敗することがある。
この問題には、依存先へhealthcheckを定義し、アプリケーション側のdepends_onでcondition: service_healthyを指定する方法が基本になる。PostgreSQLならpg_isready、MySQLならmysqladmin ping、Redisならredis-cli pingのように、サービスが実際に応答できるかを確認する。
ただし、ヘルスチェックは起動時の待機と状態判定を補助する仕組みだ。稼働中の再起動や一時的な通信障害まで自動的に解決するものではない。Laravelなどのアプリケーションには、接続や処理のリトライ、適切なタイムアウト、失敗時のログ出力を用意しておく必要がある。
起動順序を堅牢にするポイントは、コンテナ、サービス、アプリケーションの責務を分けて考えることだ。Composeには依存先の準備状態を知らせ、アプリケーションには一時的な接続失敗から復帰する手段を持たせる。この2つを組み合わせて初めて、docker compose up直後の不安定な接続エラーと、運用中のサービス再起動の両方に対応しやすい構成になる。