Docker Compose v2 (the docker compose CLI, written in Go and the default since Compose v1's docker-compose was retired in June 2023) offers five distinct mechanisms for environment variables, and they do not all reach the same place. Inline environment: and the env_file: directive inject values into the running container. .env (auto-loaded) and --env-file only feed ${VAR} substitution inside the Compose file itself — they never touch the container unless you wire them through environment:. The table below maps every form to its scope and where it sits in the precedence order.
| Form | What it does | Scope | Precedence |
|---|---|---|---|
| environment: (inline) | Sets vars directly on the container | Single service container | Highest when a value is set; bare "KEY:" falls back to the shell |
| env_file: (YAML) | Loads one or more files into the container | Container environment | Lower than environment: and shell; later files in the list override earlier ones |
| .env (auto-loaded) | Provides values for ${VAR} in the YAML | Compose file interpolation only | Feeds substitution; not seen by the container |
| --env-file (CLI) | Replaces .env for substitution at parse time | Compose file interpolation only | Overrides .env for substitution; not seen by the container |
| ${VAR} substitution | Expands ${VAR} in YAML using shell + .env / --env-file | Compose file only | Shell env > --env-file > .env, all resolved before parsing |
Most "why is my variable missing?" tickets come from confusing env_file: with .env: the former injects into the container, the latter only feeds Compose-file substitution. When a substitution is missing entirely, Compose v2 prints WARN[0000] The "FOO" variable is not set. Defaulting to a blank string. — that warning is your fastest signal that .env or --env-file isn't wired up the way you think. For the equivalent rules when running plain docker run -e, see the Docker environment variables guide.
TL;DR
- Two destinations:
environment:andenv_file:reach the container;.env,--env-file, and${VAR}only feed substitution inside the Compose file. - The most common "missing variable" bug is confusing
env_file:(injects into the container) with.env(Compose-file interpolation only). - Precedence, highest first: inline
environment:→ host shell →env_file:→ DockerfileENV. - A missing
env_file:path aborts the whole run — use the long form withrequired: false(Compose v2.24+, January 2024) to make a file optional.
How do I set environment variables inline in Docker Compose?
The simplest way to set environment variables in Docker Compose is directly in your docker-compose.yml:
services:
app:
image: node:20
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=postgres://db:5432/myappYou can also use the map syntax (without the dash):
services:
app:
image: node:20
environment:
NODE_ENV: production
PORT: "3000"Both formats are equivalent. The list syntax (- KEY=value) is more common in the wild. Note that NODE_ENV and PORT set here override anything the image baked in with ENV.
How does Docker Compose auto-load .env files?
Docker Compose automatically loads a file named .env in the same directory as your docker-compose.yml. Variables from this file are available for substitution in the YAML file itself — they are NOT automatically injected into containers.
Create a .env file:
# .env
POSTGRES_VERSION=16
APP_PORT=3000Reference these variables in docker-compose.yml with ${} syntax:
services:
db:
image: postgres:${POSTGRES_VERSION}
ports:
- "${APP_PORT}:5432"Important distinction: The .env file is for Compose file interpolation. To inject variables into the container's environment, use the env_file directive (next section).
Auto-loading is also switchable: set COMPOSE_DISABLE_ENV_FILE=1 to make Compose ignore the default .env entirely, or point COMPOSE_ENV_FILES=base.env,prod.env at a comma-separated list to replace it without repeating --env-file on every invocation.
What does the env_file directive do?
To inject environment variables directly into a container, use env_file:
services:
app:
image: node:20
env_file:
- ./app.env
- ./secrets.envWhere app.env contains:
# app.env
NODE_ENV=production
PORT=3000
API_KEY=sk-abc123Key behaviors:
- Variables are injected into the container's environment (visible via
docker exec <container> env) - Later files override earlier ones if the same variable appears
- Lines starting with
#are comments - Empty lines are ignored
- No quotes needed around values (quotes become part of the value)
- Multi-line values are not supported in
env_file— see the .env file syntax reference for the full quoting and parser-difference rules
When should I use the --env-file flag?
The --env-file flag replaces the default .env file for Compose file interpolation:
# Use .env.prod instead of .env for variable substitution
docker compose --env-file .env.prod up
# Combine with other flags
docker compose --env-file .env.staging up -dThis flag affects which file is used for ${} substitution in docker-compose.yml — it does NOT change what gets injected into containers via env_file.
How do I manage multiple .env files per environment?
A common pattern is maintaining separate env files per environment:
project/
├── docker-compose.yml
├── .env # Default/development values (auto-loaded)
├── .env.staging # Staging overrides
├── .env.production # Production overrides
├── app.env # App-specific vars (injected into container)
└── db.env # Database vars (injected into container)# docker-compose.yml
services:
app:
image: myapp:latest
env_file:
- ./app.env
environment:
- APP_ENV=${APP_ENV:-development}
db:
image: postgres:${POSTGRES_VERSION:-16}
env_file:
- ./db.envRun with different environments:
# Development (uses .env automatically)
docker compose up
# Staging
docker compose --env-file .env.staging up
# Production
docker compose --env-file .env.production up -dHow do I mix env sources per service?
Each service can have its own combination of inline variables and env files. If you use VS Code Dev Containers, the same per-service env layout drops straight into devcontainers — they integrate Docker Compose directly via dockerComposeFile:
services:
frontend:
image: nginx:alpine
environment:
- NGINX_PORT=80
backend:
image: node:20
env_file:
- ./backend.env
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://db:5432/app
db:
image: postgres:16
env_file:
- ./db.env
environment:
- POSTGRES_DB=myappPriority order (highest wins):
environmentvalues indocker-compose.yml- Shell environment variables on the host
env_filevalues- Dockerfile
ENVdefaults
"Host shell" means whatever the shell running docker compose has exported — export VAR=x on Linux/macOS, set or $env: on Windows (see the Windows environment variables guide for the session vs persistent distinction).
How does variable substitution work in docker-compose.yml?
Docker Compose supports shell-like variable substitution in YAML:
services:
app:
image: myapp:${TAG:-latest}
ports:
- "${PORT:?PORT must be set}:3000"
environment:
- DEBUG=${DEBUG:-false}| Syntax | Behavior |
|---|---|
| ${VAR} | Value of VAR, empty if unset |
| ${VAR:-default} | Default if VAR is unset or empty |
| ${VAR-default} | Default only if VAR is unset |
| ${VAR:?error} | Error message if VAR is unset or empty |
| ${VAR?error} | Error message only if VAR is unset |
Check what Compose will resolve:
docker compose configThis prints the fully resolved docker-compose.yml with all variables substituted. For more debugging commands, see the Docker cheat sheet.
How should I manage secrets in Docker Compose?
Never commit secrets to version control. Use a committed .env.example to document required variables (the same convention covered in the .env file guide) alongside an ignored .env:
# .env.example (commit this — documents required variables)
DATABASE_URL=
API_KEY=
JWT_SECRET=
# .env (DO NOT commit — add to .gitignore)
DATABASE_URL=postgres://user:pass@db:5432/app
API_KEY=sk-live-abc123
JWT_SECRET=super-secret-keyAdd to .gitignore:
.env
.env.production
.env.staging
*.env
!.env.exampleA connection string like DATABASE_URL embeds the password in plaintext, so keep it out of the committed file. For production, consider Docker secrets:
services:
app:
image: myapp:latest
secrets:
- db_password
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txtWhat are common pitfalls and debugging tips?
Quotes become part of the value
# WRONG — value will be "production" (with quotes)
NODE_ENV="production"
# RIGHT — value will be production
NODE_ENV=production.env vs env_file confusion
.env(auto-loaded) → substitution indocker-compose.ymlonlyenv_filedirective → injected into container environment
Missing env_file aborts the whole run
If a path listed under env_file: doesn't exist, Compose v2 fails with env file ./app.env not found: stat ./app.env: no such file or directory before any container starts. Compose v2.24 (released January 2024) added the long form to make a file optional:
services:
app:
image: node:20
env_file:
- path: ./app.env
required: false # skip silently if missing
- path: ./secrets.envUse required: false for files that only exist in some environments (e.g. a developer-only overrides.env) so the same compose.yml works in CI without stub files.
Variables not updating
# Recreate containers to pick up env changes
docker compose up -d --force-recreate
# Or rebuild if using build-time args
docker compose up -d --buildDebugging current values
# See resolved compose file
docker compose config
# See container environment
docker exec <container> env
# See what .env file is loaded
docker compose config --environmentDid Docker Compose v5 change environment variable handling?
Mostly no — and that is the point worth knowing. Compose jumped straight from v2.x to v5.0.0 in December 2025, deliberately skipping 3.0 and 4.0 so nobody confuses the CLI version with the long-obsolete version: "3" field from old compose files. The headline changes are an official SDK and build delegation to Docker Bake; every .env, env_file, and interpolation behaviour on this page works identically on v2.24+ and v5 (v5.1.4 is current as of May 2026).
One env-relevant fix did land: v5.0.2 made --env-file=~/secrets/.env expand the tilde to your home directory instead of treating it as a literal path. If a CI runner pins an older Compose — GitHub-hosted runners only moved to v2.40 in February 2026 — spell the path out absolutely.
References
- Docker Docs: Environment variables in Compose — official overview of every method covered above.
- Docker Docs: Environment variable precedence — authoritative ordering when the same variable is defined in multiple places.
- Docker Docs: Use secrets in Compose — recommended approach for production credentials instead of env vars.
- Compose file reference:
env_file— full schema for the directive, including format and per-file options. - The Twelve-Factor App: Config — the canonical argument for storing config in environment variables.