DEV Community

Peon Sh
Peon Sh

Posted on Originally published at peon.sh

Environment Variables in Docker Compose: env_file, environment and Interpolation

The three ways Compose handles env vars, which one wins on conflicts, and how to keep secrets out of Git while staying reproducible.

Three mechanisms that look alike and are not
Compose gives you three distinct ways to get values into containers, and most env-related confusion comes from blurring them:

environment: entries in the compose file, set directly on the container; highest precedence; visible to anyone reading the file
env_file: loads KEY=value lines from a named file into the container at start
${VAR} interpolation: substitutes values into the compose file itself at parse time, from your shell or from a .env file sitting next to docker-compose.yml

The classic confusion: .env does not enter containers
The .env file next to your compose file feeds interpolation of the YAML, it is not automatically injected into any container. DB_PASSWORD=secret in .env does nothing for your app unless the compose file passes it through explicitly. The symptom is maddening: the variable exists on the host, echo shows it, and the container sees nothing.

.env (next to docker-compose.yml)

DB_PASSWORD=s3cret
# docker-compose.yml: must reference it to pass it through
services:
app:
environment:
DB_PASSWORD: ${DB_PASSWORD}   # now it reaches the container
Enter fullscreen mode Exit fullscreen mode

Precedence, definitively
When the same key appears in multiple places, the order is: values from your shell override the .env file (for interpolation); and on the container, environment: entries override env_file: entries. One subtle trap: an interpolation with no value becomes an empty string silently, use ${VAR:?err} syntax to make missing required values fail the deploy loudly instead.

environment:
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set} # fail fast
LOG_LEVEL: ${LOG_LEVEL:-info} # default value

eeping secrets out of Git
The pattern that scales: commit the compose file with ${PLACEHOLDERS} and defaults for non-secrets; never commit real values; inject them at deploy time from a secrets store. A deployment platform formalizes this, Peon stores variables encrypted at rest, renders them when deploying the stack, and offers workspace-level shared variables so one API key serves ten services without ten copies. Rotating a credential becomes: change it in one place, redeploy consumers.

Debugging what a container actually received
Stop guessing; look:

Remember the lifecycle: env changes apply on container recreation, restart alone does not re-read env_file or compose changes
And the classic Next.js/CRA trap: build-time variables (NEXT_PUBLIC_*) must exist during the image build, not just in the runtime environment

docker exec env | sort # runtime truth
docker compose config # fully interpolated YAML
docker inspect --format '{{json .Config.Env}}' | jq

Top comments (0)