Docker Compose env_file, environment and .env: Which Does What

In Docker Compose, env_file loads a file of KEY=value lines into one service's container. The environment attribute sets container variables inline and beats env_file when both name the same key. The .env file beside compose.yaml is different: Compose reads it to fill ${VAR} placeholders in the YAML itself.

Three ways to feed variables to Compose
MechanismScopeReaches the container?
.env next to compose.yamlInterpolation of ${VAR} in the Compose fileOnly if a service references it
env_file: on a serviceThat service's containerYes, every line
environment: on a serviceThat service's containerYes, and it beats env_file
--env-file on the CLIReplaces the default .env for interpolationOnly if referenced

Updated

The confusion comes from the name: a file called .env and an attribute called env_file sound like the same thing, but one configures the Compose file and the other configures a container. Use the resolver to see which source wins for a variable, then try the interpolation forms against a set, empty or unset variable.

Which value wins?

Variable APP_MODE is set in the places ticked below.

  1. docker compose run -e
  2. environment or env_file, value interpolated from the shell or .env
  3. environment attribute (literal value): Value production
  4. env_file attribute: Value development
  5. ENV in the image: Value default

The container gets APP_MODE=production from: environment attribute (literal value)

Interpolation tester

Expression: ${VAR:-default}. VAR is set but empty.

Result: default

Every interpolation form

What each form gives for a set, empty or unset VAR
FormVAR=abcVAR emptyVAR unset
${VAR} or $VARabcemptyempty, with a warning
${VAR:-default}abcdefaultdefault
${VAR-default}abcemptydefault
${VAR:?msg}abcerror msgerror msg
${VAR?msg}abcemptyerror msg
${VAR:+other}otheremptyempty
${VAR+other}otherotherempty
$$a literal $a literal $a literal $

Compose files you can copy

env_file plus environment overrides

Shared settings come from files; the environment entries win for the keys they name. The local file is optional.

services:
  api:
    image: node:20
    env_file:
      - path: ./api.env
      - path: ./api.local.env
        required: false
    environment:
      NODE_ENV: production
      LOG_LEVEL: ${LOG_LEVEL:-info}
      SHOW_DOCS: "false"

Project .env used only for interpolation

These two values fill placeholders in compose.yaml. Neither reaches a container unless a service mentions it.

# .env (next to compose.yaml)
POSTGRES_TAG=16
WEB_PORT=8080

Using those values in compose.yaml

The image tag and the published port come from .env; the password must be set or Compose stops with a clear message.

services:
  db:
    image: postgres:${POSTGRES_TAG:-16}
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
  web:
    image: nginx
    ports:
      - "${WEB_PORT:-80}:80"

Check what Compose resolved

config prints the final model with every variable filled in; --environment lists the values used for substitution.

docker compose config
docker compose config --environment
docker compose --env-file ./staging.env config

The precedence list, top to bottom

Docker documents five levels. A value passed with docker compose run -e is strongest. Next comes environment or env_file where the value is interpolated from your shell or an environment file, then a literal value under environment, then a value from env_file, and last an ENV line baked into the image.

Notice that only one of these is the .env file, and only indirectly: it feeds level two through interpolation. A value sitting in .env that no service mentions never appears inside a container.

Keys without values

A bare name under environment, such as - API_TOKEN with no equals sign, copies the value from the shell running Compose. If the shell does not have it, the variable is left unset.

In an env_file, a line holding only a key unsets that variable and removes it from the container environment, which is handy for deliberately clearing something the image sets.

How it works

  1. Tick each place where your variable is set and type the value it has there.
  2. The resolver walks Docker's documented precedence list from the top and shows which value the container ends up with.
  3. In the interpolation box, pick a form such as ${VAR:-default}, choose whether VAR is set, empty or unset, and read the result.
  4. Run docker compose config in your project to confirm what Compose actually resolved.

Tips

Frequently asked questions

What is the difference between env_file and environment?

Both put variables into a service's container. env_file reads them from one or more files, environment lists them in compose.yaml. When the same key appears in both, the environment value wins, even if it is empty.

Does Compose pass my .env file into containers automatically?

No. The project .env file is only used to substitute ${VAR} in the Compose file. A variable reaches a container only when a service names it under environment, or loads the file with env_file.

Where does Compose look for the .env file?

In the project directory, which is the folder given with --project-directory, else the folder of the first -f file, else your current folder. Use --env-file to point at another file; with several, later files override earlier ones.

Are env_file paths relative to where I run the command?

No. Relative paths in env_file are resolved from the folder that holds the Compose file, so the stack works no matter where you start it.

What happens to ${VAR} when VAR is not set?

Compose prints a warning and substitutes an empty string. Use ${VAR:-fallback} to supply a default, or ${VAR:?message} to stop with an error instead.

Related: .env file format, Next.js environment variables, Deploy a React app, Git cheat sheet.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap