Compose warns when ${VAR} in the YAML has no value, then substitutes empty — which surfaces later as a broken image name, empty password, or skipped config. Fix interpolation explicitly and guard required vars.
Compose reads .env from the project directory — the directory of the FIRST compose file. Running from elsewhere or after a fresh clone without copying .env.example leaves variables unset.
Variables set in the shell override .env; a variable exported empty (export VAR=) silently blanks the .env value, and quoting inside .env is literal (VAR="value" keeps the quotes).
docker compose config # prints exactly what will run; blanks are visible
cp .env.example .env # or: export VAR=value && docker compose up -d
# Compose spec: ${VAR:?error message} — fails hard with your message when unset
# ${VAR:-sane-default} — no warning, deterministic behavior
config substitution guards turn the silent-blank failure mode into an immediate, self-explanatory error — the single best hardening for compose files. compose config is the ground truth: it resolves interpolation, env files, and multiple -f merges exactly as the engine will see them.
It's the other way around: shell values take precedence. If the .env value seems ignored, check the shell (env | grep VAR) — an exported empty or stale value shadows the file.
For local dev it's the norm — but .env belongs in .gitignore, with .env.example committed as documentation. CI should inject secrets via the environment, not committed files.
A production-shaped compose stack: healthchecks, resource limits, log rotation. Never debug a boot race again.
Docker Production Starter — $19 →One-time. Yours to modify. Instant download from the NinjaOps template store.