Toolarity

Docker Compose 環境変数が反映されない6つの原因

トラブル対処Docker Compose 公開 2026年10月2日

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、最後にディレクトリ名からのデフォルトです。一つ設定してから、それより優先度の高いものを重ねる、という形で一つずつ確かめました。

確認する順番

  1. docker compose config を実行し、出力のどこかに変数が置き換わった形で出ているかを見る。出ていない、または置き換わっていないなら、ほかのことはまだ気にしなくていい。
  2. コマンドを実行している場所と、compose.yaml がある場所を見比べる。-f ../something/compose.yaml に頼っているなら、.env はシェルのプロンプトの隣ではなく、その compose ファイルの隣に置く。
  3. --env-file を使っているなら、そのパスは env_file: の規則ではなく、カレントディレクトリ基準。
  4. 意図して選ぶ:${VAR}(compose ファイルの中だけで効き、コンテナに届けるには environment:/env_file: が要る)か、env_file:(ファイルの中身をすべてそのままコンテナに入れる)か。
  5. 二つの出どころで値が食い違うなら: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 が表示時に $$ をエスケープし直すこと)は、この記事独自の補足だと明記しています。