The Problem
Your Docker Compose setup needs database credentials, API keys, and configuration values that change between development and production. Hardcoding them in docker-compose.yml means committing secrets to git. Using the wrong approach means your containers start with missing or incorrect variables, causing silent failures that are painful to debug.
This guide covers every method for handling environment variables in Docker Compose, from simplest to most secure.
Method 1: Inline in docker-compose.yml
The most direct approach — define variables directly in the compose file:
# docker-compose.yml
services:
api:
image: myapp:latest
environment:
- NODE_ENV=production
- PORT=3000
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=appdb
You can also use the map syntax (equivalent, but allows empty values):
services:
api:
image: myapp:latest
environment:
NODE_ENV: production
PORT: "3000"
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: appdb
When to use: Non-sensitive configuration that is the same across all environments. Never use this for secrets.
Method 2: The .env File
Docker Compose automatically reads a .env file in the same directory as docker-compose.yml:
# .env
POSTGRES_USER=appuser
POSTGRES_PASSWORD=secretpass123
POSTGRES_DB=appdb
DB_HOST=postgres
REDIS_URL=redis://redis:6379
APP_SECRET_KEY=a1b2c3d4e5f6
Reference these variables in your compose file with ${VARIABLE} syntax:
# docker-compose.yml
services:
api:
image: myapp:latest
environment:
- DB_HOST=${DB_HOST}
- DB_USER=${POSTGRES_USER}
- DB_PASSWORD=${POSTGRES_PASSWORD}
- DB_NAME=${POSTGRES_DB}
- SECRET_KEY=${APP_SECRET_KEY}
postgres:
image: postgres:16
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
Critical: Add .env to your .gitignore:
echo ".env" >> .gitignore
Commit a .env.example with placeholder values so other developers know which variables are required:
# .env.example (committed to git)
POSTGRES_USER=your_db_user
POSTGRES_PASSWORD=your_db_password
POSTGRES_DB=your_db_name
DB_HOST=postgres
APP_SECRET_KEY=generate_a_random_key
Method 3: Shell Environment Variable Passthrough
Variables defined in your shell are available for interpolation:
# Set in shell
export API_VERSION=v2.1.0
# docker-compose.yml references it
# image: myapp:${API_VERSION}
services:
api:
image: myapp:${API_VERSION}
environment:
- BUILD_NUMBER=${BUILD_NUMBER}
- GIT_SHA=${GIT_SHA:-unknown}
The ${GIT_SHA:-unknown} syntax provides a default value if the variable is not set. This prevents Compose from issuing a warning.
Variable precedence (highest to lowest)
.env file values${VAR:-default} syntax# Shell overrides .env file
export POSTGRES_PASSWORD=override_password
docker compose up
# Container gets POSTGRES_PASSWORD=override_password regardless of .env
Method 4: env_file Directive
Point to one or more external env files — useful for separating concerns:
services:
api:
image: myapp:latest
env_file:
- ./config/common.env
- ./config/api.env
- ./config/secrets.env
# config/common.env
LOG_LEVEL=info
REGION=us-east-1
# config/api.env
PORT=3000
WORKERS=4
# config/secrets.env
DB_PASSWORD=secret123
API_KEY=sk-abc123
Later files override earlier ones if they define the same variable. This lets you layer configuration:
services:
api:
env_file:
- ./envs/base.env # Common defaults
- ./envs/production.env # Production overrides
Method 5: Multiple Compose Files for Environments
Use override files to manage environment-specific configuration:
# docker-compose.yml (base)
services:
api:
image: myapp:latest
environment:
- NODE_ENV=production
postgres:
image: postgres:16
# docker-compose.override.yml (dev — loaded automatically)
services:
api:
environment:
- NODE_ENV=development
- DEBUG=true
volumes:
- ./src:/app/src
ports:
- "3000:3000"
postgres:
ports:
- "5432:5432"
# docker-compose.prod.yml (production — loaded explicitly)
services:
api:
environment:
- NODE_ENV=production
- LOG_FORMAT=json
deploy:
replicas: 3
# Development (uses docker-compose.yml + docker-compose.override.yml)
docker compose up
# Production (explicit file list)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d
Method 6: Docker Secrets (Swarm Mode)
For production deployments with Docker Swarm, use secrets for sensitive data:
# docker-compose.yml
services:
api:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: true # Created via docker secret create
Your application reads from the file path:
# In entrypoint script
export DB_PASSWORD=$(cat /run/secrets/db_password)
For non-Swarm mode, simulate secrets with bind mounts:
services:
api:
volumes:
- ./secrets/db_password.txt:/run/secrets/db_password:ro
Variable Substitution Patterns
Docker Compose supports several substitution patterns:
services:
api:
image: myapp:${TAG} # Required — errors if TAG unset
image: myapp:${TAG:-latest} # Default value if unset or empty
image: myapp:${TAG-latest} # Default only if unset (empty is OK)
image: myapp:${TAG:?Tag is required} # Error with custom message if unset
image: myapp:${TAG?Tag is required} # Error only if unset (empty is OK)
Escaping dollar signs
If your configuration actually contains $ characters:
environment:
- PASSWORD=pa$$word # WRONG — Compose interprets $$
- PASSWORD=pa$$$$word # Use $$ to escape a literal $
Debugging Environment Variables
When containers start with wrong values:
# See what variables are actually set in a running container
docker compose exec api env | sort
# See the resolved compose file with all interpolations
docker compose config
# Check which .env file is being used
docker compose --env-file ./custom.env config
# Inspect specific variable in container
docker compose exec api printenv DB_HOST
# Check if variable is defined but empty vs undefined
docker compose exec api sh -c 'echo "DB_HOST=[${DB_HOST}]"'
The docker compose config command is invaluable — it shows the fully resolved YAML after all variable substitution, revealing exactly what Compose will use.
Production Patterns
Pattern 1: CI/CD pipeline injection
# In your CI pipeline (GitHub Actions, GitLab CI, etc.)
cat > .env << EOF
DB_PASSWORD=${DB_PASSWORD}
API_KEY=${API_KEY}
DEPLOY_TAG=${GITHUB_SHA::8}
EOF
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Pattern 2: HashiCorp Vault integration
# Pull secrets from Vault into env file
vault kv get -format=json secret/myapp | \
jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"' > .env
docker compose up -d
rm .env # Clean up secrets file
Pattern 3: AWS Parameter Store
# Pull parameters from SSM
aws ssm get-parameters-by-path \
--path /myapp/production/ \
--with-decryption \
--query "Parameters[*].[Name,Value]" \
--output text | \
awk '{split($1,a,"/"); print a[4]"="$2}' > .env
docker compose up -d
Common Mistakes
.env to git — This is the most common security mistake. Always add .env to .gitignore before your first commit..env files, DB_PASS="secret" includes the quotes as part of the value. Use DB_PASS=secret without quotes unless you actually want them.environment and env_file — environment sets variables in the container. Variables in .env are for Compose interpolation (${VAR} syntax) unless passed through via env_file..env file. If something works locally but not in CI, check if CI sets conflicting environment variables.docker compose config to debug — When variables are not resolving correctly, this command shows you exactly what Compose sees.env_file with sensitive data in CI — The env files exist on disk and may be cached by the CI runner. For CI/CD, prefer injecting via shell environment.Quick Reference
| Method | Use Case |
|---|---|
| <code class="inline-code">environment:</code> in YAML | Non-sensitive, static config |
| <code class="inline-code">.env</code> file | Local development variables |
| <code class="inline-code">${VAR:-default}</code> | Interpolation with defaults |
| <code class="inline-code">env_file:</code> | Multiple config files, separation of concerns |
| Shell export | CI/CD injection, per-run overrides |
| Docker secrets | Production Swarm deployments |
| Override files | Environment-specific compose configs |
| <code class="inline-code">docker compose config</code> | Debug resolved configuration |
Summary
Start with .env for local development. Use env_file to separate concerns across multiple files. Layer with compose override files for environment-specific configuration. In CI/CD, inject secrets via shell environment variables. For production Swarm deployments, use Docker secrets. Always run docker compose config when things do not resolve as expected.
---
Frequently Asked Questions
How do I pass environment variables in Docker Compose?
You can use the environment key in your service definition for inline values, reference an .env file with env_file, or use variable substitution with ${VARIABLE} syntax that pulls from the shell environment or a .env file in the project root. For secrets, prefer env_file with the file excluded from version control.
What is the difference between .env file and env_file in Docker Compose?
The .env file in the project root is used for variable substitution within docker-compose.yml itself (e.g., image: myapp:${VERSION}). The env_file directive injects variables directly into the container's environment. They serve different purposes and can be used together — .env for compose-time variables and env_file for runtime container variables.
Why are my environment variables not working in Docker Compose?
Common causes include: the .env file is not in the same directory as docker-compose.yml, variable names have spaces around the = sign, the file uses Windows line endings (CRLF), or you're referencing variables that need quotes around values containing special characters. Run docker compose config to see the resolved configuration with all variables expanded.
How do I use different environment variables per deployment environment?
Create separate env files like .env.development, .env.staging, .env.production and reference them with env_file: .env.${DEPLOY_ENV} or use docker compose profiles. Alternatively, use docker compose -f docker-compose.yml -f docker-compose.prod.yml up to layer environment-specific overrides.
How do I handle secrets in Docker Compose?
Never put secrets directly in docker-compose.yml or commit .env files to Git. Use Docker secrets for Swarm mode, external secret managers (Vault, AWS Secrets Manager) injected at runtime, or mount secrets as files. For local development, use an untracked .env.local file and add it to .gitignore.
---
Related Resources
- YAML/JSON Converter — YAML to JSON converter for Docker Compose files
- Kubernetes Pod Troubleshooting — K8s troubleshooting after containerization
- Docker Production Best Practices — Security and optimization for production containers
- Secrets Management in DevOps — Secure handling of environment secrets
- Docker Container Logs Debugging — Debugging container environment issues