Skip to content
Go back

Docker Compose Environment Variable Precedence

By SumGuy 4 min read
Docker Compose Environment Variable Precedence

The Three-Way Fight

You’ve got an env var set in your .env file. You’ve also got it in your docker-compose.yml under environment:. And maybe your host system has it too. When the container starts, which one wins? Spoiler: it’s probably not the one you think.

This is the kind of mistake that only shows up on a Tuesday in production when someone deployed the stack differently than you tested it locally.

The Precedence Order (Highest to Lowest)

This is the actual order Docker Compose uses:

  1. Command-line overridedocker-compose run --env VAR=value
  2. Shell environment variablesexport VAR=value then run compose
  3. .env file in the Compose directory.env next to docker-compose.yml
  4. env_file: in the Compose file — explicitly loaded files
  5. environment: in the Compose file — hardcoded values

Let me break down what that means in practice.

The .env File Gotcha

You’ve got this setup:

.env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DEBUG=false
docker-compose.yml
version: '3.8'
services:
app:
image: myapp:latest
environment:
DATABASE_HOST: ${DATABASE_HOST}
DATABASE_PORT: ${DATABASE_PORT}
DEBUG: "true"

What happens?

The hardcoded DEBUG: "true" in your Compose file overrides the .env setting. This trips people up constantly.

When Your Shell Environment Wins

You’re on your dev machine. You do:

Terminal window
export DATABASE_HOST=prod.example.com
docker-compose up

Your shell environment is higher priority than .env. The container gets prod.example.com even though .env says localhost.

This is actually useful for overrides, but it’s dangerous if you’re not careful. You might forget you exported something six hours ago and wonder why your containers are connecting to the wrong database.

Check what you’ve got set:

Terminal window
env | grep DATABASE

Explicit env_file: Loading

You can load a specific file:

version: '3.8'
services:
app:
image: myapp:latest
env_file: .env.production
environment:
DEBUG: "false"

This loads variables from .env.production, but anything in environment: still wins.

Load multiple files in order:

env_file:
- .env
- .env.local
- .env.production

Files are loaded left-to-right, so .env.production overwrites .env.local, which overwrites .env.

Then environment: wins over all of them.

The Real-World Disaster

You’re deploying to production. Your team has:

# docker-compose.yml (checked in)
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: "dev_password_123"

Someone forgot to create a .env file on the prod server. Container starts with dev_password_123.

Meanwhile, your security audit finds a hardcoded dev password in your database.

Better approach:

version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Then create .env (or .env.local and .gitignore it):

Terminal window
# .env.local (gitignored)
POSTGRES_PASSWORD: "real_secure_password_here"

Or use env_file::

version: '3.8'
services:
db:
image: postgres:15
env_file: .env.secrets
environment:
POSTGRES_DB: myapp

Debugging What Actually Got Set

Want to see what your container actually received?

Terminal window
docker-compose exec app env | sort

This shows every env var inside the running container. Compare it to what you expected:

Terminal window
docker-compose config

This outputs the effective Compose config after all variable substitution. If ${VAR} isn’t being replaced, you’ll see it here and know to check precedence.

Best Practices

  1. Never hardcode secrets in docker-compose.yml. Always use ${VAR} and load from .env or env_file:.

  2. Use .env for local dev, .env.production for prod. Compose will look for .env by default, but you can specify others with env_file:.

  3. Check docker-compose config before deploying. See what actually got substituted.

  4. Export shell vars only when you need them. They shadow everything else, which is great for quick overrides but terrible for accidental pollution.

  5. Be explicit about precedence. If you want to guarantee a value doesn’t get overridden, hardcode it in environment:. If you want it to be overridable, use ${VAR} and set it elsewhere.

Compose V2 and —env-file

Docker Compose V2 added --env-file flag to load a specific file instead of .env:

Terminal window
docker-compose --env-file .env.staging up -d

This replaces the default .env lookup with .env.staging. Useful for environment-specific deployments without editing files:

Terminal window
# Dev
docker-compose --env-file .env.local up -d
# Staging
docker-compose --env-file .env.staging up -d
# Production
docker-compose --env-file .env.production up -d

Note: this flag controls variable substitution in the Compose file itself. The env_file: key inside services still works separately.

Check Before You Deploy

Before every deploy, run:

Terminal window
docker-compose config

This prints the final resolved Compose config with all variables substituted. If you see ${VAR} anywhere, that variable isn’t getting set — and your container will start with a blank or broken value.

This stuff seems simple until it’s 2 AM and your containers are connecting to the wrong database because a shell variable from yesterday’s testing session is still set.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Systemd Timers vs Cron: Scheduling Tasks Like It's Not 1995
Next Post
Immich vs PhotoPrism: Self-Hosted Google Photos That Won't Sell Your Memories

Related Posts