The env_file key looks simple but has surprisingly rich behavior, you can load multiple files, mark some as optional, and control how they’re parsed.
The two flavors of .env
There are two completely different things called “.env” in Compose:
- The project
.envfile at the same level ascompose.yml, used for interpolation of${VAR}in the Compose file itself (see Tip #42) env_file:at service level, loaded as runtime environment variables inside the container
This post is about the second one.
Single file (basic form)
services:
app:
image: myapp
env_file: .env.app
# .env.app
DATABASE_URL=postgres://db:5432/mydb
LOG_LEVEL=info
All key-value pairs end up as environment variables in the container.
Multiple files with priority
Chain multiple env files, later files override earlier ones:
services:
app:
image: myapp
env_file:
- .env.common # Shared defaults
- .env.${ENV:-dev} # Environment-specific overrides
- .env.local # Local developer tweaks (gitignored)
Result for a missing key: nothing, as expected. For a key defined in .env.common and .env.local: the .env.local value wins.
Optional files
By default, a missing env file causes Compose to fail. Use required: false to make it optional:
services:
app:
image: myapp
env_file:
- path: .env.common
required: true # Default
- path: .env.local
required: false # Skip if missing
Great for .env.local files that each developer may or may not have.
Format option
The default format treats each line as KEY=VALUE. For more complex values (multi-line, JSON blobs, strings with special characters), use format: raw to parse the file differently, or just stick to the default format and quote your values:
services:
app:
env_file:
- path: .env.app
format: raw
For most cases, the default format is what you want.
Interpolation inside env files
Env files support ${VAR} substitution, pulling from the host environment or the project .env:
# .env.app
DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
API_KEY=${API_KEY}
As long as DB_USER, DB_PASSWORD, etc. are available on the host (or in the project .env), Compose resolves them before passing to the container.
Precedence: env_file vs environment
When both env_file and environment define the same variable, environment wins:
services:
app:
env_file:
- .env.app # LOG_LEVEL=info
environment:
LOG_LEVEL: debug # ← this wins
The running container sees LOG_LEVEL=debug. Useful for one-off overrides without editing the env file.
A practical multi-environment setup
my-app/
├── compose.yml
├── .env.common
├── .env.dev
├── .env.staging
├── .env.prod
└── .env.local # gitignored
services:
app:
image: myapp
env_file:
- .env.common
- path: .env.${ENV:-dev}
required: true
- path: .env.local
required: false
docker compose up # uses .env.dev
ENV=staging docker compose up # uses .env.staging
ENV=prod docker compose up # uses .env.prod