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.
Key ideas
- 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
.envfile sitting next to your compose.yaml and substitutes${VARIABLE}references, keeping actual secret VALUES out of the tracked file. - 02
The pattern: compose.yaml references
${POSTGRES_PASSWORD}, and a.envfile (which you.gitignore, exactly like Phase 2.3's Dockerfile secrets lesson) holds the actual value — anyone cloning the repo copies a.env.exampletemplate and fills in their own values, never committing real secrets. - 03
Compose supports layering MULTIPLE files together:
docker compose -f compose.yaml -f compose.dev.yaml upmerges both files, with the SECOND file's settings overriding/extending the first — a common pattern is a basecompose.yamlwith shared production-like settings, plus acompose.dev.yamlthat adds bind mounts for live code editing and debug ports, used only locally. - 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.
- 05
docker compose configmerges 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. - 06
Environment variables can also be set directly under a service's
environment:key without a.envfile at all — appropriate for genuinely NON-secret configuration (likeSPRING_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=dockerdirectly (non-secret, fine to commit), whileSPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}pulls from.env(a real secret, never committed) — Spring Boot's own profile-basedapplication-docker.propertiesthen activates docker-specific settings on top of the base config.
Code & diagrams
Never committed — .gitignore this file, per Phase 2.3.
DB_PASSWORD=a_real_secret_value_here
DB_NAME=mydbservices:
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: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# 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 configExplain it without notes
Why is putting POSTGRES_PASSWORD: mysecretpassword directly in a committed compose.yaml a real security problem, even for a 'just local development' project?
When layering compose.yaml and compose.dev.yaml together, which file's settings take precedence for a key defined in BOTH?
Practice
Set up a .env file and reference a variable from it in your compose.yaml, confirm it resolves correctly with docker compose config.
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
-fflags 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 configto debug merged configuration before assuming something is broken.