env.dev

Docker Compose Environment Variables: The Complete Guide

Use environment variables in Docker Compose: .env auto-load, env_file, --env-file, multi-environment patterns, substitution, and secrets.

By env.dev Updated

Docker Compose v2 (the docker compose CLI, written in Go and the default since Compose v1's docker-compose was retired in June 2023) offers five distinct mechanisms for environment variables, and they do not all reach the same place. Inline environment: and the env_file: directive inject values into the running container. .env (auto-loaded) and --env-file only feed ${VAR} substitution inside the Compose file itself — they never touch the container unless you wire them through environment:. The table below maps every form to its scope and where it sits in the precedence order.

FormWhat it doesScopePrecedence
environment: (inline)Sets vars directly on the containerSingle service containerHighest when a value is set; bare "KEY:" falls back to the shell
env_file: (YAML)Loads one or more files into the containerContainer environmentLower than environment: and shell; later files in the list override earlier ones
.env (auto-loaded)Provides values for ${VAR} in the YAMLCompose file interpolation onlyFeeds substitution; not seen by the container
--env-file (CLI)Replaces .env for substitution at parse timeCompose file interpolation onlyOverrides .env for substitution; not seen by the container
${VAR} substitutionExpands ${VAR} in YAML using shell + .env / --env-fileCompose file onlyShell env > --env-file > .env, all resolved before parsing

Most "why is my variable missing?" tickets come from confusing env_file: with .env: the former injects into the container, the latter only feeds Compose-file substitution. When a substitution is missing entirely, Compose v2 prints WARN[0000] The "FOO" variable is not set. Defaulting to a blank string. — that warning is your fastest signal that .env or --env-file isn't wired up the way you think. For the equivalent rules when running plain docker run -e, see the Docker environment variables guide.

TL;DR

  • Two destinations: environment: and env_file: reach the container; .env, --env-file, and ${VAR} only feed substitution inside the Compose file.
  • The most common "missing variable" bug is confusing env_file: (injects into the container) with .env (Compose-file interpolation only).
  • Precedence, highest first: inline environment: → host shell → env_file: → Dockerfile ENV.
  • A missing env_file: path aborts the whole run — use the long form with required: false (Compose v2.24+, January 2024) to make a file optional.

How do I set environment variables inline in Docker Compose?

The simplest way to set environment variables in Docker Compose is directly in your docker-compose.yml:

yaml
services:
  app:
    image: node:20
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DATABASE_URL=postgres://db:5432/myapp

You can also use the map syntax (without the dash):

yaml
services:
  app:
    image: node:20
    environment:
      NODE_ENV: production
      PORT: "3000"

Both formats are equivalent. The list syntax (- KEY=value) is more common in the wild. Note that NODE_ENV and PORT set here override anything the image baked in with ENV.

How does Docker Compose auto-load .env files?

Docker Compose automatically loads a file named .env in the same directory as your docker-compose.yml. Variables from this file are available for substitution in the YAML file itself — they are NOT automatically injected into containers.

Create a .env file:

ini
# .env
POSTGRES_VERSION=16
APP_PORT=3000

Reference these variables in docker-compose.yml with ${} syntax:

yaml
services:
  db:
    image: postgres:${POSTGRES_VERSION}
    ports:
      - "${APP_PORT}:5432"

Important distinction: The .env file is for Compose file interpolation. To inject variables into the container's environment, use the env_file directive (next section).

Auto-loading is also switchable: set COMPOSE_DISABLE_ENV_FILE=1 to make Compose ignore the default .env entirely, or point COMPOSE_ENV_FILES=base.env,prod.env at a comma-separated list to replace it without repeating --env-file on every invocation.

What does the env_file directive do?

To inject environment variables directly into a container, use env_file:

yaml
services:
  app:
    image: node:20
    env_file:
      - ./app.env
      - ./secrets.env

Where app.env contains:

ini
# app.env
NODE_ENV=production
PORT=3000
API_KEY=sk-abc123

Key behaviors:

  • Variables are injected into the container's environment (visible via docker exec <container> env)
  • Later files override earlier ones if the same variable appears
  • Lines starting with # are comments
  • Empty lines are ignored
  • No quotes needed around values (quotes become part of the value)
  • Multi-line values are not supported in env_file — see the .env file syntax reference for the full quoting and parser-difference rules

When should I use the --env-file flag?

The --env-file flag replaces the default .env file for Compose file interpolation:

bash
# Use .env.prod instead of .env for variable substitution
docker compose --env-file .env.prod up

# Combine with other flags
docker compose --env-file .env.staging up -d

This flag affects which file is used for ${} substitution in docker-compose.yml — it does NOT change what gets injected into containers via env_file.

How do I manage multiple .env files per environment?

A common pattern is maintaining separate env files per environment:

text
project/
├── docker-compose.yml
├── .env                 # Default/development values (auto-loaded)
├── .env.staging         # Staging overrides
├── .env.production      # Production overrides
├── app.env              # App-specific vars (injected into container)
└── db.env               # Database vars (injected into container)
yaml
# docker-compose.yml
services:
  app:
    image: myapp:latest
    env_file:
      - ./app.env
    environment:
      - APP_ENV=${APP_ENV:-development}

  db:
    image: postgres:${POSTGRES_VERSION:-16}
    env_file:
      - ./db.env

Run with different environments:

bash
# Development (uses .env automatically)
docker compose up

# Staging
docker compose --env-file .env.staging up

# Production
docker compose --env-file .env.production up -d

How do I mix env sources per service?

Each service can have its own combination of inline variables and env files. If you use VS Code Dev Containers, the same per-service env layout drops straight into devcontainers — they integrate Docker Compose directly via dockerComposeFile:

yaml
services:
  frontend:
    image: nginx:alpine
    environment:
      - NGINX_PORT=80

  backend:
    image: node:20
    env_file:
      - ./backend.env
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://db:5432/app

  db:
    image: postgres:16
    env_file:
      - ./db.env
    environment:
      - POSTGRES_DB=myapp

Priority order (highest wins):

  1. environment values in docker-compose.yml
  2. Shell environment variables on the host
  3. env_file values
  4. Dockerfile ENV defaults

"Host shell" means whatever the shell running docker compose has exported — export VAR=x on Linux/macOS, set or $env: on Windows (see the Windows environment variables guide for the session vs persistent distinction).

How does variable substitution work in docker-compose.yml?

Docker Compose supports shell-like variable substitution in YAML:

yaml
services:
  app:
    image: myapp:${TAG:-latest}
    ports:
      - "${PORT:?PORT must be set}:3000"
    environment:
      - DEBUG=${DEBUG:-false}
SyntaxBehavior
${VAR}Value of VAR, empty if unset
${VAR:-default}Default if VAR is unset or empty
${VAR-default}Default only if VAR is unset
${VAR:?error}Error message if VAR is unset or empty
${VAR?error}Error message only if VAR is unset

Check what Compose will resolve:

bash
docker compose config

This prints the fully resolved docker-compose.yml with all variables substituted. For more debugging commands, see the Docker cheat sheet.

How should I manage secrets in Docker Compose?

Never commit secrets to version control. Use a committed .env.example to document required variables (the same convention covered in the .env file guide) alongside an ignored .env:

ini
# .env.example (commit this — documents required variables)
DATABASE_URL=
API_KEY=
JWT_SECRET=

# .env (DO NOT commit — add to .gitignore)
DATABASE_URL=postgres://user:pass@db:5432/app
API_KEY=sk-live-abc123
JWT_SECRET=super-secret-key

Add to .gitignore:

text
.env
.env.production
.env.staging
*.env
!.env.example

A connection string like DATABASE_URL embeds the password in plaintext, so keep it out of the committed file. For production, consider Docker secrets:

yaml
services:
  app:
    image: myapp:latest
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

What are common pitfalls and debugging tips?

Quotes become part of the value

ini
# WRONG — value will be "production" (with quotes)
NODE_ENV="production"

# RIGHT — value will be production
NODE_ENV=production

.env vs env_file confusion

  • .env (auto-loaded) → substitution in docker-compose.yml only
  • env_file directive → injected into container environment

Missing env_file aborts the whole run

If a path listed under env_file: doesn't exist, Compose v2 fails with env file ./app.env not found: stat ./app.env: no such file or directory before any container starts. Compose v2.24 (released January 2024) added the long form to make a file optional:

yaml
services:
  app:
    image: node:20
    env_file:
      - path: ./app.env
        required: false   # skip silently if missing
      - path: ./secrets.env

Use required: false for files that only exist in some environments (e.g. a developer-only overrides.env) so the same compose.yml works in CI without stub files.

Variables not updating

bash
# Recreate containers to pick up env changes
docker compose up -d --force-recreate

# Or rebuild if using build-time args
docker compose up -d --build

Debugging current values

bash
# See resolved compose file
docker compose config

# See container environment
docker exec <container> env

# See what .env file is loaded
docker compose config --environment

Did Docker Compose v5 change environment variable handling?

Mostly no — and that is the point worth knowing. Compose jumped straight from v2.x to v5.0.0 in December 2025, deliberately skipping 3.0 and 4.0 so nobody confuses the CLI version with the long-obsolete version: "3" field from old compose files. The headline changes are an official SDK and build delegation to Docker Bake; every .env, env_file, and interpolation behaviour on this page works identically on v2.24+ and v5 (v5.1.4 is current as of May 2026).

One env-relevant fix did land: v5.0.2 made --env-file=~/secrets/.env expand the tilde to your home directory instead of treating it as a literal path. If a CI runner pins an older Compose — GitHub-hosted runners only moved to v2.40 in February 2026 — spell the path out absolutely.

References

Was this helpful?

Frequently Asked Questions

Can I use multiple .env files at once with Docker Compose?

Not with the auto-loaded .env. Docker Compose only auto-loads one .env file. Use --env-file to specify a different one, or use the env_file directive in YAML for container-level env files (which does support multiple files).

What is the difference between environment and env_file in Docker Compose?

environment sets variables inline in the YAML. env_file loads variables from a file. Both inject into the container. environment values take priority over env_file values.

Does docker compose --env-file affect env_file in the YAML?

No. --env-file only affects ${} substitution in the Compose file. The env_file directive in YAML independently loads its specified files into the container.

Do I need docker-compose.yml or compose.yml?

Both work. Docker Compose V2 (the docker compose command) prefers compose.yml but supports both. Use whichever your team prefers.

How do I pass host environment variables to a container?

List the variable name without a value in the environment section (e.g. - MY_HOST_VAR). Docker Compose passes the host's current value of MY_HOST_VAR into the container.

Did Docker Compose v5 change how .env files work?

No. Compose jumped from v2.x straight to v5.0.0 (December 2025), skipping 3.0 and 4.0 to avoid confusion with the obsolete version: field in compose files. All .env, env_file, and interpolation behaviour is unchanged; the one env-related fix is that v5.0.2 expands a tilde in --env-file paths to the home directory.

How do I stop Docker Compose from auto-loading the .env file?

Set COMPOSE_DISABLE_ENV_FILE=1 (or true) and Compose ignores the default .env entirely. To swap in different files instead, set COMPOSE_ENV_FILES to a comma-separated list, which behaves like passing --env-file for each entry.

Stay up to date

Get notified about new guides, tools, and cheatsheets.