✦ For everyone, free.

Practical knowledge for real and everyday life

Home

20.2.1.4 Compose Env Files

A focused guide to Compose Env Files, connecting core concepts with practical Docker and container operations.

Environment files in Docker Compose provide a clean way to manage configuration values — database passwords, API keys, port numbers, feature flags — without hardcoding them in compose.yml or in application source code. Compose supports two distinct mechanisms for environment files: the project-level .env file that Compose reads automatically for variable substitution, and the per-service env_file key that passes variables directly into a container's environment. Understanding both and how they differ prevents configuration errors.

The Project-Level .env File

Compose automatically reads a file named .env in the same directory as compose.yml. Values defined in .env are available for variable substitution inside compose.yml using ${VARIABLE_NAME} syntax:

.env:

POSTGRES_PASSWORD=devsecret
APP_PORT=3000
NODE_ENV=development

compose.yml:

services:
  api:
    build: .
    ports:
      - "${APP_PORT}:3000"
    environment:
      NODE_ENV: ${NODE_ENV}
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

When Compose reads compose.yml, it substitutes ${APP_PORT}, ${NODE_ENV}, and ${POSTGRES_PASSWORD} with the values from .env. The substitution happens in the Compose file itself — the resulting configuration is what Compose uses to create and configure containers.

What .env Controls

The .env file controls Compose file configuration — service names, image tags, port mappings, volume names, and any other value that can appear in compose.yml. It does not automatically inject variables into the container's environment. A variable defined in .env is only available inside a container if it is explicitly referenced in the service's environment section.

This is a common source of confusion: defining NODE_ENV=production in .env does not make NODE_ENV available inside the container unless compose.yml includes:

environment:
  NODE_ENV: ${NODE_ENV}

The env_file Service Key

The env_file key specifies one or more files whose contents are loaded directly as environment variables into the container, without needing to list each variable in the environment section:

compose.yml:

services:
  api:
    build: .
    env_file:
      - .env.api

.env.api:

DATABASE_URL=postgres://user:pass@db:5432/myapp
REDIS_URL=redis://cache:6379
SECRET_KEY=abc123xyz
NODE_ENV=production
LOG_LEVEL=info

Every variable in .env.api becomes available inside the api container as an environment variable. This is different from the project-level .envenv_file passes variables into the container; .env substitutes values in the Compose file itself.

Multiple env_file Entries

A service can load multiple env files, applied in order (later files override earlier ones for the same key):

services:
  api:
    env_file:
      - .env.common
      - .env.local

.env.common might define shared defaults; .env.local might override specific values for the local development environment.

Combining .env and env_file

Both mechanisms can be used together:

compose.yml:

services:
  api:
    build: .
    ports:
      - "${APP_PORT}:3000"
    env_file:
      - .env.api
    environment:
      NODE_ENV: ${NODE_ENV}

Here, APP_PORT and NODE_ENV are read from .env for Compose-level substitution. The variables in .env.api are passed directly into the container. The explicit environment entry for NODE_ENV overrides any NODE_ENV that might appear in .env.api.

Precedence Order

When the same variable is defined in multiple places, Compose applies this precedence (highest wins):

  1. Variables set in the environment section of compose.yml.
  2. Variables from env_file files (later files in the list override earlier ones).
  3. Variables from the project-level .env file (for Compose variable substitution only, not direct container injection unless referenced in environment).

Protecting Secrets with .gitignore

The .env and env files typically contain passwords, API keys, and other secrets. These must never be committed to version control:

.gitignore:

.env
.env.*
!.env.example

The negation !.env.example allows committing a template file with placeholder values that other developers can copy and fill in:

.env.example:

POSTGRES_PASSWORD=changeme
APP_PORT=3000
NODE_ENV=development
SECRET_KEY=your_secret_key_here

This pattern — commit .env.example, ignore .env — is the standard approach for Compose projects that need secrets.

Verifying What Variables a Container Receives

To inspect all environment variables set inside a running container:

docker compose exec api env

Or for a specific variable:

docker compose exec api printenv NODE_ENV

This shows the actual environment the running process sees, regardless of how the variables were injected (.env, env_file, environment, or inherited from the host shell).

Using Shell Environment Variables

If a variable is referenced in compose.yml but not defined in .env, Compose falls back to the shell environment where docker compose is running:

export APP_PORT=8080
docker compose up -d

If APP_PORT is not in .env, Compose uses the shell's APP_PORT=8080. This allows CI/CD systems to inject configuration through environment variables without needing an .env file in the repository.

Variable Substitution Syntax

Compose supports several substitution forms:

environment:
  # Simple substitution
  PORT: ${APP_PORT}
  # Default value if variable is unset or empty
  PORT: ${APP_PORT:-3000}
  # Default value only if variable is unset (not empty)
  PORT: ${APP_PORT-3000}
  # Error if variable is unset or empty
  PORT: ${APP_PORT:?APP_PORT is required}

The default-value forms (:- and -) prevent Compose from failing with an "undefined variable" error when .env is not present — useful for optional configuration with sensible defaults.