iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

.env Files

Compose pulls environment values from .env files, CLI flags, and the host environment in a defined order. Knowing the precedence and the difference between Composes own variables and the variables your container sees stops the "why is X set in compose.yaml but missing inside the container?" confusion.

env_file, environment, variable interpolation, secrets

EXAMPLE
# compose.yaml — three places env vars live

services:
  api:
    image: ${IMAGE_API:-shop/api:dev}            # Composes interpolation; uses .env or shell
    environment:
      NODE_ENV: ${NODE_ENV:-production}            # Container env: from interpolated host var
      DB_HOST: db                                  # Container env: literal
      APP_VERSION: '1.4.0'
    env_file:
      - ./envs/common.env                          # Container env: file
      - ./envs/api.env                             # later file overrides earlier
    secrets:
      - api_signing_key                            # mounted at /run/secrets/api_signing_key

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD?required}   # FAILS if .env / shell does not set DB_PASSWORD
      POSTGRES_DB: shop
      POSTGRES_USER: shop

secrets:
  api_signing_key:
    file: ./secrets/api_signing_key.txt

# ===== Precedence — first match wins for variable interpolation =====
# 1) Shell environment when running 'docker compose ...'
# 2) --env-file flag value(s)
# 3) The default .env file in the project root
# 4) The default specified after :- in compose.yaml (${X:-default})

# ===== env_file vs environment =====
# environment: keys/values inside compose.yaml; can use Compose interpolation
# env_file:    a file of KEY=VALUE; loaded into the container
#              MULTIPLE files allowed; later overrides earlier
# Both inject into the CONTAINER. The interpolation step has already happened.

# ===== Composes interpolation operators =====
# ${X}             error if X is empty
# ${X:-default}    use 'default' if X is unset OR empty
# ${X-default}     use 'default' if X is unset (empty is fine)
# ${X:?msg}        FAIL with 'msg' if X is unset or empty (use for required prod values)
# ${X?msg}         FAIL with 'msg' if X is unset (empty allowed)
# ${X:+value}      use 'value' if X is set AND non-empty
# ${X+value}       use 'value' if X is set

# ===== Confirm what compose ACTUALLY sees =====
docker compose config
# Prints the fully-interpolated YAML; if a var is wrong, you see it here.

# ===== Confirm what the CONTAINER sees =====
docker compose exec api env | sort

# ===== .env files vs --env-file =====
# .env  in project root is auto-loaded by Compose.
# Use --env-file when you have per-environment files:
docker compose --env-file ./envs/staging.env up -d
docker compose --env-file ./envs/prod.env    up -d

# ===== Secrets (top-level 'secrets:' block) =====
# - Mounted to /run/secrets/<name> inside the container as a file
# - Not part of environment by default; the app reads the file
# - Avoid putting secrets in 'environment:' — they leak via 'docker inspect',
#   container logs, and crash dumps

# ===== Multiple environments — override files =====
# compose.yaml          : base
# compose.dev.yaml      : dev tweaks (mounts source, exposes DB port)
# compose.prod.yaml     : prod tweaks (no mounts, tighter limits)

docker compose -f compose.yaml -f compose.dev.yaml  up -d
docker compose -f compose.yaml -f compose.prod.yaml up -d

# ===== Common pitfalls =====
# - Setting NODE_ENV in .env but expecting it in the COMPOSE-FILE interpolation
#   only (not the container). .env values DO appear in both via interpolation,
#   but only env_file values guarantee they reach the container regardless of
#   interpolation typos.
# - Quoting values in .env: do NOT wrap in quotes; Compose does not strip them.
# - Trailing whitespace in .env lines — bites you on multi-line values.
# - Using 'environment:' for production passwords; switch to 'secrets:'.
# - Forgetting that --env-file replaces the default .env (it does not stack).

# ===== Checklist before pushing to prod =====
# 1) Every secret comes from --env-file <env-specific> OR 'secrets:'
# 2) Use ${VAR:?msg} for variables that MUST be set
# 3) 'docker compose config' shows the fully resolved YAML
# 4) No plain passwords inside compose.yaml itself
# 5) Per-env override files are committed (without the secret values)

Why it matters

Use `${VAR:?required}` on every value that must be set in production. The failure mode flips from "container starts, then crashes on first request" to "compose refuses to start with a clear message" — and the deploy never lands in the half-broken state that comes from a silently-empty env var.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
# .env at repo root
POSTGRES_PASSWORD=secret
# referenced via ${POSTGRES_PASSWORD} in compose.yaml
Try it Yourself »

Discussion

Loading…