3.4Variables, profiles and per-environment overrides
The essentials in 30 seconds
- .env: one value, several faces: ${PORT:-3000} reads the variable or its default — one .env file per environment, one compose for N faces.
- Overrides: base + variant: compose.override.yml auto-loaded in dev, -f compose.yml -f compose.prod.yml in prod — each file describes only its difference.
- Profiles: opt-in services: profiles: [dev] makes adminer absent by default, present with --profile dev — the default stack is minimal and safe.
Running mini-case — Idir unifies dev and prod
Idir, junior DevOps in Algiers, maintains two copied compose.yml files (dev with adminer, prod without) — they diverge, and one day the dev console goes to production. He redesigns: one unique base with ${PORT:-3000}, adminer under profiles: [dev], .env per uncommitted environment. Dev: --profile dev up -d; prod: up -d, lean by default. One source of truth, two faces — prod never contains dev tools again.
This same case returns in the lesson, the diagram, the script and the 48h challenge.
1. Interpolation and .env: parameterise without duplicating
In compose.yml, ${PORT:-3000} reads the PORT variable or 3000 by default: the compose becomes PARAMETERISABLE. An .env file next to the compose sets values per environment (dev: published ports + adminer; prod: other values, no adminer) — loaded automatically. One file, N faces: the same image, the same structure, changing values.
Hygiene rule: the .env containing secrets is NEVER committed — version a dummy .env.example documenting the expected keys. And ${X:-…} defaults are chosen prod-safe: without a variable, the stack starts in its leanest form, not its most open.
2. -f overrides and profiles: variants and opt-in
The OVERRIDES: keep a BASE compose.yml and add variants — compose.override.yml, automatically loaded in dev (bind mounts, debug), or compose.prod.yml called explicitly (-f compose.yml -f compose.prod.yml). Each file describes only its DIFFERENCE; left to right, each -f overrides the previous one. No more full duplication per environment.
The PROFILES: profiles: [dev] on adminer makes it OPT-IN — absent by default, present with --profile dev. No more "I forgot to remove the console in prod": by default the stack is minimal and safe, dev tools activate explicitly. Minimal base, options in profiles, prod-safe default.
🔎 Concrete example — Rich dev, lean prod: same base, two faces
Idir parameterises the port, puts adminer in the dev profile, and launches each environment.
# compose.yml (socle)
services:
api:
build: ./api
ports: ["${PORT:-3000}:3000"]
networks: [appnet]
adminer:
image: adminer:4.8
profiles: [dev]
ports: ["8081:8080"]
networks: [appnet]
networks:
appnet:
# .env (dev, NON commité) : PORT=3000
# Dev : docker compose --profile dev up -d
# Prod : PORT=80 docker compose up -d
Reading: same base, adminer only in dev, port by variable.
Lesson: prod never contains dev tools — lean by default.
Section vocabulary
- Interpolation
- ${VAR:-default} resolved from the environment or .env: the parameterisable compose.
Simple example: Idir's ${PORT:-3000}, 3000 without a variable.
What it is not: a hard-coded port duplicated in two files.
Reflex: prod-safe default after each :-.
- Override
- Extra -f file modifying the base: each -f overrides the previous one, left to right.
Simple example: -f compose.yml -f compose.prod.yml in production.
What it is not: a second compose copied 95% identical.
Reflex: each variant describes only its difference.
- override
- Dev override (compose.override.yml) loaded automatically: binds, debug, without touching the base.
Simple example: Idir's live mounts in dev only.
What it is not: dev binds leaking into production.
Reflex: dev in the override, never in the base.
- Profile
- Opt-in group (profiles: [dev]) enabled by --profile: service absent by default.
Simple example: Adminer present only with --profile dev.
What it is not: the dev console shipped to prod by mistake.
Reflex: dev tools in profiles, lean stack by default.
- .env
- Local values file next to the compose, auto-loaded — uncommitted if it holds secrets.
Simple example: Idir's .env with PORT and local passwords.
What it is not: a .env with secrets pushed to the repo.
Reflex: version .env.example, never secrets.
In practice — Dev/prod without duplication
- Add ${PORT:-3000} and a dev profile (adminer) to your base compose.
- Launch without a profile: check that adminer is absent (lean stack).
- Relaunch with --profile dev: adminer appears — explicit opt-in.
- Change PORT via the environment and relaunch: the port follows, without touching the file.
Ready-to-say script — justifying the single base (30 s)
"A single base compose: the port comes from a variable, the dev console from an opt-in profile, prod differences from an override. Rich dev with --profile dev, lean prod by default — no more copied files diverging, no more dev tools leaking into prod."
Say in architecture review; show the empty diff between dev and prod.
Copy-paste template — parameterised base + dev profile
# Socle : dev riche en opt-in, prod sobre par défaut.
services:
api:
build: ./api
ports:
- "${PORT:-3000}:3000"
environment:
DATABASE_URL: ${DATABASE_URL}
networks:
- appnet
adminer:
image: adminer:4.8
profiles:
- dev
ports:
- "8081:8080"
networks:
- appnet
networks:
appnet:
# Dev : docker compose --profile dev up -d
# Prod : docker compose -f compose.yml -f compose.prod.yml up -d
Freelancer
Base + .env.example and each client declines without touching the file.
SME in Algiers
Idir unifies dev and prod: one base, two .env files, zero divergence.
Project team
Require minimal base + opt-in profiles + prod-safe defaults in the charter.
Beginner
This week: one ${PORT:-3000} variable + one service in a dev profile.
✓ Best practices
- Keep the base minimal, put options in opt-in profiles.
- Choose prod-safe defaults after each :- (${X:-…}).
- Version a .env.example, never real secrets.
✗ Common pitfalls
- Duplicating the whole file per environment: copies diverge.
- Dev profiles active by default: opt-in no longer exists, the console leaks into prod.
- Committing a .env with passwords: secret exposed in history.
Key takeaways
- ${VAR:-default} makes compose parameterisable: one file, N faces.
- OVERRIDES for differences (-f, dev override): never duplicate.
- OPT-IN profiles for tools: absent by default, --profile dev to enable.
- DEFAULT = safe prod; versioned .env.example, secrets never committed.
48h challenge — apply tomorrow
Merge 2 copied composes into 1 base + 1 dev profile + 1 override, and prove prod starts lean.
- My single base serves dev and prod with no copy-paste.
- My dev tools are opt-in, lean by default.
- My secrets are out of the repo, my defaults prod-safe.
Frequently asked questions
Where to place the .env, and how to keep it out of the repo?
NEXT TO compose.yml: loaded automatically, one per environment. If it holds secrets, never committed (add it to .gitignore) — provide a versioned .env.example with dummy values documenting each key. In prod, values come from the environment or a managed secret.
Several -f, in which order, and override or prod?
LEFT TO RIGHT: each -f overrides the previous one — base first, variant second (-f compose.yml -f compose.prod.yml). compose.override.yml is the exception: loaded AUTOMATICALLY (no -f), it carries dev (binds, debug). Rule: dev = auto override, prod = explicit -f, and each file describes only its difference.
Adminer must exist only in dev. Which mechanism?