Docker Compose has two different things both called ‘env file’, and conflating them is the source of nearly every environment-variable headache. There is the .env file, which Compose reads automatically to substitute ${VARIABLES} inside your compose.yaml. And there is the env_file attribute, which sets environment variables inside a container. One configures the Compose file itself; the other configures what the running process sees. They look alike, they overlap, and knowing which is which is most of the battle.
The two are easy to mix up because both hold KEY=value lines. The distinction - and the precedence order when they conflict - is what this comes down to.
Table of contents
- The .env file: interpolation into compose.yaml
- The env_file attribute: variables inside the container
- environment vs env_file, and who wins
- Practical rules that avoid the traps
- How this fits the rest of the stack
- FAQ
The .env file: interpolation into compose.yaml
A file literally named .env next to your compose.yaml is read automatically. Its values fill in ${...} placeholders in the Compose file before anything runs:
# .env
POSTGRES_VERSION=16
APP_PORT=8080
# compose.yaml
services:
db:
image: postgres:${POSTGRES_VERSION}
web:
ports:
- "${APP_PORT}:8080"
This is variable substitution for the config file itself - image tags, port numbers, volume paths. The values do not automatically appear inside the containers; they are used to build the Compose configuration. Run docker compose config to see the file with everything substituted, which is the fastest way to check what Compose actually resolved.
The env_file attribute: variables inside the container
The env_file attribute names one or more files whose contents become environment variables in the container’s process:
# compose.yaml
services:
web:
image: myapp
env_file:
- ./web.env # these become env vars INSIDE the container
This is how your application gets DATABASE_URL, API_KEY, and the rest of its runtime configuration. It is separate from the top-level .env file, uses a path you specify (not automatic), and you can list several files, evaluated in order. This is the one you want for application secrets and settings.
environment vs env_file, and who wins
services:
web:
env_file:
- ./web.env # DEBUG=0 here
environment:
- DEBUG=1 # this wins
When the same variable is set in both, the inline environment attribute beats the env_file. The rough precedence, highest to lowest: docker compose run -e on the command line, then the environment attribute, then env_file, then any ENV baked into the image. A value set closer to the run wins over one set further away. Knowing this order is what turns ‘why is DEBUG still 1’ from a mystery into a two-second answer.
Practical rules that avoid the traps
- Use
.envfor compose-file values - image versions, ports, paths - that you want to parameterize. - Use
env_filefor container runtime config - the variables your app reads at startup. - Never commit real secrets in either file. Add
.envand*.envto.gitignoreand inject production secrets through your platform’s secret store, not a checked-in file. - Run
docker compose configwhenever a variable is not what you expect - it shows the fully-resolved configuration and ends the guessing.
The single most common bug is expecting .env values to appear inside the container. They do not, unless you also pass them through environment or env_file. Keep the two roles separate in your head and the whole system becomes predictable.
How this fits the rest of the stack
Environment configuration is deceptively simple until precedence and file roles blur together, and then a wrong value costs an hour. A platform that surfaces environment variables and secrets explicitly removes most of that ambiguity, because you can see what the running service will read. The RunxBuild hosting calculator shows a service, its database, and bandwidth as line items, and the RunxBuild dashboard is where you set environment variables and secrets and confirm what the container actually gets.
Useful related references:
- Docker Compose environment variables
- Dockerfile secrets: doing it safely
- docker exec: running commands in a container
- Docker services on RunxBuild
FAQ
What is the difference between the .env file and env_file in Docker Compose?
The .env file is read automatically and substitutes ${VARIABLE} placeholders inside your compose.yaml itself - image tags, ports, paths. The env_file attribute names files whose contents become environment variables inside the container’s process. One configures the Compose file; the other configures the running application. Values in .env do not appear inside containers unless you also pass them via environment or env_file.
Does the .env file set environment variables inside my container?
No, not by itself. The top-level .env file only fills in ${…} placeholders in the compose.yaml during configuration. To get a variable into the container, set it under the environment attribute or list a file under env_file. Expecting .env values to appear inside the container is the single most common Docker Compose environment bug.
What wins when a variable is set in both environment and env_file?
The inline environment attribute takes precedence over env_file. The broader order from highest to lowest is: docker compose run -e on the command line, then the environment attribute, then env_file, then any ENV baked into the image. A value set closer to the actual run overrides one set further away, so command-line and inline values win.
How do I check what variables Docker Compose actually resolved?
Run docker compose config. It prints the fully-resolved compose configuration with all ${…} substitutions applied, so you can see exactly which image tags, ports, and values Compose computed from your .env file and defaults. It is the fastest way to diagnose a variable that is not what you expected without starting the containers.
Should I commit my Docker Compose env files to git?
Not ones containing real secrets. Add .env and *.env patterns to .gitignore and inject production secrets through your platform’s secret store rather than a checked-in file. It is fine to commit an example file like .env.example with placeholder values so teammates know which variables are required, as long as the real values stay out of version control.