Environment Variables in Docker Compose
Environment variables are essential for configuring Docker Compose applications across different environments. This guide covers all three methods: the `environment` directive, `env_file` option, and `.env` file for variable substitution.
Environment variables allow you to configure your containers without hardcoding values in your docker-compose.yml file. This is essential for:
- Environment-specific configuration - Development, staging, and production have different database passwords, API keys, and URLs.
- Security - Secrets like passwords and tokens shouldn't be committed to version control.
- Portability - The same compose file works across different environments with different configurations.
- Reusability - Parameterize compose files for different use cases.
Docker Compose provides three ways to work with environment variables: the `environment` directive (inside the compose file), `env_file` (external file loaded into the container), and the `.env` file (for variable substitution in the compose file itself).
The `environment` directive sets environment variables directly inside the container. You can define them inline in the docker-compose.yml file. This is the simplest method, but it hardcodes values in the compose file—not ideal for secrets.
services:
app:
image: myapp:latest
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=postgresql://user:pass@db:5432/app
- API_KEY=secret123
- REDIS_HOST=redis
- REDIS_PORT=6379
database:
image: postgres:15
environment:
POSTGRES_DB: myapp
POSTGRES_USER: admin
POSTGRES_PASSWORD: secretpass # Not recommended for production!
The `env_file` option loads environment variables from an external file. This is perfect for secrets because you can add the file to `.gitignore`. Multiple `env_file` entries are allowed; later files override earlier ones.
# .env.production (do not commit this file)
DATABASE_URL=postgresql://prod:prodpass@db:5432/myapp
API_KEY=prod-secret-key
NODE_ENV=production
# docker-compose.yml
services:
app:
image: myapp:latest
env_file:
- .env.common
- .env.production
environment:
- NODE_ENV=production # Overrides env_file
database:
image: postgres:15
env_file:
- ./config/db.env
- ./secrets/db.env
# Multiple env files are merged, with later files taking precedence
Docker Compose automatically loads variables from a `.env` file in the same directory and uses them for variable substitution in the compose file itself. This is different from `env_file`—these variables are used by Compose to interpret the YAML, not passed into containers.
# .env file (loaded automatically by compose)
COMPOSE_PROJECT_NAME=myproject
TAG=latest
PORT=8080
DB_PASSWORD=secret
# docker-compose.yml
version: '3.8'
services:
app:
image: myapp:${TAG:-latest}
ports:
- "${PORT}:3000"
environment:
- DB_PASSWORD=${DB_PASSWORD}
labels:
- "project=${COMPOSE_PROJECT_NAME}"
volumes:
data:
name: ${COMPOSE_PROJECT_NAME}_data
Compose supports several syntax variations for variable substitution:
# Basic substitution
image: myapp:${TAG}
# Default value if variable not set
image: myapp:${TAG:-latest}
# Required variable (fails if not set)
image: myapp:${TAG:?TAG is required}
# Alternative default syntax
ports:
- "${PORT:=3000}:3000"
# Escape $ sign (use $$ to output literal $)
command: echo "$${VARIABLE}"
# Examples
services:
app:
image: myapp:${VERSION:-1.0.0}
environment:
- NODE_ENV=${ENV:-development}
ports:
- "${API_PORT:-8080}:${APP_PORT:-3000}"
volumes:
- ${DATA_PATH:-./data}:/app/data
When multiple methods define the same variable, Compose follows this priority order (highest to lowest):
- Shell environment variables - Variables set before running `docker compose up`
- Environment directive in compose file - Hardcoded in the YAML
- env_file files - Later files override earlier ones
- .env file - For substitution (not passed to container by default)
# Shell variable takes highest precedence
export DATABASE_URL=postgresql://prod:prodpass@db:5432/myapp
docker compose up
# Environment directive overrides env_file
services:
app:
env_file:
- .env.common # DATABASE_URL=postgresql://dev:devpass@db:5432/myapp
environment:
- DATABASE_URL=postgresql://local:localpass@db:5432/myapp # This wins
# .env (shared across all environments)
COMPOSE_PROJECT_NAME=myapp
APP_VERSION=1.0.0
# .env.dev (development overrides)
ENV=development
PORT=8080
DB_PASSWORD=devpass
# .env.prod (production overrides - not committed)
ENV=production
PORT=80
DB_PASSWORD=prodsecret
# docker-compose.yml
version: '3.8'
services:
app:
image: myapp:${APP_VERSION}
build:
context: .
args:
- NODE_VERSION=${NODE_VERSION:-18}
ports:
- "${PORT:-3000}:3000"
environment:
- NODE_ENV=${ENV:-development}
- DATABASE_URL=postgresql://user:${DB_PASSWORD}@db:5432/myapp
env_file:
- ./config/common.env
volumes:
- ./src:/app/src:${MOUNT_MODE:-ro}
database:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
name: ${COMPOSE_PROJECT_NAME}_data
# Usage:
# Development: docker compose --env-file .env.dev up -d
# Production: docker compose --env-file .env.prod up -d
In Docker Swarm, you can combine `env_file` with secrets for better security. Secrets are encrypted and only mounted to containers that need them.
# docker-compose.yml (Swarm mode)
version: '3.8'
secrets:
db_password:
external: true
api_key:
file: ./secrets/api_key.txt
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
entrypoint: /bin/sh -c "export DB_PASSWORD=$$(cat /run/secrets/db_password) && exec node server.js"
- Never commit secrets to version control - Add `*.env` and `*.secret` to `.gitignore`. Commit example files (`.env.example`) with placeholder values.
- Use .env for non-sensitive defaults - Default ports, version numbers, and feature flags are fine in `.env`.
- Use env_file for secrets in production - Load secrets from files outside the repository.
- Use environment for defaults in compose file - Set sensible defaults that work out of the box.
- Use shell variables for CI/CD overrides - In pipelines, export variables to override compose values.
- Document required variables - Use `:?` syntax to fail if required variables are missing.
- Use different env files per environment - `.env.dev`, `.env.staging`, `.env.prod` with appropriate values.
# .env.example (commit this)
# Copy this file to .env and fill in your values
NODE_ENV=development
PORT=3000
DB_PASSWORD=change_me
API_KEY=your_api_key_here
# .gitignore (add these lines)
.env
*.env
!.env.example
*.secret
secrets/
# Check what variables Compose sees
docker compose config
# View environment variables inside a running container
docker compose exec app env
docker compose exec app printenv
# Debug substitution with --dry-run
docker compose --dry-run up
# Check which env file is being loaded
docker compose config --env-file .env.dev
# Debug config with resolved variables
docker compose config --variables
# Check specific variable
docker compose exec app printenv DATABASE_URL
| Method | Best For | Commit to Git? | Passed to Container | Used by Compose |
|---|---|---|---|---|
| environment外 | Defaults, non-secrets, simple configs | Yes (safe values only) | Yes | No |
| env_file | Secrets, environment-specific configs | No (add to .gitignore) | Yes | No |
| .env file | Variable substitution in compose file itself | No (except .env.example) | No (unless manually passed) | Yes |
| Shell variables | CI/CD pipelines, temporary overrides | N/A | Yes (if passed through) | Yes (highest priority) |
env_file loads variables INTO the container. The `.env` file is used for variable substitution IN the compose file itself (replacing ${VARIABLE} syntax). They serve different purposes and are often used together.Proper environment variable management is essential for secure, portable Docker Compose configurations. Use the right method for the right purpose.