
この場合、Laravelのマイグレーションやキュー処理が本番DBに接続し、意図しない処理を始めるおそれがある。原因は単なる .env の記述ミスとは限らない。Docker Composeでは、同じ環境変数が複数の場所に存在すると、どの値を採用するかが決められている。
しかも、ここには二つの優先順位がある。compose.yaml に書いた ${DB_HOST} を置き換える段階と、最終的にコンテナへ環境変数を渡す段階だ。この二つを一緒に考えると、ローカル開発環境から本番DBへ接続する事故につながる。
なぜローカル開発環境が本番DBに接続されるのか:変数補間の罠
[Docker Composeは、compose](/articles/dockerdao-runofen-qi-dian-ge/).yaml や docker-compose.yml 内に書かれた ${VAR} 形式の変数を、コマンド実行時に値へ置き換える。この処理は、コンテナが起動した後ではなく、Composeが設定ファイルを読み込んで構成を解決するときに行われる。
たとえば、設定ファイルに次のような記述があるとする。
DB_HOST: ${DB_HOST}
この右辺の ${DB_HOST} は、コンテナの中にあるLaravelの .env を直接読んでいるわけではない。docker compose コマンドを実行したホスト側の環境や、Composeが参照する環境変数ファイルから値を探している。
Composeファイルでは、environment は単独の文字列ではなく、マッピングまたはリストとして指定する必要がある。したがって、次のような書き方は有効な設定にならない。
environment: DB_HOST=${DB_HOST}
マッピング形式なら、次のように書く。
`environment:
DB_HOST: ${DB_HOST}`
リスト形式を使う場合は、次のようになる。
`environment:
- DB_HOST=${DB_HOST}`
どちらも、${DB_HOST} の補間元がホスト側にあるという点は変わらない。YAMLの書式を正しくしても、ホスト側で本番用の値が優先されていれば、その値がコンテナへ渡される。
補間に使われる主な候補は、次の順で確認すると理解しやすい。
1. docker compose を実行したシェルの環境変数
2. --env-file で明示した環境変数ファイル
3. プロジェクトディレクトリにあるデフォルトの .env
シェルで次のように設定していた場合、プロジェクト内の .env に別の値を書いても、そちらが採用される。
export DB_HOST=prod-db.example.com
プロジェクトの .env が次の内容だったとしても、実行時のシェルに DB_HOST が存在すれば、補間される値はシェル側のものになる。
DB_HOST=127.0.0.1
ここで注意したいのは、.env が読み込まれなかったわけではないという点だ。読み込み自体は行われていても、より優先度の高いシェル環境変数によって値が置き換えられている可能性がある。
.env に正しい値を書いたことと、Composeがその値を使うことは同じではない。事故につながる典型パターン
この種のトラブルは、次のような手順で発生しやすい。
- 本番運用の作業で、端末に本番DBの接続情報を一時的に設定する
~/.bashrcや~/.zshrcに、その設定を残したままにする- 別のプロジェクトでローカル用の
.envを新しく作成する DB_HOSTがローカルの値になっていることを確認せず、docker compose upを実行する- Laravelやキューワーカーが、設定された接続先へ接続する
この流れでは、ローカル環境から本番DBへ接続するおそれがある。実際にマイグレーションや書き込み処理が実行されるかどうかは、Laravelの設定や起動しているサービスによって異なるが、接続先を誤った時点で止めるべきだ。
特に危険なのは、シェルプロファイルに本番用の変数を登録しているケースだ。export を実行したターミナルだけの一時設定だと思っていても、実際にはシェルの起動設定から再び読み込まれていることがある。
一度ターミナルを閉じれば消えると思い込んでいると、別のターミナルを開いたときに同じ値が復活する。問題は過去のセッションではなく、シェルの初期化ファイルに残った設定である。
「読み込まれない」と「上書きされる」は違う
Docker Composeで環境変数が期待どおりにならない場合、すぐに「.env が読み込まれていない」と判断しないほうがいい。
原因は大きく二つに分かれる。
- 参照している
.envの場所やファイル名が違う .envは読まれているが、別のソースの値が優先されている
前者なら、値が未定義になったり、Composeから警告が出たりする。後者では、設定が一見正常に見えたまま、意図しない接続先が使われる。後者のほうが発見しにくい。
DB_HOST のような一般的な名前は、別のプロジェクトや作業用スクリプト、シェルプロファイルにも登場しやすい。環境変数名が短く一般的であるほど、どこから入ってきた値なのかを追いにくくなる。
補間とコンテナ内の環境変数は別の処理
ここで、二つの処理を切り分けておく必要がある。
一つ目は、Composeが設定ファイル内の ${DB_HOST} を実際の文字列へ置き換える処理だ。これはホスト側で行われる。
二つ目は、置き換え後の値や env_file の値を、コンテナ内のプロセスへ環境変数として渡す処理だ。こちらはコンテナの生成時に行われる。
たとえば、Composeファイルに次のような設定があったとする。
`environment:
DB_HOST: ${DB_HOST}`
この場合、最初にホスト側の環境から ${DB_HOST} が解決される。その後、解決された値がコンテナの DB_HOST として渡される。
一方、次のような設定では、env_file がコンテナへ渡す環境変数の供給元になる。
env_file: .env.local
この二つを同じ「.env の読み込み」として扱うと、トラブルの切り分けに失敗する。補間用の環境変数ファイルと、コンテナへ注入する env_file は、名前が似ていても役割が異なる。
Docker Composeにおける環境変数の評価順序と優先ルール
まずは、Composeファイルを解釈するときの優先順位を確認する。ここで見るのは、compose.yaml の ${VAR} を何で置き換えるかというルールである。
| 優先順位 | 補間に使われるソース | 役割 |
|---|---|---|
| 1 | シェル環境変数 | コマンドを実行したプロセスが持つ値 |
| 2 | --env-file で指定したファイル | コマンド単位で明示する補間用ファイル |
| 3 | デフォルトの .env | プロジェクトで自動的に参照される補間用ファイル |
--env-file は、開発・検証・CIなどで使用する補間元を明示したい場合に役立つ。ただし、シェル環境変数が残っていれば、--env-file の値よりもシェル側の値が優先される。
つまり、次のようにファイルを明示しただけでは、ホストの環境変数による上書きを完全には防げない。
docker compose --env-file .env.local up
このコマンドを実行するシェルに DB_HOST が export されていれば、ファイルの値ではなくシェル側の値が使われる。コマンドにファイル名が書かれているため安心しやすいが、明示指定は優先順位の最上位を無効にするものではない。
シェル環境変数を確認する
接続先が怪しいときは、まずホスト側で値を確認する。
printenv DB_HOST
または、シェルで設定されているかだけを確認するなら、次の方法も使える。
env | grep '^DB_HOST='
値が不要なら、現在のシェルから削除する。
unset DB_HOST
ただし、unset は現在のシェルにしか効かない。次にターミナルを開いたときも同じ値が現れるなら、~/.bashrc、~/.zshrc、~/.profile などを調べる必要がある。
複数のターミナル、端末のシェル、IDEの統合ターミナル、タスクランナーがそれぞれ異なる環境を持つこともある。同じプロジェクトを開いているのに、実行場所によって結果が変わる場合は、.env だけでなく、コマンドを実行しているプロセスの環境を確認する。
シェルの外側にも、環境変数が入り込む経路はある。IDEが起動時に親プロセスの環境を引き継いでいたり、タスクランナーが独自の設定ファイルから変数を追加していたりするためだ。ターミナルで確認した値と、IDEから起動したComposeの値が違う場合は、同じコマンドを実行しているように見えても、実際の実行環境は同じではない。
Composeファイルの選択にも注意する
環境変数の値が正しいのに設定が変わらない場合は、そもそも別のComposeファイルを読み込んでいないかを疑う。
Compose V2では、標準的なファイル名として compose.yaml が使われる。従来の docker-compose.yml も利用できるが、同じディレクトリに複数の候補を置いたままにすると、どのファイルが実際に選ばれているかが分かりにくくなる。
さらに、次のような要素も解決後の設定に影響する。
-fで明示したComposeファイル- 複数ファイルを指定した場合の後勝ちのマージ
- 実行したディレクトリとプロジェクトディレクトリの違い
COMPOSE_FILE環境変数によるファイル指定COMPOSE_PROJECT_NAMEや-pによるプロジェクト名の指定
「このファイルを編集したのに反映されない」という場合、編集対象と実行対象が同じとは限らない。設定ファイルの内容だけでなく、どのファイルをどの順番で読み込んだかまで確認する必要がある。
コンテナ起動時に適用される環境変数の階層構造
補間が終わったあと、Composeはコンテナへ渡す環境変数を組み立てる。この段階では、補間の優先順位とは別のルールが働く。
大まかな関係は次のとおりだ。
| 優先順位 | 設定ソース | 適用される場面 |
|---|---|---|
| 1 | docker compose run -e KEY=VALUE | run で一時コンテナを起動するとき |
| 2 | Composeファイルの environment | サービス定義からコンテナへ渡すとき |
| 3 | Composeファイルの env_file | ファイルからサービスの環境変数を渡すとき |
| 4 | Dockerfileの ENV | イメージに組み込まれたデフォルト値 |
厳密には、Composeファイルの environment でも、値を直接記述する場合と、${VAR} から補間する場合がある。しかし、どちらも最終的には environment に定義された値として扱われ、同じキーの env_file より優先される。
この表で重要なのは、docker compose run -e と docker compose up を混同しないことだ。run -e は、指定した一時的なサービス実行に対して環境変数を渡すオプションであり、通常の up のサービス定義を変更するものではない。
Dockerfile の ENV は、イメージにあらかじめ入れておく初期値として扱うのが安全だ。Composeの environment や env_file に同じキーがあれば、イメージ内の値よりもCompose側の値が優先される。
environment と env_file の違い
environment は、Composeファイル内でサービスに渡す変数を定義する。値を直接書くことも、ホスト側の変数から補間することもできる。
マッピング形式では、キーと値の関係が明確になる。
`environment:
APP_ENV: local
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT:-3306}`
DB_HOST の値がホスト環境や補間用ファイルから解決され、コンテナ内の DB_HOST に渡される。DB_PORT が未定義の場合は、指定したデフォルト値が使われる。
リスト形式でも同じ目的を達成できる。
`environment:
- APP_ENV=local
- DB_HOST=${DB_HOST}`
ただし、キーごとの設定を見比べる必要があるComposeファイルでは、マッピング形式のほうが重複や書き間違いを発見しやすい。今回のように特定の変数を補間する場合も、environment: DB_HOST=${DB_HOST} のようにスカラーで書かず、必ずマッピングまたはリストで記述する。
env_file は、複数の環境変数をファイルからまとめて読み込むための指定だ。機密情報をComposeファイルから分離したい場合や、環境ごとに設定ファイルを切り替えたい場合に使いやすい。
ただし、environment と env_file の両方で同じキーを指定した場合は、environment 側が優先される。env_file に正しいローカル値を書いていても、environment に本番向けの値が残っていれば、コンテナには後者が渡る。
このため、設定を分割する場合は、同じキーを複数の場所で定義しないほうがよい。どうしても上書きが必要なら、どのファイルを先に読み、どの定義が最終値になるのかをプロジェクト内で明文化しておく。
env_file を複数指定する場合
複数の環境変数ファイルを使う場合は、ファイルの並び順にも意味がある。共通設定を先に置き、ローカル固有の設定を後に置く構成なら、後から指定した値で上書きする設計になる。
たとえば、共通設定を .env.common、開発者ごとの値を .env.local に分けるとする。
.env.commonには、すべての環境で共通する設定を置く.env.localには、開発マシンだけで変わる接続先やポートを置く- 同じキーが両方にある場合は、後に指定したファイルの値を採用する
- ファイルに存在しないキーは、別のソースやイメージ側の値が補う可能性がある
この構成は便利だが、「後ろにあるファイルがローカル用だから安全」とは限らない。後ろのファイル自体を取り違えたり、シェル環境変数がさらに上位で設定されていたりすれば、期待どおりにはならない。
Laravelの .env も別に確認する
Docker Composeでコンテナに DB_HOST が渡っていても、Laravelが実際に使う値は、Laravelの設定キャッシュによって変わることがある。
Laravelでは、アプリケーションの設定がキャッシュされていると、.env を変更しただけでは実行中のアプリケーションに反映されない場合がある。Compose側で接続先を直したのに、Laravelが以前のDBへ向かい続けるなら、コンテナ内の環境変数だけでなく、設定キャッシュも確認する必要がある。
さらに、PHP-FPMやキューワーカーのような長時間起動するプロセスは、起動時に読み込んだ設定を保持していることがある。ファイルを修正しただけで既存プロセスの状態まで変わるわけではない。Compose側の設定を更新したときは、コンテナの再作成やアプリケーションプロセスの再起動が必要になる場合がある。
確認の順番は、次のように分けるとよい。
1. ホスト上で、Composeの補間に使われる DB_HOST を確認する
2. docker compose config で解決後のサービス設定を確認する
3. 起動したコンテナ内で printenv DB_HOST を確認する
4. Laravelの設定キャッシュや起動済みワーカープロセスを確認する
5. 実際の接続ログで、ホスト名とポートを確認する
この順番なら、「ホストでは正しい」「Composeの設定も正しい」「コンテナ内も正しい」のに、アプリケーションだけが古い値を使っているという段階まで切り分けられる。
docker compose config コマンドによる設定の可視化と検証
本番誤接続を防ぐうえで、最も手軽で効果が高いのが docker compose config の確認だ。これは、Composeファイルや環境変数を解決した後の構成を表示する。
設定ファイルに ${DB_HOST} と書いてあっても、出力では実際に採用された値が確認できる。ここに本番DBのホスト名が表示されるなら、コンテナを起動する前に止められる。
docker compose config
機密情報が含まれる可能性があるため、出力をそのまま共有したり、CIログへ無制限に保存したりするのは避ける。確認したいキーだけを抽出する、ホスト名をマスクして保存するなど、ログの扱いには注意が必要だ。
検証で見るべき項目
docker compose config の出力では、単に DB_HOST の値だけを見るのでは不十分だ。次の項目をまとめて確認する。
- 対象サービスの
environmentに入った最終値 env_fileが指定されているかenvironmentとenv_fileに同じキーがないかDB_HOST、DB_PORT、DB_DATABASEなど接続先を構成する値の組み合わせ- 使用されているComposeファイル
- 複数ファイルのマージによって追加・上書きされた設定
- 未定義変数や空文字列になった変数
- 接続先に関係するネットワーク、ポート、ボリュームの設定
DBホストだけがローカルでも、ポートやデータベース名が本番用のままということがある。逆に、ホスト名が開発用でも、VPN経由で本番ネットワークへ到達できる構成なら安全とは言い切れない。
DB_HOST だけを見て安心せず、接続に使う一組の設定として確認することが重要だ。Laravelの場合は、DB_CONNECTION、DB_HOST、DB_PORT、DB_DATABASE、DB_USERNAME をまとめて確認すると、環境の食い違いを見つけやすい。
--quiet の使いどころ
docker compose config --quiet は、解決後の構成を大量に表示せず、設定が有効かどうかを確認する用途に向いている。未定義変数など、構成上の問題を起動前に検出するゲートとして使える。
ただし、--quiet だけでは、接続先が本番かローカルかを人間が確認できない。構文や解決の成否を機械的に判定するチェックと、危険なホスト名を検出するチェックは別に考えるべきだ。
開発者向けの起動スクリプトでは、次のような順番にする。
1. Composeの構成を解決する
2. 接続先を検査する
3. 期待する開発用の値でなければ起動を中止する
4. 問題がなければサービスを起動する
接続先を検査するときは、単純に本番ホスト名だけを禁止するのではなく、許可するローカルホスト名や開発用ネットワークを決めておくほうが堅牢だ。禁止リストは、接続先の命名変更や別の本番エンドポイント追加で抜け道が生まれやすい。
未定義変数と空文字列
compose.yaml が ${DB_HOST} を参照しているのに、シェルにも補間用の環境変数ファイルにも値がない場合、Composeは警告を出すことがある。設定によっては、未定義の値が空文字列として扱われる可能性もある。
空文字列なら安全、とは限らない。アプリケーション側のデフォルト値が使われたり、別の設定ファイルの値で補われたりするためだ。接続失敗になる場合もあるが、意図しないフォールバックで別のサービスへ向かう場合もある。
接続先のような重要な変数は、未定義を許容しない設計にする。Composeの補間構文には、必須変数として扱うための書き方もある。たとえば ${DB_HOST:?DB_HOST is required} のように指定すれば、値がなければ構成解決の段階で停止させられる。
この方法でも、誤った値が設定されている問題までは解決できない。必須チェックと許可値の検査を組み合わせる必要がある。
本番誤接続を防ぐための環境変数管理と運用プラクティス
優先順位の仕様を知るだけでは、事故は止まらない。必要なのは、誤った値が入っていても起動前に検出できる構成である。
シェルプロファイルを定期的に棚卸しする
最初に確認すべきなのは、各開発者のシェル環境だ。~/.bashrc、~/.zshrc、~/.profile などに、次のような一般的な変数を直接 export していないか確認する。
DB_HOSTDB_PORTDB_DATABASEDB_USERNAMEREDIS_HOSTAPP_ENV- 外部APIの接続先を示す変数
グローバルなシェル変数は、複数のプロジェクトに影響する。特定のプロジェクトだけで使う値をシェル全体へ出すと、別のCompose構成まで汚染する。
本番作業が必要な場合も、常時 export しておくのではなく、作業用のコマンドや限定されたシェルで扱うほうが安全だ。作業終了後に unset するだけでなく、永続設定に書き込まれていないかまで確認する。
.env.example と実値を分離する
プロジェクトでは、.env.example に必要なキーと安全な例を置き、実際の .env はGit管理から外す。雛形には、本番のホスト名や認証情報を入れない。
ローカル用の初期値を用意する場合も、接続先は開発用であることが明確な値にする。たとえば、開発用DBのサービス名を db と決めているなら、ローカルの .env.example でもその名前を使う。実値を手入力するたびに接続先が変わる構成は、設定ミスを増やす。
ただし、.env.example があるだけでは設定漏れを防げない。初回セットアップ用のスクリプトで .env を生成し、必須キーの存在を確認する方法も有効だ。生成後に、DBホストが開発用の許可値になっているかを検査すれば、雛形と実値の取り違えも減らせる。
環境ごとにファイルを明示する
開発、テスト、ステージング、本番で、同じ .env を使い回さない。起動コマンドも環境ごとに固定し、どのファイルを参照しているかをコマンドから分かるようにする。
たとえば、開発時は .env.local、テスト時は .env.test のように役割を分ける。CIでは暗黙のカレントディレクトリにある .env に依存せず、ジョブの中で使用するファイルを明示する。
ただし、ファイルを明示してもシェル側の値が優先される点は変わらない。CIのジョブ開始時に、不要な接続先変数を削除する処理を入れることも検討する。CIサービスやランナーが自動的に注入する環境変数がある場合は、その一覧も管理対象になる。
開発用の起動経路を一つに寄せる
開発者がそれぞれ異なるコマンドを実行すると、環境変数の扱いもばらばらになる。直接 docker compose up を実行する人、独自のシェルスクリプトを使う人、IDEのタスクから起動する人が混在すると、どの段階で値が変わったのか追跡しにくい。
そこで、プロジェクトの起動経路を一つに寄せる。Makefileやスクリプトを使う場合も、次の処理を含めるとよい。
- 必要な環境変数が定義されているか確認する
- 解決後のCompose設定を検証する
- DBホストが開発用の許可値か検査する
- 本番を示す接続先があれば起動を中止する
- 問題がなければComposeを起動する
起動処理を一か所へ集めると、個々の開発者が覚えるべきComposeの細かなオプションも減る。その代わり、スクリプト自体はレビュー対象にする。起動処理が安全であることを確認できなければ、単に複雑さを別の場所へ移しただけになる。
DB側でもローカル接続を止める
アプリケーション側の設定だけに依存しないことも重要だ。ローカル端末から本番DBへ接続できるネットワーク構成は、環境変数を一つ間違えただけで事故になる。
本番DBのアクセス制御では、次のような防御を組み合わせる。
- 開発端末から本番DBへ直接到達できないようにする
- 本番DBへの接続元を限定する
- VPNや踏み台を経由しなければ接続できないようにする
- 開発用アカウントに本番データを書き換える権限を与えない
- 本番DBで破壊的な操作を監視・制限する
DB側の制御は、Composeの設定ミスを修正するものではない。しかし、アプリケーション層の事故をインフラ側で止める最後の境界になる。
接続先を意味のある名前で管理する
DB_HOST に本番とローカルで似たようなIPアドレスを設定すると、値を目視したときの判断を誤りやすい。環境ごとに役割が分かるホスト名を使い、開発用の接続先を許可リストで管理するほうがよい。
また、DB_HOST だけでなく、DB_DATABASE や APP_ENV も組み合わせて検査する。APP_ENV=local なのに本番DBホストが指定されている、あるいは開発用ホストなのに本番データベース名が指定されているといった矛盾を、起動前にエラーにする。
設定値を単独で検査するより、環境の組み合わせを検査したほうが事故を見つけやすい。
本番誤接続は、接続文字列の一項目だけが間違って起きるとは限らない。ホスト、ポート、データベース名、実行環境を一つの組み合わせとして扱う必要がある。
実際に切り分けるときの順番
すでにローカル環境が本番DBへ接続した疑いがある場合は、まず書き込み処理を止める。Laravelのキュー、スケジューラ、マイグレーション、ワーカーが動いているなら、Composeの設定を調べる前に停止対象を確認する。
そのうえで、次の順番で値を追う。
1. ホスト側の値を確認する
printenv DB_HOST で、現在のシェルが持つ値を確認する。シェルプロファイルやIDEの起動設定も対象にする。
2. 補間用ファイルを確認する
デフォルトの .env と、--env-file で指定しているファイルが一致しているか確認する。実行ディレクトリも合わせて見る。
3. 解決後のCompose設定を確認する
docker compose config で、environment に入った値、env_file、複数ファイルのマージ結果を確認する。
4. コンテナ内の値を確認する
起動済みコンテナで printenv DB_HOST を実行し、Composeが渡した値を確認する。ホスト側で見た値と一致するとは限らない。
5. Laravelの設定を確認する
設定キャッシュ、ワーカー、長時間起動しているプロセスが古い値を保持していないか調べる。
6. DBの接続ログを確認する
アプリケーションが表示している設定値ではなく、DB側の接続ログや監査ログで、実際にどの接続元からアクセスがあったか確認する。
接続情報をログへ出す場合は、パスワードやトークンを記録しない。ホスト名やポートなど、原因調査に必要な最小限の情報だけを扱う。
設定を修正したあとも、すでに作成済みのコンテナや長時間動いているプロセスが古い値を保持していないか確認する。Composeファイルや環境変数ファイルを直しただけで、既存コンテナ内のプロセスが自動的に新しい値へ切り替わるとは限らない。再作成や再起動の要否を確認し、修正後の値をもう一度 docker compose config とコンテナ内の両方で確認する。
優先順位を知識で終わらせない
Docker Composeの環境変数に関する問題は、.env の書式を覚えるだけでは解決しない。どの値が補間に使われ、どの値がコンテナへ渡され、さらにアプリケーションがどの設定を参照しているかを、段階ごとに確認する必要がある。
特に注意すべきなのは、次の三つを別物として扱うことだ。
- Composeファイル内の
${VAR}を解決するための環境変数 environmentやenv_fileからコンテナへ渡される環境変数- Laravelの設定ファイルや設定キャッシュが実際に参照する値
このどこか一つだけを確認しても、誤接続の全体像は見えない。
ローカル起動前には、docker compose config で解決後の構成を確認する。シェルプロファイルに残った本番用の export を削除する。環境ごとのファイルを明示し、起動経路を統一する。最後に、本番DB側のネットワークと権限で、ローカルからの書き込みを止める。
Docker Composeは、接続先が本番かローカルかを判断してくれない。指定された値が有効なら、そのままコンテナへ渡すだけだ。だからこそ、docker compose env 読み込まれない と感じたときは、まず「読み込まれなかった」のか「より上位の値で上書きされた」のかを分けて考える。
docker compose 環境変数 優先順位 挙動 を確認するときに必要なのは、単一の順位表ではない。補間の優先順位と、コンテナ内環境変数の優先順位を分け、さらにLaravelの設定キャッシュまで含めて追跡することだ。
優先順位そのものは、開発者の意図を理解してくれない。安全にするには、起動前に最終値を可視化し、許可されない接続先なら機械的に止める仕組みを置くしかない。