Command Palette

Search for a command to run...

Hectal
PHASE 5Intermediate ~13 min· topic 3 of 3

Topic 5.3

Environment Variables, .env & Multiple Compose Files

In one line

Keeping secrets and per-environment differences OUT of your committed compose.yaml, and layering multiple Compose files for dev vs production variations of the same base stack.

0/3 · 0%

Key ideas

  1. 01

    Hardcoding a real database password directly in compose.yaml is a security mistake the moment that file is committed to version control — Compose automatically reads a .env file sitting next to your compose.yaml and substitutes ${VARIABLE} references, keeping actual secret VALUES out of the tracked file.

  2. 02

    The pattern: compose.yaml references ${POSTGRES_PASSWORD}, and a .env file (which you .gitignore, exactly like Phase 2.3's Dockerfile secrets lesson) holds the actual value — anyone cloning the repo copies a .env.example template and fills in their own values, never committing real secrets.

  3. 03

    Compose supports layering MULTIPLE files together: docker compose -f compose.yaml -f compose.dev.yaml up merges both files, with the SECOND file's settings overriding/extending the first — a common pattern is a base compose.yaml with shared production-like settings, plus a compose.dev.yaml that adds bind mounts for live code editing and debug ports, used only locally.

  4. 04

    This layering avoids duplicating your ENTIRE stack definition twice (once for dev, once for prod) — you write the shared parts ONCE in the base file, and each environment-specific override file only needs to specify what's actually DIFFERENT for that environment.

  5. 05

    docker compose config merges and prints the FINAL, fully-resolved configuration (after applying all layered files and variable substitutions) — extremely useful for debugging 'why isn't my override taking effect' by seeing exactly what Compose actually computed.

  6. 06

    Environment variables can also be set directly under a service's environment: key without a .env file at all — appropriate for genuinely NON-secret configuration (like SPRING_PROFILES_ACTIVE=docker) that's fine to have visible in the committed file.

In your stack

  • →

    A real pattern: compose.yaml sets SPRING_PROFILES_ACTIVE=docker directly (non-secret, fine to commit), while SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} pulls from .env (a real secret, never committed) — Spring Boot's own profile-based application-docker.properties then activates docker-specific settings on top of the base config.

Code & diagrams

.envmarkdown

Never committed — .gitignore this file, per Phase 2.3.

DB_PASSWORD=a_real_secret_value_here
DB_NAME=mydb
compose.yamlmarkdown
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}      # pulled from .env, never hardcoded
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pgdata:/var/lib/postgresql/data

  app:
    build: .
    environment:
      SPRING_PROFILES_ACTIVE: docker           # non-secret, fine to commit directly
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}

volumes:
  pgdata:
compose.dev.yamlmarkdown

An override file — only what's DIFFERENT for local dev.

services:
  app:
    build:
      context: .
      target: dev            # a dev-specific build stage, see Phase 7
    volumes:
      - ./src:/app/src        # bind-mount source for live reload, per Phase 3.3
    ports:
      - "5005:5005"           # extra port for a Java remote debugger
compose-layering.shmarkdown
# Base only (closer to production shape)
docker compose up

# Base + dev overrides layered on top (local development)
docker compose -f compose.yaml -f compose.dev.yaml up

# See the FINAL merged config Compose actually computed — great for debugging
docker compose -f compose.yaml -f compose.dev.yaml config

Explain it without notes

01

Why is putting POSTGRES_PASSWORD: mysecretpassword directly in a committed compose.yaml a real security problem, even for a 'just local development' project?

02

When layering compose.yaml and compose.dev.yaml together, which file's settings take precedence for a key defined in BOTH?

Practice

01

Set up a .env file and reference a variable from it in your compose.yaml, confirm it resolves correctly with docker compose config.

02

Create a base compose.yaml and a compose.dev.yaml that overrides one setting (like adding a bind mount), and confirm with docker compose config that the merged result reflects your override correctly.

Trade-offs

  • ↔

    Layered Compose files add a small amount of indirection (you now need to remember which combination of -f flags to use for which environment) — usually worth documenting in a README or wrapping in a simple Makefile/script target so nobody has to memorize the exact flag combination.

Done when you can

  • No real secret ever appears directly in a compose.yaml I commit to version control.

  • I can layer a base compose.yaml with an environment-specific override file.

  • I use docker compose config to debug merged configuration before assuming something is broken.