- Docker Compose env_file: When to Use vs environment Variables
Docker Compose env_file: When to Use vs environment Variables
Precedence rules, security trade-offs, and when to use each approach

🐳 Docker & DevOps Implementation Guides
Complete Docker guides with optimization techniques, deployment strategies, and automation prompts to streamline your containerization workflow.
Related Posts:
Early on, I kept bumping into the same frustrating problem: I'd set a variable in my .env file, Docker Compose would seem to ignore it, then I'd add it under environment: and it'd suddenly work — but then something else would break. I didn't understand the system.
Turns out Docker Compose doesn't have one mechanism for environment variables. It has three, and they interact in a specific precedence order that's non-obvious until you've been burned by it.
This guide covers all three mechanisms, how they rank against each other, the --env-file CLI flag most developers don't know about, when to switch to Docker Secrets instead, and the fixes for the most common failure modes.
TL;DR — Quick reference
| Mechanism | Where defined | Interpolation support | Priority |
|---|---|---|---|
docker compose run -e | CLI at runtime | Yes (from shell) | Highest |
environment: with interpolated value | compose.yml | Yes — reads from shell / .env | 2nd |
environment: with plain literal | compose.yml | No | 3rd |
env_file: directive | compose.yml → external file | No (inside the file) | 4th |
Image ENV directive | Dockerfile | No | Lowest |
The Three Mechanisms
1. environment: — inline in compose.yml
Define variables directly in your compose file:
services:
app:
environment:
- DB_HOST=postgres
- DB_PORT=5432
# Or mapping syntax:
API_URL: http://localhost:${HOST_PORT}
The key feature here is interpolation: values can reference shell variables or variables from your .env file using ${VAR} syntax. If HOST_PORT=3000 is set in your shell or .env, the above resolves to http://localhost:3000 at runtime.
Use environment: for:
- Service-specific config that doesn't need to be shared across services
- Values that depend on shell interpolation
- Overrides on top of a shared
env_file:
The downside: literal secrets in your compose file end up in version control. Never put passwords or tokens here as hardcoded values — use interpolation from a git-ignored .env file instead.
2. env_file: — load from external files
Tell Compose to load variables from one or more external files:
services:
app:
env_file:
- ./common.env
- ./app.env
The file format is simple — one KEY=value per line, # for comments:
# common.env DB_HOST=postgres DB_PORT=5432 REDIS_URL=redis://cache:6379
Multiple files and load order: files listed later override those listed earlier. So if both common.env and app.env define DB_HOST, the value from app.env wins.
The required field (Compose v2.24.0+): by default, Compose throws an error if a listed env file doesn't exist. You can make files optional:
env_file:
- path: ./default.env
required: true # default — error if missing
- path: ./override.env
required: false # silently skip if missing
Useful for environment-specific overrides that may not exist in all deployment contexts.
The interpolation limitation: ${VAR} syntax does not work inside .env files. If you write API_URL=${BASE_URL}/api inside an env file, Compose treats it as a literal string — BASE_URL won't expand. Use environment: in the compose file for anything that needs interpolation.
3. The automatic .env pickup
This is the one that catches most people. Docker Compose automatically loads a .env file from your project root (next to your compose.yaml) without any configuration. You don't need to declare it anywhere.
This file serves a different purpose from env_file: — it's primarily a source for variable interpolation inside compose.yaml itself, not a direct injection into the container environment.
# .env — auto-loaded from project root HOST_PORT=3000 DB_PASSWORD=secret
# compose.yaml — uses values from .env via interpolation
services:
app:
ports:
- "${HOST_PORT}:3000"
environment:
- DB_PASSWORD=${DB_PASSWORD}
If you don't specify --env-file and haven't set COMPOSE_DISABLE_ENV_FILE=true, this automatic loading always happens. It's documented behavior, not a side effect.
The automatic .env file is not the same as the env_file: directive. The .env auto-load provides variables for interpolation in compose.yaml. The env_file: directive injects variables directly into the container's environment. Both work simultaneously — they serve different purposes.
The Full Precedence Order
When the same variable is defined in multiple places, Docker Compose resolves it in this order (highest wins):
docker compose run -e VAR=value— CLI flag at runtimeenvironment:orenv_file:with the value interpolated from shell or.envenvironment:with a plain literal valueenv_file:directiveENVin the Dockerfile
A concrete example — DB_HOST defined in three places:
# .env (project root, auto-loaded) DB_HOST=from-dotenv
# compose.yaml
services:
app:
env_file:
- ./app.env # contains: DB_HOST=from-env-file
environment:
- DB_HOST=from-environment
Result: the container sees DB_HOST=from-environment because environment: (level 3) beats env_file: (level 4).
Run docker compose run -e DB_HOST=from-cli app bash and the container sees from-cli — level 1 beats everything.
The --env-file CLI Flag
Most developers know about the env_file: directive in compose.yaml. Fewer know about the --env-file flag on the CLI itself — and they're different things.
# Override the default .env with a specific file at runtime
docker compose --env-file ./config/.env.staging up
This flag replaces the automatic .env pickup for that command — it doesn't merge with it. It changes which file Compose uses as the interpolation source, not which variables get injected into containers.
You can pass multiple --env-file flags in one command. Later files override earlier ones:
docker compose --env-file .env --env-file .env.override up
The practical use case: switching between environments without editing files.
# Development
docker compose --env-file envs/dev.env up
# Staging
docker compose --env-file envs/staging.env up
Each file sets different values for DB_HOST, API_URL, etc. — same compose file, different environments, no file editing required.
--env-file (CLI flag) changes the interpolation source for compose.yaml. env_file: (compose.yaml directive) injects variables into the container. They look similar but operate at different layers.
When to Use Docker Secrets Instead
Env files work fine for most config. But there's a security boundary worth knowing: environment variables are visible in docker inspect.
Run docker inspect <container> on any running container and you'll see the full Env array in the output — including anything you passed via environment: or env_file:. For non-sensitive config this is fine. For database passwords, API keys, and tokens in production, it's a problem.
Docker Compose supports secrets, which are mounted as files under /run/secrets/<n> inside the container rather than exposed as environment variables:
services:
app:
image: myapp:latest
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Your app reads the secret from /run/secrets/db_password at runtime.
env_file vs secrets — when to use which:
env_file: | Docker Secrets | |
|---|---|---|
Visible in docker inspect | Yes (Env array) | No (mount only) |
| File format | KEY=value | Raw value (entire file) |
| Local dev complexity | Low | Low (Compose v2) |
| Production (Swarm/K8s) | ⚠️ Avoid for secrets | ✅ Native support |
| Best for | Non-sensitive config | Passwords, tokens, keys |
For local development, the overhead difference is small. For production, if you're using Docker Swarm or Kubernetes, secrets are the right tool for anything sensitive.
Troubleshooting: env_file Not Working
Three failure modes come up constantly.
1. Quoted values are treated as literals
This is the most common one. In a .env file, quotes are not string delimiters — they're literal characters.
# This sets DB_PASSWORD to: "secret" (with the quotes) DB_PASSWORD="secret" # This sets DB_PASSWORD to: secret (correct) DB_PASSWORD=secret
If your app is receiving "secret" instead of secret, this is why. Strip the quotes.
2. Path is relative to the compose file, not your terminal
The path in env_file: resolves relative to the compose.yaml location, not your current working directory. If you run docker compose from a parent directory, paths that look correct may not resolve.
# This resolves relative to where compose.yaml lives
env_file:
- ./config/app.env
If in doubt, use an absolute path or always run docker compose from the same directory as your compose.yaml.
3. UTF-8 BOM on Windows-created files
If you created the .env file on Windows (Notepad, certain editors), it may have a UTF-8 BOM (byte order mark) at the start of the file. Docker Compose can't parse this — the first variable name gets garbled or the file fails silently.
Fix: open the file in VS Code, check the encoding indicator in the bottom-right corner, and re-save as UTF-8 (not UTF-8 with BOM).
The Power Combo
The most maintainable pattern combines all three mechanisms in the right roles:
services:
app:
env_file:
- ./common.env # shared config across services
- path: ./local.env
required: false # optional local overrides
environment:
- NODE_ENV=${NODE_ENV} # interpolated from .env or shell
- DEBUG=false # service-specific, non-sensitive
# .env (git-ignored, project root) NODE_ENV=development HOST_PORT=3000
# common.env (can be committed if non-sensitive) DB_HOST=postgres DB_PORT=5432 REDIS_URL=redis://cache:6379
The .env provides interpolation values for the compose file. common.env holds shared non-sensitive config. environment: handles service-specific values and anything needing shell interpolation. For actual secrets in production, graduate to Docker Secrets.
And for switching between dev/staging/prod: docker compose --env-file envs/staging.env up — no file editing, no mistakes.
Thanks, Matija.


