Skip to main content

Docker Compose .env Not Loading: Where the Variable Actually Goes

The usual cause is not a file that failed to load. It is a file that loaded into a different place than expected: .env supplies variables to the compose file itself, and env_file supplies them to the container. A variable can be visibly working in one and completely absent in the other.

Two channels, not one

Almost every “.env not loading” report is one of these two being mistaken for the other. They have different sources, different destinations, and a variable in one is invisible to the other.

ChannelComes fromFeedsWhere you see it
Interpolation.env in the project directory, and the shell environmentThe compose file itself — ${VAR} in image, ports, volumes, anywhereAppears in the resolved compose file, not in the container
Container environmentenv_file: and environment: on the serviceThe environment variables the process in the container can readAppears under environment: in the resolved compose file

The demonstration

Three files. Watch where each variable ends up.

.env

FROM_DOTENV=dotenv-value
TAG=v1
SHARED=from-dotenv

svc.env

FROM_ENVFILE=envfile-value
SHARED=from-envfile

compose.yaml

services:
  app:
    image: alpine:${TAG}
    env_file: svc.env
    environment:
      SHARED: from-environment

Running docker compose config resolves it:

resolved

services:
  app:
    environment:
      FROM_ENVFILE: envfile-value
      SHARED: from-environment
    image: alpine:v1

TAG from .env became the image tag. FROM_DOTENV is nowhere in the container environment. The file loaded — it just does not feed the container.

SHARED was set in all three places and came out as from-environment, which is the precedence rule below in one line.

What was measured

ScenarioResultTells you?
FROM_DOTENV is in .env, service has no env_file entry for itNot present in the container environment. No warning.Silent
TAG=v1 in .env, but TAG is exported in the shellimage: alpine:from-shell — the shell wins over .envSilent
SHARED set in both env_file: and environment:SHARED: from-environment — environment: winsSilent
${NOPE} is referenced and set nowhereWarning, then image: "alpine:" — a blank string, not an errorSays something
Running from a subdirectory that has its own .env, with -f ../compose.yamlThe subdirectory .env is ignored; the one beside the compose file is usedSilent
--env-file only.env, where only.env omits TAGTAG is unset. The flag replaces .env rather than adding to it.Says something
env_file: points at a file that is not thereHard error naming the resolved path. This one is easy.Says something

The silent rows are the ones that cost an afternoon. An exported shell variable overriding .env is the cruellest, because the file you are staring at is correct and is simply not the value being used.

Precedence, in both channels

For ${VAR} in the compose file:

  1. 1. Shell environment (highest — an exported variable beats the file)
  2. 2. --env-file if given, otherwise .env in the project directory
  3. 3. Values used for ${...} interpolation in the compose file

For what the process inside the container can read:

  1. 1. environment: on the service (highest)
  2. 2. env_file: on the service
  3. 3. Nothing else — .env does not reach here on its own

A key under environment: with no value — PASSTHRU: on its own — means take it from the shell environment at the moment the command runs. If it is unset there, it stays unset.

Where the files are looked for

.env is read from the project directory — beside the compose file by default, not from whatever directory you happen to be standing in. Running with -f ../compose.yaml from a subdirectory that has its own .env used the one beside the compose file and ignored the local one entirely.

Paths under env_file: resolve relative to the compose file too. And --env-file replaces .env rather than adding to it: pointing it at a file that omits a variable leaves that variable unset, even though .env is sitting right there with a value in it.

Passing a value through

To pass a value from .env through to the container, name it on the service. Either list the file under env_file:, or bridge it explicitly with SOME_VAR: ${SOME_VAR} under environment:.

services:
  app:
    environment:
      FROM_DOTENV: ${FROM_DOTENV}   # interpolation -> container

That line is doing something specific: the right-hand side is interpolation reading .env, and the left-hand side is the container variable. It looks redundant and is exactly the bridge between the two channels.

The one command worth running

docker compose config resolves the whole file locally and prints the result: the interpolated values and the exact environment each service will receive. Reading that output answers the question directly, without starting anything.

docker compose config

Every result on this page came from that command. If the variable is missing from its output, it will be missing in the container — and you have found the answer before waiting for anything to build.

Checking the file itself

If the routing is right and the value still looks wrong, the remaining suspect is the syntax of the file. The ENV Formatter parses pasted .env text and shows the key and value it read, which settles questions about quotes and stray spaces.

None of this can be checked from a web page. Nothing here reads your Docker daemon, your compose file, or your shell — the tool below only parses .env text you paste into it, and the resolution rules above come from running Compose locally.

Related

This page is about a variable that does not arrive. If it arrives with the wrong value — quotes kept, a # truncating it, $VAR expanded when you did not want it — why the same .env file behaves differently compares how each consumer parses the same file.

Measured with

  • Docker Compose5.3.1
  • Docker CLI29.6.2
  • Methoddocker compose config

Resolution was measured with docker compose config, which computes interpolation, env_file and precedence locally. The environment it prints for a service is what Compose passes to that service; no container was started to produce these results.