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 env_file .env file
Why Environment Variables in Compose?

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).

Method 1: The `environment` Directive

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!
Hardcoding secrets in docker-compose.yml is a security risk. Never commit passwords, API keys, or tokens to version control. Use env_file or .env for secrets.
Method 2: The `env_file` Option

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
Add `*.env` to your `.gitignore` file to prevent committing secrets. Commit example files (like `.env.example`) with placeholder values.
Method 3: The `.env` File (Variable Substitution)

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
The `.env` file is automatically loaded by `docker compose up`. No special flags needed. Variables are substituted in the compose file before it's parsed.
Variable Substitution Syntax

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
Priority Order (Which Takes Precedence?)

When multiple methods define the same variable, Compose follows this priority order (highest to lowest):

  1. Shell environment variables - Variables set before running `docker compose up`
  2. Environment directive in compose file - Hardcoded in the YAML
  3. env_file files - Later files override earlier ones
  4. .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
Complete Example: Multi-Environment Setup
# .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
Using env_file with Docker Secrets (Swarm)

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"
Best Practices for Environment Variables
  • 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/
Debugging Environment Variables
# 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
Comparison: When to Use Each Method
MethodBest ForCommit to Git?Passed to ContainerUsed by Compose
environmentDefaults, 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)
Frequently Asked Questions
What's the difference between env_file and .env file?
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.
Does Docker Compose automatically load .env file?
Yes, by default `docker compose up` loads variables from a `.env` file in the current directory. Use `--env-file` to specify a different file.
Can I use .env file for secrets?
Not recommended. The `.env` file is often committed by accident. Use `env_file` with files excluded from version control for secrets.
How do I pass shell variables to compose?
Shell variables are automatically available for substitution. Example: `export TAG=v1 && docker compose up` makes ${TAG} available in the compose file.
What's the syntax for default values in substitution?
Use `${VAR:-default}` for a default when VAR isn't set. Use `${VAR:=default}` to set VAR to default and also use it. Use `${VAR:?error}` to require VAR.
Can multiple env_file files override each other?
Yes. Variables from later files override earlier files. Order matters: `env_file: [.env.common, .env.production]` - production overrides common.
How do I escape $ in compose files?
Use `$$` to output a literal dollar sign. For example, `command: echo "$$HOME"` prints `$HOME` instead of substituting.
Why are my environment variables not showing in the container?
Check: Did you use `environment` or `env_file`? Are you checking in the right container? Use `docker compose exec container env` to verify.
Previous: Docker Compose Commands Next: Compose for Production

Proper environment variable management is essential for secure, portable Docker Compose configurations. Use the right method for the right purpose.