The env_file key looks simple but has surprisingly rich behavior, you can load multiple files, mark some as optional, and control how they’re parsed.

The two flavors of .env

There are two completely different things called “.env” in Compose:

  • The project .env file at the same level as compose.yml, used for interpolation of ${VAR} in the Compose file itself (see Tip #42)
  • env_file: at service level, loaded as runtime environment variables inside the container

This post is about the second one.

Single file (basic form)

services:
  app:
    image: myapp
    env_file: .env.app
# .env.app
DATABASE_URL=postgres://db:5432/mydb
LOG_LEVEL=info

All key-value pairs end up as environment variables in the container.

Multiple files with priority

Chain multiple env files, later files override earlier ones:

services:
  app:
    image: myapp
    env_file:
      - .env.common         # Shared defaults
      - .env.${ENV:-dev}    # Environment-specific overrides
      - .env.local          # Local developer tweaks (gitignored)

Result for a missing key: nothing, as expected. For a key defined in .env.common and .env.local: the .env.local value wins.

Optional files

By default, a missing env file causes Compose to fail. Use required: false to make it optional:

services:
  app:
    image: myapp
    env_file:
      - path: .env.common
        required: true    # Default
      - path: .env.local
        required: false   # Skip if missing

Great for .env.local files that each developer may or may not have.

Format option

The default format treats each line as KEY=VALUE. For more complex values (multi-line, JSON blobs, strings with special characters), use format: raw to parse the file differently, or just stick to the default format and quote your values:

services:
  app:
    env_file:
      - path: .env.app
        format: raw

For most cases, the default format is what you want.

Interpolation inside env files

Env files support ${VAR} substitution, pulling from the host environment or the project .env:

# .env.app
DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
API_KEY=${API_KEY}

As long as DB_USER, DB_PASSWORD, etc. are available on the host (or in the project .env), Compose resolves them before passing to the container.

Precedence: env_file vs environment

When both env_file and environment define the same variable, environment wins:

services:
  app:
    env_file:
      - .env.app            # LOG_LEVEL=info
    environment:
      LOG_LEVEL: debug      # ← this wins

The running container sees LOG_LEVEL=debug. Useful for one-off overrides without editing the env file.

A practical multi-environment setup

my-app/
├── compose.yml
├── .env.common
├── .env.dev
├── .env.staging
├── .env.prod
└── .env.local      # gitignored
services:
  app:
    image: myapp
    env_file:
      - .env.common
      - path: .env.${ENV:-dev}
        required: true
      - path: .env.local
        required: false
docker compose up                  # uses .env.dev
ENV=staging docker compose up      # uses .env.staging
ENV=prod docker compose up         # uses .env.prod

Further reading