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.
| Mechanism | Scope | Reaches the container? |
|---|---|---|
| .env next to compose.yaml | Interpolation of ${VAR} in the Compose file | Only if a service references it |
| env_file: on a service | That service's container | Yes, every line |
| environment: on a service | That service's container | Yes, and it beats env_file |
| --env-file on the CLI | Replaces the default .env for interpolation | Only 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.
Variable APP_MODE is set in the places ticked below.
The container gets APP_MODE=production from: environment attribute (literal value)
Expression: ${VAR:-default}. VAR is set but empty.
Result: default
| Form | VAR=abc | VAR empty | VAR unset |
|---|---|---|---|
| ${VAR} or $VAR | abc | empty | empty, with a warning |
| ${VAR:-default} | abc | default | default |
| ${VAR-default} | abc | empty | default |
| ${VAR:?msg} | abc | error msg | error msg |
| ${VAR?msg} | abc | empty | error msg |
| ${VAR:+other} | other | empty | empty |
| ${VAR+other} | other | other | empty |
| $$ | a literal $ | a literal $ | a literal $ |
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"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=8080The 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"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
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.
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.
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.
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.
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.
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.
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.
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.