Skip to main content
Docker·10 min read

Docker Compose Environment Variables — Why Your Secrets Keep Leaking Into Logs

Learn every method for passing environment variables in Docker Compose. Covers .env files, shell interpolation, multiple environments, secrets management, and production patterns.

DT

DevOps Engineer & Technical Writer

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.

Docker Compose Environment Variable Precedence Higher priority overrides lower priority .env file Default values Priority: 1 (lowest) Shell ENV export VAR=val Priority: 2 compose.yaml environment: key Priority: 3 docker-compose .override.yml Priority: 4 (highest) Container Final resolved values Low Priority Medium High Highest Priority

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)

  • Shell environment variables
  • .env file values
  • Default values in ${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

  • Committing .env to git — This is the most common security mistake. Always add .env to .gitignore before your first commit.
  • Quoting values incorrectly — In .env files, DB_PASS="secret" includes the quotes as part of the value. Use DB_PASS=secret without quotes unless you actually want them.
  • Confusing environment and env_fileenvironment sets variables in the container. Variables in .env are for Compose interpolation (${VAR} syntax) unless passed through via env_file.
  • Variable precedence surprises — Shell environment always wins over .env file. If something works locally but not in CI, check if CI sets conflicting environment variables.
  • Not using docker compose config to debug — When variables are not resolving correctly, this command shows you exactly what Compose sees.
  • Using 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

    MethodUse Case
    <code class="inline-code">environment:</code> in YAMLNon-sensitive, static config
    <code class="inline-code">.env</code> fileLocal 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 exportCI/CD injection, per-run overrides
    Docker secretsProduction Swarm deployments
    Override filesEnvironment-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.

    ---