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.
Quick answer: Docker Compose env file vs .env vs environment:
.env (project root): loaded automatically. Its values feed ${VAR} interpolation inside compose.yaml. It does not, by itself, put variables into your containers.
env_file: (per service): loads the variables from the listed file(s) straight into that service's container environment.
environment: (per service): sets variables inline in the compose file, and wins over env_file: when both define the same key.
So the short version: .env fills in the blanks in your compose file, env_file: bulk-loads container variables, and environment: is for explicit per-service values and overrides.
Tip
TL;DR — Quick reference
Mechanism
Where defined
Interpolation support
Priority
docker compose run -e
CLI at runtime
n/a
Highest
environment:
compose.yml
Yes — ${VAR} resolves from shell / .env
2nd
env_file: directive
compose.yml → external file
Keep it out of the file; do it in environment:
3rd
Image ENV directive
Dockerfile
No
Lowest
The project-level .env is not in this table because it does not set container variables directly. It supplies values for ${VAR} interpolation in the compose file.
The Three Mechanisms
1. environment: — inline in compose.yml
Define variables directly in your compose file:
yaml
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:
yaml
services:app:env_file:-./common.env-./app.env
The file format is simple — one KEY=value per line, # for comments:
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:
yaml
env_file:-path:./default.envrequired:true# default — error if missing-path:./override.envrequired:false# silently skip if missing
Useful for environment-specific overrides that may not exist in all deployment contexts.
Interpolation: the reliable place for ${VAR} substitution is the compose file itself, so I keep env_file: contents as plain KEY=value lines and put anything that needs interpolation under environment:.
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
# .env — auto-loaded from project root
HOST_PORT=3000
DB_PASSWORD=secret
yaml
# compose.yaml — uses values from .env via interpolationservices: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.
Note
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 runtime
environment: in the compose file (including values interpolated from your shell or .env)
env_file: directive
ENV in the Dockerfile
Separately, for ${VAR} interpolation inside the compose file, a variable set in your shell beats the same variable in .env.
A concrete example — DB_HOST defined in three places:
Result: the container sees DB_HOST=from-environment because environment: (level 2) beats env_file: (level 3).
Run docker compose run -e DB_HOST=from-cli app bash and the container sees from-cli — level 1 beats everything.
Note that DB_HOST=from-dotenv in .env never reaches the container here. .env only matters if the compose file references ${DB_HOST}.
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.
bash
# 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:
bash
docker compose --env-file .env --env-file .env.override up
The practical use case: switching between environments without editing files.
bash
# 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.
Note
--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:
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. Quotes show up in the value
If your app receives "secret" instead of secret, the quotes were passed through as part of the value. Current Compose v2 parses quoted values in env files, but other tools reading the same file (plain docker run --env-file, shell scripts, older docker-compose v1) may keep the quotes literally. Since unquoted values work everywhere, the safest fix is to strip them:
env
DB_PASSWORD=secret
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.
yaml
# This resolves relative to where compose.yaml livesenv_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:
yaml
services:app:env_file:-./common.env# shared config across services-path:./local.envrequired:false# optional local overridesenvironment:-NODE_ENV=${NODE_ENV}# interpolated from .env or shell-DEBUG=false# service-specific, non-sensitive
# 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.
FAQ
What is the difference between .env and env_file in Docker Compose?
.env is read automatically from the project directory and supplies values for ${VAR} interpolation in your compose file. env_file: is declared on a service and loads variables directly into that container's environment. Different layers, different jobs.
Does docker compose .env pass variables into the container?
Not automatically. A variable in .env only reaches a container if you reference it as ${VAR} under environment: (or elsewhere in the compose file) or also list the file under env_file:.
Which wins, environment or env_file?
environment: wins. If the same key appears in both, the container gets the environment: value.
Can I use a different file than .env?
Yes, with docker compose --env-file <path> up. That replaces the default .env as the interpolation source for that command. For container variables, list files under env_file: instead.
Should I put secrets in an env file?
For local development it is common, as long as the file is git-ignored. In production, secrets in environment: or env_file: are visible via docker inspect, so prefer Docker secrets or a secrets manager. I cover the second option in self-hosting Infisical for secrets management.
Where to go next
Once your variables are sorted, these are the natural follow-ups: