Docker Compose 環境変数が反映されない6つの原因
compose.yaml の隣に .env を置いて docker compose up を実行したのに、コンテナの中の変数が空のまま、あるいは古い値のまま。ファイルにはちゃんと書いてあるのに、Compose が「設定されていない」と言ってくる。これは一つのバグではありません。原因は少なくとも六つあり、外から見ても同じようには見えません。
Docker Compose v5.5.1 で、docker compose config(と docker compose config --format json)を使って一つずつ確かめました。このコマンドは compose ファイルと .env を up とまったく同じように解釈しますが、デーモンを起動しなくても動きます。以下のコマンドと出力はすべて実際のターミナルからコピーしたもので、記憶で書いたものではありません。
結論(Docker Compose v5.5.1、2026-09-29 に検証):
| 症状 | 原因 | 対処 |
|---|---|---|
.env に変数は書いたのに、コンテナには入っていない | .env は compose ファイルの中の ${VAR} 展開に使われるだけで、コンテナに自動で渡されるわけではない | environment: か env_file: で明示的にコンテナへ渡す |
.env はそこにあるのに、何も置き換わらない | .env はカレントディレクトリではなくプロジェクトディレクトリ(最初の -f のファイルがあるディレクトリ、または --project-directory)から読まれる | .env を compose ファイルの隣に移すか、--project-directory を渡す |
--env-file custom.env が別のファイルを拾う | このパスはカレントディレクトリ基準で、env_file:(compose ファイル基準)とは逆 | 絶対パスを使うか、カレントディレクトリを確認する |
env_file: の値で思ったとおりに上書きされない | environment: は常に env_file: に勝ち、後に書いた env_file: が先のものに勝つ | 順番を入れ替えるか、上書きしたい値を environment: に移す |
空の変数で ${VAR:-default} と ${VAR-default} の結果が違う | :- は「設定済みだが空」のときも発動し、- は完全に未設定のときだけ発動する | 意図してどちらかを選ぶ |
config の出力が price$$5 で、本当に $ が二つなのか確信が持てない | config は表示するときにリテラルの $ を $$ に戻して出力する | デーモンなしではコンテナ内の実際の値を確認できなかった。下の注意書きを参照 |
まず:Compose が .env を読んでいるか確かめる
プロジェクトディレクトリで次を実行します。コンテナが動いている必要はありません。
$ docker compose config
.env から来るはずの変数が、この出力のどこにも正しく置き換わった形で出てこないなら、その先のどこにも届いていません。以下の内容はすべて、このコマンドか docker compose config --format json で確認しています。
原因1:.env は変数展開に使われるだけで、コンテナの変数にはならない
いちばん多い誤解で、しかも誤解しやすいものです。.env と env_file: はどちらも名前に「env」が入っているからです。
compose ファイルには image: nginx:${TAG} 以外は何も書かず、.env は TAG=1.25 だけ。docker compose config を実行するとこうなります。
name: exp1_interpolation
services:
web:
image: nginx:1.25
networks:
default: null
出力に environment: ブロックがまったく無いことに注目してください。.env がした仕事は一つだけで、YAML の中の ${TAG} を埋めたことです。コンテナの中のアプリが process.env.TAG を読んでも何も入っていません。TAG をコンテナの環境に入れるよう Compose に指示したものは、何も無いからです。
変数を実際にコンテナへ入れるには、次のどちらかが必要です。
services:
web:
environment:
- TAG # reads TAG the same way ${TAG} does: shell first, then .env
または
services:
web:
env_file:
- app.env # loads every key in app.env into the container, unconditionally
environment: - TAG という値なしの書き方は、あえて個別に確かめました。シェルしか見ないと思い込みがちだからです。実際はそうではありません。シェル変数が無い状態では .env から TAG=from-dotenv をそのまま読み、シェル変数を設定するとそちらで上書きされました。解決順序は ${TAG} の展開と同じで、書き方が違うだけです。
env_file: は別物です。env_file: [app.env] は app.env の二つのキーを両方コンテナに入れ、プロジェクト直下の .env にある同名のキーは無視されました。env_file: は .env をまったく参照せず、指定したファイルだけを読みます。
原因2:.env はカレントディレクトリではなく、プロジェクトディレクトリに置く
これはテスト環境を作っている最中に自分でもはまりました。Compose が .env を探すのはプロジェクトディレクトリです。--project-directory を渡せばそのディレクトリ、それ以外なら最初の -f/--file の compose ファイルがあるディレクトリ、さらにそれ以外ならカレントディレクトリで決まります。コマンドを実行したときにたまたまいる場所ではありません。
compose ファイルを proj/ に置き、隣の other_cwd/ から -f ../proj/compose.yaml で実行しました。proj/ にだけある .env は正しく使われました。
$ (cd other_cwd && docker compose -f ../proj/compose.yaml config)
image: nginx:in-proj-dir
次に、別の .env を other_cwd/(コマンドを実行している場所のすぐ隣)に置いて、まったく同じコマンドをもう一度実行しました。結果は同じで、other_cwd/.env は一度も読まれていません。
$ (cd other_cwd && docker compose -f ../proj/compose.yaml config)
image: nginx:in-proj-dir
デプロイ用スクリプトや Makefile のターゲットが compose.yaml とは別のフォルダにあって、そこから Compose を呼び、「便利だから」とスクリプトの隣に .env を置いた場合、それは黙って無視されます。Compose は警告を出さず、変数は compose ファイルに書かれたデフォルト値に戻るだけです。
--project-directory は .env を読む場所を変え、デフォルトのプロジェクト名も変えます。
$ (cd proj && docker compose --project-directory ../other_cwd -f compose.yaml config)
name: other_cwd
image: nginx:in-othercwd-dir
原因3:--env-file と env_file: は相対パスの基準が違う
この二つは見た目がほぼ同じなのに動きが違います。この記事でいちばん役に立つ点を一つ挙げるなら、ここです。
env_file:(compose.yaml の中の YAML 属性) は、相対パスをcompose ファイル自身のディレクトリ基準で解決します。proj/compose.yaml に env_file: [config/app.env] と書き、まったく別のディレクトリから -f ../proj/compose.yaml で実行しても、proj/config/app.env を正しく見つけました。
$ (cd elsewhere && docker compose -f ../proj/compose.yaml config)
environment:
FOO: from-proj-config
--env-file(CLI のフラグ) は、相対パスをコマンドを実行したディレクトリ基準で解決します。other_cwd/ から、proj/ にある compose ファイルを指定した場合:
$ (cd other_cwd && docker compose -f ../proj/compose.yaml --env-file custom.env config)
name: proj
image: nginx:from-custom-env-file
custom.env は proj/(プロジェクトディレクトリ)ではなく other_cwd/(カレントディレクトリ)から読まれました。しかもプロジェクト名は proj と表示されたままです。ローカルのシェル、CI、ラッパースクリプトなど、実行する場所が変わると、相対パスの --env-file は別のファイルを指しうる。一方、env_file: の中の同じ相対パスは動きません。
原因4:優先順位――シェルが勝ち、environment: は env_file: に勝つ
変数展開では、シェルの環境変数が .env に勝ちます。
$ unset TAG; docker compose config | grep image
image: nginx:from-dotenv
$ TAG=from-shell-env docker compose config | grep image
image: nginx:from-shell-env
コンテナに最終的に入る値では、environment: が env_file: に勝ちます。同名のシェル変数を設定していても同じでした。environment: ブロックは値を直接書いていて、${...} の参照ではなかったからです。
env_file: [base.env] # COLOR=red
environment:
COLOR: green
$ COLOR=fromshell docker compose config --format json
"COLOR": "green"
これは Docker の優先順位のページ(2026-09-29 確認)と一致します。高い順に、docker compose run -e、次にシェルや .env から展開した値を使う environment:/env_file:、次に値を直接書いた environment:、次に env_file:、最後にイメージ自身の ENV です。
env_file: を複数並べた場合、後のファイルが前のファイルをキーごとに上書きし、片方のファイルにしかないキーはそのまま残ります。
env_file: [base.env, override.env] # base: COLOR=red, SIZE=small; override: COLOR=blue
"COLOR": "blue",
"SIZE": "small"
原因5:.env の書式のクセ――ほとんどは問題なし、ただし # の規則だけはすぐには分からない
.env にいろいろな境界ケースを試しました。引用符、空白、コメント、export、CRLF の改行、UTF-8 の BOM。ほとんどはそのまま動きました。
KEY = value with spaces:=の前後の空白は両側とも取り除かれる。KEY="quoted value"とKEY='quoted value':引用符は正しく外される。export KEY=value:exportという接頭辞(シェルの設定ファイルからコピーするとよく付いてくる)は認識されて外される。- 空行と、行全体の
# commentsは読み飛ばされ、次の行には影響しない。 - CRLF の改行(
KEY=value\r\n)でも、値の末尾に余計な\rは残らなかった。 - ファイル先頭の UTF-8 BOM で、最初の変数名が壊れることはなかった。
すぐには分からないことが一つあります。# がコメントの始まりになるのは、前に空白があるときだけです。KEY=abc#nospace は # を含めた文字列全体がそのまま残り、KEY=abc #comment は abc に切り詰められます。値を引用符で囲めば、どちらの場合も # は残ります。
ダブルクォートの値は、中の変数も再帰的に展開されます。BASE=core と NESTED="prefix-${BASE}-suffix" なら prefix-core-suffix になります。シングルクォートでは展開されず、NESTED='prefix-${BASE}-suffix' は ${BASE} を文字どおり残します。
原因6:$$ は置き換えを止めるが、config の表示はうのみにしない
$$ はリテラルの $ を書く方法として(Compose Spec に、2026-09-29 確認)記載されています。置き換えを止める部分は確認できました。.env で TAG を設定していても、$$TAG は置き換えられず、docker compose config が出力したのは $$TAG のままで、TAG の値ではありませんでした。
確認できていないのは、動いているコンテナが実際に何を受け取るかです。docker compose config は出力するときにリテラルの $ を $$ に戻してエスケープし直し、その出力をもう一度 Compose に通しても正しく読めるようにしています。これは直接見ました。リテラルの ${BASE} を含むシングルクォートの .env の値が、config からは prefix-$${BASE}-suffix として返ってきました。元のファイルには無い $ が一つ増えています。なので config の出力にある price$$5 は、おそらくコンテナには price$5(ドル記号は二つではなく一つ)として渡っています。ただ、それを確かめるには Docker デーモンが必要で、今回はあえて起動していません。これを前提にする前に、docker compose run --rm <service> env で確かめてください。
${VAR:-default} と ${VAR-default}、それに ${VAR?err}
.env に EMPTY_VAR=(空文字列を設定)と SET_VAR=actual-value を書いた場合:
| 式 | 結果 |
|---|---|
${UNSET_VAR:-fallback}(未設定) | fallback |
${EMPTY_VAR:-fallback}(設定済み、空) | fallback |
${EMPTY_VAR-fallback}(設定済み、空、コロンなし) | “(空文字列。フォールバックにはならない) |
${SET_VAR:-fallback}(設定済み、中身あり) | actual-value |
コロンの有無が効きます。:- は「空に設定されている」を「未設定」と同じに扱います。コロンが無ければ、明示的に空にした変数は空のままです。
${VAR?message}(または ${VAR:?message})は、置き換える代わりにエラーで失敗します。
$ docker compose -f compose2.yaml config
error while interpolating services.web.environment.WITH_REQUIRED: required variable REQUIRED_VAR is missing a value: custom error message
終了コードは 1 でした。「必須の変数を誰かが設定し忘れた」を CI で安く検出するのに使えます。何かが起動しようとする前に失敗してくれます。
COMPOSE_PROJECT_NAME
デフォルトのプロジェクト名はディレクトリ名から作られ、正規化されます(小文字にし、空白と記号を取り除く)。My Weird Dir! というディレクトリは myweirddir になりました。優先順位は高い順に、-p フラグ、シェルの COMPOSE_PROJECT_NAME、.env の中の COMPOSE_PROJECT_NAME、最後にディレクトリ名からのデフォルトです。一つ設定してから、それより優先度の高いものを重ねる、という形で一つずつ確かめました。
確認する順番
docker compose configを実行し、出力のどこかに変数が置き換わった形で出ているかを見る。出ていない、または置き換わっていないなら、ほかのことはまだ気にしなくていい。- コマンドを実行している場所と、
compose.yamlがある場所を見比べる。-f ../something/compose.yamlに頼っているなら、.envはシェルのプロンプトの隣ではなく、その compose ファイルの隣に置く。 --env-fileを使っているなら、そのパスはenv_file:の規則ではなく、カレントディレクトリ基準。- 意図して選ぶ:
${VAR}(compose ファイルの中だけで効き、コンテナに届けるにはenvironment:/env_file:が要る)か、env_file:(ファイルの中身をすべてそのままコンテナに入れる)か。 - 二つの出どころで値が食い違うなら:
environment:はenv_file:に勝ち、後のenv_file:は前のものに勝つ。
検証環境と、この記事で扱っていないこと
Docker Compose v5.5.1(docker compose version)、Docker CLI 29.8.0、macOS 15.3.1、2026-09-29 に検証。上の内容はすべて docker compose config/docker compose config --format json で確認しました。このコマンドはデーモンが動いていなくても compose ファイルと .env を解釈します。docker compose up、docker compose run の実行も、Docker Desktop の起動も、あえてしていません。なので、コンテナが動いて初めて分かること(とりわけ原因6の $$ の件)は、推測で書かずに未確認と明記しています。
未検証:Windows と Linux のホスト、古い Compose(v1、初期の v2)、docker-compose.override.yaml のマージの挙動。
2026-09-29 に確認した公式ドキュメント:Docker の variable-interpolation、set-environment-variables、envvars-precedence の各ページと、GitHub にある Compose Spec の interpolation のページ。ドキュメントに書かれていない細部をこの記事で補ったところ(--env-file と env_file: のパス基準の違い、config が表示時に $$ をエスケープし直すこと)は、この記事独自の補足だと明記しています。