Environment Variables in Compose: Configuring Your Services
Environment variables configure your services. Database passwords. API keys. Feature flags. They change between environments. Managing them properly makes your Compose files flexible.
🎯 The Big Picture​
Think of environment variables like settings on a phone. Same phone (container). Different settings (variables). Different behavior. That's environment variables.
Environment variables make your Compose files flexible. Same file. Different environments. Different configurations.
Why Environment Variables?​
The problem without variables:
services:
db:
environment:
POSTGRES_PASSWORD: my-secret-password
# Hard-coded! Can't change!
The solution with variables:
services:
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
# From environment! Flexible!
That's the difference. Hard-coded vs flexible. Inflexible vs configurable.
Setting Environment Variables​
Three ways to set variables:
1. In docker-compose.yml​
services:
app:
environment:
- NODE_ENV=production
- DB_HOST=db
- DB_PORT=5432
Use when: Default values. Development. Simple cases.
2. From .env File​
Create .env file:
DB_PASSWORD=secret123
API_KEY=sk_live_abc123
DEBUG=false
Use in Compose:
services:
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
Docker Compose automatically reads .env file.
Use when: Sensitive data. Different per environment. Best practice.
3. From Shell Environment​
# Set in shell
export DB_PASSWORD=secret123
# Use in Compose
docker compose up
Compose reads from shell environment.
Use when: CI/CD pipelines. Temporary values. Override defaults.
Variable Substitution​
Docker Compose substitutes variables:
services:
app:
environment:
DB_HOST: ${DB_HOST:-localhost}
DB_PORT: ${DB_PORT:-5432}
Syntax: ${VARIABLE:-default}
What it means:
- Use
DB_HOSTif set - Otherwise use
localhost(default)
Think of it as: Fallback values. If variable not set, use default.
Real-World Example: Complete Configuration​
Let me show you a real example:
.env file:
# Database
DB_PASSWORD=super-secret-password
DB_NAME=myapp
DB_USER=appuser
# Application
NODE_ENV=production
API_KEY=sk_live_abc123def456
DEBUG=false
# Redis
REDIS_PASSWORD=redis-secret
docker-compose.yml:
services:
postgres:
image: postgres:14-alpine
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME:-myapp}
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
app:
build: .
environment:
NODE_ENV: ${NODE_ENV:-development}
DB_HOST: postgres
DB_PORT: 5432
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: ${DB_NAME}
REDIS_HOST: redis
REDIS_PASSWORD: ${REDIS_PASSWORD}
API_KEY: ${API_KEY}
DEBUG: ${DEBUG:-false}
depends_on:
- postgres
- redis
volumes:
postgres-data:
redis-data:
What this does:
- Reads from .env file
- Uses defaults when not set
- Configures all services
- Flexible and secure
The Phone Settings Analogy​
Think of environment variables like phone settings:
Same phone (container): Same hardware Different settings (variables): Different behavior .env file: Settings file Defaults: Factory settings
Once you see it this way, environment variables make perfect sense.
Best Practices​
1. Use .env for Sensitive Data​
Don't commit secrets:
# BAD - Hard-coded in file
POSTGRES_PASSWORD: my-secret-password
Do use .env:
# GOOD - From .env file
POSTGRES_PASSWORD: ${DB_PASSWORD}
Add .env to .gitignore:
.env
.env.local
.env.*.local
2. Provide Defaults​
Use defaults:
environment:
DB_PORT: ${DB_PORT:-5432}
NODE_ENV: ${NODE_ENV:-development}
Why: Works out of the box. No configuration needed for development.
3. Document Variables​
Add comments:
# Database configuration
# DB_PASSWORD: PostgreSQL password (required)
# DB_NAME: Database name (default: myapp)
Or create .env.example:
# Database
DB_PASSWORD=your-password-here
DB_NAME=myapp
DB_USER=appuser
# Application
NODE_ENV=production
API_KEY=your-api-key-here
Why: Clear documentation. Easy to set up.
4. Use Different .env Files​
For different environments:
.env.development
.env.staging
.env.production
Load specific file:
docker compose --env-file .env.production up
Why: Different configs per environment. Easy to manage.
My Take: Environment Variable Strategy​
Here's what I do:
Development:
- .env file with defaults
- Easy to edit
- No secrets (use defaults)
Staging/Production:
- .env file from secrets manager
- Never commit
- Rotate regularly
The key: Use .env for everything. Never hard-code. Always provide defaults.
Memory Tip: The Phone Settings Analogy​
Environment variables = Phone settings
Container: Phone Variables: Settings .env file: Settings file Defaults: Factory settings
Once you see it this way, environment variables make perfect sense.
Common Mistakes​
- Hard-coding secrets: Security risk
- Committing .env: Secrets in git
- No defaults: Breaks without configuration
- Not documenting: Hard to know what's needed
- Wrong syntax: Variables don't substitute
Hands-On Exercise: Use Environment Variables​
1. Create .env file:
MESSAGE=Hello from environment variable
PORT=3000
2. Create docker-compose.yml:
services:
app:
image: alpine
command: sh -c "echo ${MESSAGE} && sleep 3600"
environment:
- MESSAGE=${MESSAGE:-Default message}
- PORT=${PORT:-8080}
3. Run it:
docker compose up
# Should show: Hello from environment variable
4. Override from shell:
MESSAGE="Hello from shell" docker compose up
# Should show: Hello from shell
5. Check variables:
docker compose exec app env | grep MESSAGE
# Shows the variable value
Key Takeaways​
- Environment variables configure services - Flexible configuration
- Use .env file for sensitive data - Never commit secrets
- Provide defaults - Works out of the box
- Document variables - Clear what's needed
- Use different .env files - Per environment
What's Next?​
Now that you understand environment variables, let's learn about production Compose files. Next: Production Compose.
Remember: Environment variables are like phone settings. Same container. Different settings. Different behavior. Use .env files. Never hard-code secrets.