※本記事にはアフィリエイト広告が含まれます。
Laravel+MySQLをDocker Composeで動かしていると、docker compose upの直後にマイグレーションやキューワーカーがDB接続エラーで落ちることがあります。depends_on: - dbと書いてあるのに、なぜDBより先にアプリが動いてしまうのでしょうか。
この記事では、Docker公式ドキュメントとMySQLのリファレンスマニュアルをもとに、depends_onとhealthcheckの正しい組み合わせ方を整理します。あわせて、よく使われるmysqladmin pingの落とし穴も説明します。仕様の記述は、いずれも確認日(2026年10月15日)時点の公式ドキュメントにもとづいています。なお、記事中の設定例は公式仕様に沿って組み立てたもので、動作結果は載せていません。
結論:depends_onの短い書き方は「起動」までしか待たない。DB側にhealthcheck、アプリ側にcondition: service_healthyを書く
Docker公式の起動順序のページには、次のように書かれています。
On startup, Compose does not wait until a container is “ready”, only until it’s running.
つまり、depends_onの短い書き方は「DBコンテナが動き出した」ところまでしか待ちません。MySQLのプロセスが接続を受け付けられる状態かどうかは見ていないのです。準備完了まで待たせるには、DB側にhealthcheckを定義し、アプリ側のdepends_onを長い書き方にしてcondition: service_healthyを指定します。
最小の形は次のとおりです(値は後の章で説明します)。
services:
db:
image: mysql:8.4
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
app:
build: .
depends_on:
db:
condition: service_healthy
Compose file referenceにも、service_healthyを指定した依存先については「Compose waits for healthchecks to pass」と書かれています。
depends_onの3つのconditionと、restart・requiredの意味
depends_onの長い書き方では、依存先ごとにcondition・restart・requiredを指定できます。Compose file referenceの記述を表にまとめます。
| 項目 | 値・意味 | 導入バージョン・既定値 |
|---|---|---|
condition: service_started | 短い書き方と同じ。依存先が起動すれば満たされる | – |
condition: service_healthy | 依存先がhealthcheckで「healthy」になるまで待つ | – |
condition: service_completed_successfully | 依存先が正常に完了(終了)するまで待つ | – |
restart: true | Composeが依存先を更新したあと、このサービスも再起動する | Docker Compose 2.17.0〜 |
required: false | 依存先が起動していない・使えない場合も警告だけで済ませる | Docker Compose 2.20.0〜。未指定ならtrue |
restart: trueには注意点があります。リファレンスでは、対象は「an explicit restart controlled by a Compose operation」で、「automated restart by the container runtime after the container dies」は含まないとされています。DBコンテナが落ちてDocker側の再起動ポリシーで立ち上がり直した場合は、アプリは再起動されません。restart: trueは「どんな場合でも再接続させる」設定ではないと考えておくのが安全です。
なお、起動順序のページの例でも、restart: trueは「Composeの操作でdbが更新・再起動されたときに、webも再起動して接続し直せるようにする」ためのものとして使われています。
restartやrequiredは、古いCompose(2.17.0・2.20.0より前)では使えません。チームでComposeのバージョンがそろっていない場合は、docker compose versionで手元のバージョンを確かめてから使ってください。
healthcheckの各項目と既定値(test・interval・timeout・retries・start_period・start_interval)
testの書き方:CMD/CMD-SHELL/NONE
testは文字列かリストで書きます。リストの場合、先頭はNONE・CMD・CMD-SHELLのいずれかです。CMD-SHELLはコンテナの既定のシェルでコマンドを実行し、NONEはhealthcheckを無効にします。文字列で書くと、CMD-SHELLに続けてその文字列を書いたのと同じ扱いになります。
環境変数を展開したいときはCMD-SHELLを使います。CMDはシェルを通さないためです。
既定値はDockerfileのHEALTHCHECKと同じ
Composeのhealthcheckは「It works in the same way, and has the same default values, as the HEALTHCHECK Dockerfile instruction」とされています。そこでDockerfile referenceのHEALTHCHECK節から既定値を引くと、次のとおりです。
| Compose | Dockerfile | 既定値 | 意味 |
|---|---|---|---|
interval | --interval | 30s | チェックの間隔 |
timeout | --timeout | 30s | 1回のチェックの制限時間 |
start_period | --start-period | 0s | 起動直後の猶予期間 |
start_interval | --start-interval | 5s | 猶予期間中のチェック間隔 |
retries | --retries | 3 | 何回連続で失敗したらunhealthyにするか |
既定のままだと、DBがhealthyになるのは早くても最初のチェックが成功した時点です。間隔30秒のままでは、起動のたびにアプリ側の待ち時間が長くなりがちです。開発環境ではintervalを短めにしておくと扱いやすくなります。
start_periodの中の失敗は数えない
Dockerfile referenceによると、start_periodの間に失敗したチェックは、retriesの回数に数えられません。ただし、期間中に一度でも成功すると、その時点でコンテナは起動済みとみなされ、以後の連続した失敗はretriesに数えられます。start_intervalは、この猶予期間中のチェック間隔です(Docker Engine 25.0以降が必要)。
Compose file referenceでは、interval・timeout・start_period・start_intervalの時間指定の説明に、Docker Compose 2.20.2で導入された旨の注記があります。start_intervalを使う場合は、ComposeとDocker Engineの両方のバージョンを確認してください。
終了ステータスの意味
チェック用コマンドの終了ステータスは、0がhealthy(使える状態)、1がunhealthy、2は予約済みで使ってはいけない値です。自作のチェックスクリプトを書くときは、失敗時に2を返さないようにします。
Laravel+MySQLのcompose.yaml例:app・queue・schedulerとマイグレーション用サービス
ここまでの仕様を、Laravelでよくある「Webアプリ・キューワーカー・スケジューラー」の3サービス構成に当てはめます。ポイントは、マイグレーションを一回限りのサービス(migrate)に分け、ほかのサービスにservice_completed_successfullyで待たせることです。これで、テーブルができる前にキューワーカーがジョブを取りに行く、という順序の問題も避けられます。
以下は公式仕様に沿った設定例で、動作結果は載せていません。build: .のDockerfileや、.envの中身は各自の環境に合わせてください。
services:
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u $${MYSQL_USER} -p$${MYSQL_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
migrate:
build: .
command: php artisan migrate --force
env_file: .env
environment:
DB_HOST: db
depends_on:
db:
condition: service_healthy
app:
build: .
env_file: .env
environment:
DB_HOST: db
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfully
queue:
build: .
command: php artisan queue:work
env_file: .env
environment:
DB_HOST: db
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfully
scheduler:
build: .
command: php artisan schedule:work
env_file: .env
environment:
DB_HOST: db
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfully
volumes:
db-data:
設定例の各部分の根拠は次のとおりです。
$$のエスケープ:Compose file内の${...}はComposeが展開します。$$と書くと、Composeに処理させずにコンテナ側の環境変数として参照できます。公式の起動順序のページも、PostgreSQLの例でpg_isready -U $${POSTGRES_USER}のように書いています。DB_HOSTにはサービス名:Composeのネットワークでは、各コンテナがサービス名(ここではdb)でほかのコンテナのIPアドレスを引けます。127.0.0.1やlocalhostはアプリのコンテナ自身を指すので、DBにはつながりません。migrate --force:Laravelのマイグレーションは、本番のDBに対しては実行前に確認を求めます。--forceを付けると確認なしで実行されます。対話できないコンテナで確認待ちにならないよう付けています。schedule:work:Laravelの公式ドキュメントでは、ローカルではcronを登録する代わりにschedule:workを使えるとされています。フォアグラウンドで動き、止めるまで毎分スケジューラーを呼び出すため、コンテナのメインプロセスにできます。queue:work:キューワーカーを動かすArtisanコマンドです。
migrateサービスが正常に完了しなかった場合、service_completed_successfullyの条件が満たされないため、app・queue・schedulerは起動しません。マイグレーションの失敗を見落としたままアプリが動き出す状態を防げます。
よくある誤解:healthyなのに接続できない
mysqladmin pingは「Access denied」でも0を返す
MySQLのhealthcheckとして広く使われているmysqladmin pingですが、MySQL 8.4 Reference Manualには次のように書かれています。
The return status from mysqladmin is 0 if the server is running, 1 if it is not. This is 0 even in case of an error such as Access denied, because this means that the server is running but refused the connection, which is different from the server not running.
Composeのhealthcheckは終了ステータス0をhealthyとみなします。そのため、healthcheckの-u・-pに書いたユーザー名やパスワードが間違っていても、サーバーさえ動いていればhealthyになります。healthyが示すのは「MySQLサーバーが応答している」ことだけです。アプリのDB_USERNAME・DB_PASSWORDで認証できること、DB_DATABASEのデータベースがあることまでは保証しません。
先ほどの設定例でhealthcheckにMYSQL_USERを渡しているのは、アプリと同じユーザーを使う意図をはっきりさせるためです。ただし、この仕様があるので、それだけで認証の確認にはなりません。認証情報の誤りは、DBに実際につなぐmigrateサービスの段階で表に出ることになります。
初回起動だけ遅いのは、初期化中は接続を受け付けないから
Docker HubのMySQL公式イメージのページでは、コンテナ起動時にデータベースが初期化されていなければ既定のデータベースを作成し、「it will not accept incoming connections until such initialization completes」と説明されています。さらに、Docker Composeのように複数のコンテナを同時に起動するツールでは問題になりうる、とも書かれています。
つまり、データ用ボリュームが空の初回起動は、2回目以降より準備完了まで時間がかかります。start_periodを設けておけば、この間のチェック失敗はretriesに数えられないので、初期化の途中でunhealthyと判定されることを避けられます。start_periodが既定の0sのままだと、interval×retriesの範囲で初期化が終わらなければunhealthyになりえます。
接続エラーが出たときに確認する順番
Laravel側にリトライ処理を足す前に、次の順で設定を見直すと原因を切り分けやすくなります。
- アプリ側の
depends_onが短い書き方のままになっていないか(condition: service_healthyがあるか) - DB側に
healthcheckが定義されているか。CMD形式で環境変数を使っていないか - 初回起動なら、
start_periodが初期化にかかる時間に足りているか DB_HOSTがサービス名になっているか- healthyなのに接続できないなら、アプリの認証情報とデータベース名(
mysqladmin pingはここを見ていない)
MySQL公式イメージのページも、接続をリトライするループを置く必要がある場合があるとしています。healthcheckで起動時の待ち合わせを整えたうえで、運用中の切断に備えてアプリ側のリトライも検討する、という順番がよいでしょう。
まとめ
depends_onは、短い書き方のままではコンテナの起動までしか待ちません。DBの準備完了を待たせるための設定を、チェックリストにまとめます。
- DB側:
healthcheckを定義する。環境変数を使うならCMD-SHELLと$$。intervalは既定の30sより短めにする - アプリ側:
depends_onを長い書き方にしてcondition: service_healthy。マイグレーションは別サービスに分けてservice_completed_successfullyで待つ - 初回起動:MySQLは初期化が終わるまで接続を受け付けない。
start_periodで猶予を取る - 落とし穴:
mysqladmin pingはAccess deniedでも0を返す。healthyは認証の成功を意味しない - バージョン:
restartはCompose 2.17.0〜、requiredは2.20.0〜
Docker環境でPHPやLaravelのバージョンを上げる予定がある方は、関連記事もあわせて参考にしてください。
PHP 8.2のサポートは2026年12月31日で終了|移行先は8.3・8.4・8.5のどれか、Laravelの対応表と非互換の変更から比べる
Laravel 12から13へのアップグレードガイド|影響の大きい変更をPHP 8.3の必須化・CSRF・セッション・キャッシュの順に解説
Composeの設定だけでなく、イメージ・ボリューム・ネットワークの考え方から体系的に押さえたい場合は、Dockerの入門書を1冊手元に置いておくと、公式ドキュメントを読むときの土台になります。


