Why docker compose prioritizes shell environment over .env
created: 2025-11-10
Last week I was surprised when docker compose prioritized the shell environment's values over .env. Initially I resolved this by removing the environment variable from my .bashrc. But after reading the docs (here and here) and digging into why docker behaves this way, I found a larger issue with my workflow.
Verifying my understanding
To verify I setup a small docker compose project with two variables: SHELL_AND_DOT_ENV and ONLY_DOT_ENV.
compose.yml:
services:
echoer:
build: .
environment:
SHELL_AND_DOT_ENV: ${SHELL_AND_DOT_ENV}
ONLY_DOT_ENV: ${ONLY_DOT_ENV}
Dockerfile:
FROM alpine:3.20
CMD echo "SHELL_AND_DOT_ENV is: $SHELL_AND_DOT_ENV" && \
echo "ONLY_DOT_ENV is: $ONLY_DOT_ENV"
.env:
SHELL_AND_DOT_ENV=hello-from-dot-env
ONLY_DOT_ENV=hello-from-dot-env
And running it...
$ SHELL_AND_DOT_ENV=hi-from-os docker compose up --build
...snip...
echoer-1 | SHELL_AND_DOT_ENV is: hi-from-os
echoer-1 | ONLY_DOT_ENV is: hello-from-dot-env
Understanding why
According to the interpolation docs the shell environment takes precedence to make it possible to override a variable from .env when running a command. Which, ironically, is how I decided to test this. So it's working as intended, and my mental model was mistaken.
Docker is assuming I'm following the best practice of not putting project specific variables in my global environment.
Getting the value from .env
In my (mistaken) view, I had expected SHELL_AND_DOT_ENV to say "hello-from-dot-env" because I view it as the OS environment existing, and .env being applied on top of it since it is more specific.
To get the .env to take priority, I found two ways:
Don't define variable at the shell level
First, don't have the environment variable defined in my environment:
~/code/docker-echo-test $ docker compose up --build
...snip...
echoer-1 | SHELL_AND_DOT_ENV is: hello-from-dot-env
echoer-1 | ONLY_DOT_ENV is: hello-from-dot-env
The downside of this approach is that if I later define SHELL_AND_DOT_ENV in my .bashrc, this project will be affected.
Use env_file to explicitly point to .env
Alternatively, I can define the values via env_file instead of environment in compose.yml:
services:
echoer:
build: .
env_file: .env
Which gives:
~/code/docker-echo-test $ SHELL_AND_DOT_ENV=hi-from-os docker compose up --build
...snip...
echoer-1 | SHELL_AND_DOT_ENV is: hello-from-dot-env
echoer-1 | ONLY_DOT_ENV is: hello-from-dot-env
This skips the interpolation step which is prioritizing the shell environment over .env, which solves the issue. In a more realistic compose.yml I would define a .env per service, to keep environment variables from leaking between services. For example, ServiceA doesn't need or want API keys that are only used by ServiceB.
The downside of this approach is it splits the compose.yml into more files, which can add unnecessary complexity depending on the project size. For a small project, it feels like too much. For a large project, the extra layer of organization may be helpful.
Conclusion
In my situation, I opted to stick with my original solution of removing the problematic environment variable from my .bashrc. But also... to move all my project specific variables out of my .bashrc, which is the real lesson here.
Surprisingly, digging into why docker compose was behaving differently than I expected helped me reveal a larger problem with my workflow.