Docker Compose: Environment variables betray you — the .env mistakes that break prod

Was this helpful?

The outage starts small. A container boots with NODE_ENV=development, or your database suddenly accepts
connections with a default password. Nothing “changed” in the application. The CI job is green. You shipped the same
Compose file you shipped last week.

What changed was the most fragile part of your deployment: the invisible string of environment variables running
through Docker Compose, your shell, and a tiny .env file that nobody reviews because it “isn’t code.”
It is code. It just doesn’t get linted.

A mental model that won’t lie to you

Docker Compose uses environment variables in two different ways, and most production failures happen when teams
treat them as the same thing:

1) Variables used by Compose itself (render-time)

These variables exist to render the Compose configuration: things like ${IMAGE_TAG} inside
compose.yaml, COMPOSE_PROJECT_NAME, or COMPOSE_PROFILES.
Compose resolves them before it starts containers. If Compose gets them wrong, your containers might not even be the
ones you think you deployed.

2) Variables passed into containers (runtime)

These variables are part of the container environment: what your app reads via getenv.
They come from environment:, env_file:, and sometimes from your shell via
implicit passthrough.

Render-time variables influence the final YAML. Runtime variables influence process behavior in the container.
Confusing these two is how you end up “fixing” a container bug by editing a host shell profile, then learning that
systemd doesn’t read your shell profile.

One operational truth: you can’t “just check the .env file.” You have to check what Compose
actually rendered and what the container actually received.

Quote to keep on your desk: paraphrased idea“Hope is not a strategy.” (paraphrased idea attributed to
Gordon Sullivan, often cited in engineering and reliability circles)

Joke #1: Environment variables are like office gossip—everyone swears they didn’t start it, but somehow it’s in every room.

Facts and history you should know (so you stop arguing with YAML)

  • Fact 1: The .env file used by Docker Compose is not automatically the same format as a shell script. It’s a simpler “KEY=VALUE” parser with its own quirks.
  • Fact 2: Compose originally grew out of Fig (2014), and a lot of its variable behavior is legacy convenience rather than pure design elegance.
  • Fact 3: Compose v2 is implemented as a Docker CLI plugin, and behavior can differ subtly between versions because the engine is now closer to the Docker CLI ecosystem.
  • Fact 4: Compose uses environment variables for both configuration rendering and container environments; different precedence rules apply for each path.
  • Fact 5: Variable interpolation happens before most validation. A missing variable can silently become an empty string and still form a “valid” YAML value.
  • Fact 6: env_file is runtime input for containers; it does not generally influence Compose’s own interpolation unless you explicitly load variables into the shell or use a toolchain that does.
  • Fact 7: The docker compose config command is the closest thing to a truth serum: it shows the fully rendered configuration Compose will run.
  • Fact 8: The same project on two hosts can render differently because Compose reads the host environment, current directory, and optional --env-file inputs.
  • Fact 9: COMPOSE_PROJECT_NAME affects network names, volume names, and container names. A project-name change can “orphan” old volumes and create brand-new ones.

Fast diagnosis playbook

When production is on fire, you don’t need philosophy. You need a sequence that rapidly narrows the blast radius.
Here’s the order I use because it separates “render-time” bugs from “runtime” bugs in minutes.

First: confirm what Compose rendered

  1. Run docker compose config and inspect interpolated values (image tags, ports, volume paths,
    project name, profiles). If the rendered config is wrong, don’t waste time inside containers.
  2. Check for empty strings, “null”-ish values, unexpected defaults, or duplicated service definitions due to profiles.

Second: confirm what the container actually received

  1. Inspect the container’s environment (docker inspect) or print inside the container
    (env).
  2. Compare it to what you think you set via env_file and environment.

Third: confirm which .env file and which host environment were used

  1. Verify the working directory and the selected env file. If you ran Compose from the wrong directory, you might
    be using the wrong .env.
  2. Check CI/CD: is it passing --env-file? Is it exporting variables? Is systemd wiping the environment?

If storage or networking looks weird, suspect project name and volume names

A changed COMPOSE_PROJECT_NAME or directory name can create new networks and new volumes.
The app “lost” its data because it’s writing to a different volume with a different name.

Practical tasks: commands, outputs, and decisions

These are the field tests. Each one includes: a command, what typical output means, and the decision you make.
Run them in order when you’re unsure where the truth is hiding.

Task 1: Verify Compose version (behavior varies)

cr0x@server:~$ docker compose version
Docker Compose version v2.24.6

Meaning: You’re on Compose v2.x. Good—most modern behavior and flags apply.
If this were v1, several flags and edge behaviors differ.
Decision: Capture this version in incident notes; if behavior differs across hosts, align versions.

Task 2: See what Compose thinks the project name is

cr0x@server:~$ docker compose ls
NAME                STATUS              CONFIG FILES
payments-prod        running(6)          /srv/payments/compose.yaml

Meaning: The project is payments-prod. Networks/volumes will be prefixed with that.
Decision: If you expected a different project name, stop: you may be operating on the wrong project.

Task 3: Render the fully interpolated config (the “truth”)

cr0x@server:~$ cd /srv/payments
cr0x@server:~$ docker compose config
services:
  api:
    environment:
      DB_HOST: db
      LOG_LEVEL: info
    image: registry.local/payments-api:1.9.3
    ports:
      - mode: ingress
        target: 8080
        published: "8080"
        protocol: tcp
  db:
    environment:
      POSTGRES_DB: payments
    image: postgres:15
volumes:
  payments-prod_db-data: {}
networks:
  default:
    name: payments-prod_default

Meaning: Interpolation happened. This is what Compose will run.
Decision: If the image tag or port is wrong here, the bug is in variable resolution (not in container runtime).

Task 4: Identify which env file is being used

cr0x@server:~$ ls -la /srv/payments/.env
-rw------- 1 root root 412 Jan  2 09:11 /srv/payments/.env

Meaning: A local .env exists in the project directory.
Decision: Verify you’re running Compose from this directory; otherwise you’re not reading this file.

Task 5: Spot whitespace and quoting landmines in .env

cr0x@server:~$ sed -n '1,120p' /srv/payments/.env
IMAGE_TAG=1.9.3
DB_PASSWORD=correct-horse-battery-staple
LOG_LEVEL=info
API_BASE_URL=https://payments.internal
BAD_SPACES =oops
QUOTED="literal quotes?"

Meaning: BAD_SPACES =oops is suspicious: many parsers treat that key as BAD_SPACES (with a trailing space) or reject it.
QUOTED="literal quotes?" may preserve quotes depending on parser.
Decision: Fix formatting: no spaces around =, avoid quotes unless you know the parser behavior.

Task 6: Check whether a variable is missing at render-time

cr0x@server:~$ grep -n 'IMAGE_TAG' -n /srv/payments/compose.yaml
12:    image: registry.local/payments-api:${IMAGE_TAG}

Meaning: Compose needs IMAGE_TAG to render the image string.
Decision: Ensure IMAGE_TAG is set in the right .env or exported in the environment used by Compose.

Task 7: Detect silent empty interpolation

cr0x@server:~$ IMAGE_TAG= docker compose config | grep -n 'image:'
7:    image: registry.local/payments-api:

Meaning: Empty IMAGE_TAG renders an invalid-ish image reference that might still pass YAML parsing.
Decision: Add required-variable guards using Compose interpolation defaults (see later) and fail CI on empties.

Task 8: Inspect environment inside a running container

cr0x@server:~$ docker compose exec -T api env | egrep 'DB_|LOG_LEVEL|API_BASE_URL'
API_BASE_URL=https://payments.internal
DB_HOST=db
LOG_LEVEL=info

Meaning: The container got variables. If something is missing, it’s a runtime env injection problem.
Decision: Compare against docker compose config and the env_file contents.

Task 9: Confirm what Docker thinks the environment is (authoritative)

cr0x@server:~$ docker inspect payments-prod-api-1 --format '{{json .Config.Env}}'
["API_BASE_URL=https://payments.internal","DB_HOST=db","LOG_LEVEL=info","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]

Meaning: This is what Docker will pass to the process. If it’s not here, your app won’t see it.
Decision: If Compose config shows it but inspect doesn’t, you have a deployment drift or a recreate issue.

Task 10: Detect “didn’t recreate container” problems

cr0x@server:~$ docker compose up -d
[+] Running 2/2
 ✔ Container payments-prod-db-1   Running
 ✔ Container payments-prod-api-1  Running

Meaning: Compose didn’t recreate containers. Env changes won’t apply to a running container unless it’s recreated.
Decision: If you changed env vars, force recreate: docker compose up -d --force-recreate.

Task 11: Force recreate and confirm new env is applied

cr0x@server:~$ docker compose up -d --force-recreate
[+] Running 2/2
 ✔ Container payments-prod-db-1   Running
 ✔ Container payments-prod-api-1  Started

Meaning: The API container was restarted/recreated.
Decision: Re-run Task 8/9 to confirm environment changes actually landed.

Task 12: Detect project-name drift that creates “new” volumes

cr0x@server:~$ docker volume ls | grep -E 'payments.*db-data'
local     payments-prod_db-data
local     payments_db-data

Meaning: Two similarly named volumes exist, likely from different project names.
Decision: Confirm which volume is attached to the running DB container before you “clean up” anything.

Task 13: Confirm which volume a container is actually using

cr0x@server:~$ docker inspect payments-prod-db-1 --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}'
payments-prod_db-data /var/lib/postgresql/data

Meaning: The DB is using payments-prod_db-data.
Decision: If the app “lost data,” compare with the volume you expected. Don’t rm volumes until you prove they’re unused.

Task 14: Identify which .env is used when you run from a different directory

cr0x@server:~$ cd /tmp
cr0x@server:~$ docker compose -f /srv/payments/compose.yaml config | head
services:
  api:
    image: registry.local/payments-api:

Meaning: Running from /tmp likely caused Compose to miss the intended /srv/payments/.env, so interpolation failed.
Decision: Always run from the project directory or supply --env-file /srv/payments/.env.

Task 15: Prove env precedence between host and .env

cr0x@server:~$ cd /srv/payments
cr0x@server:~$ export IMAGE_TAG=2.0.0
cr0x@server:~$ docker compose config | grep -n 'image:'
7:    image: registry.local/payments-api:2.0.0

Meaning: The exported host variable overrode the value in .env.
Decision: In production, avoid relying on “whatever is exported in the shell.” Make the env source explicit.

Task 16: Detect accidental Windows CRLF in .env (yes, still)

cr0x@server:~$ file /srv/payments/.env
/srv/payments/.env: ASCII text, with CRLF line terminators

Meaning: CRLF can sneak into keys/values, causing baffling “variable not found” or values with hidden \r.
Decision: Convert to LF: sed -i 's/\r$//' /srv/payments/.env, then re-render config.

Task 17: Confirm hidden carriage returns in a specific value

cr0x@server:~$ python3 -c 'import os;print(repr(open("/srv/payments/.env","rb").read().splitlines()[-1]))'
b'QUOTED="literal quotes?"\r'

Meaning: That trailing \r is real. It can break auth tokens, URLs, or passwords.
Decision: Normalize line endings in CI, and treat .env as a text artifact that needs checks.

Task 18: Show differences between env_file and environment in the final config

cr0x@server:~$ docker compose config | sed -n '1,80p'
services:
  api:
    environment:
      DB_HOST: db
      LOG_LEVEL: info
    image: registry.local/payments-api:1.9.3

Meaning: You can see inline environment: values clearly. If you used env_file, it may not inline-expand in output the way you expect.
Decision: If your auditing depends on seeing variables, don’t rely on env_file alone; use explicit config validation steps.

Precedence and scope: who wins when variables collide

Most teams can’t answer this question without guessing: “If I set FOO in the shell, in .env,
and in environment:, which one wins?” The answer depends on whether you mean render-time or runtime.
That’s why this keeps breaking in production: people talk past each other.

Render-time precedence (interpolation in compose.yaml)

When Compose interpolates ${VAR} in the YAML, it looks at its environment sources. In practice, the
exported environment of the Compose process is the heavyweight contender. The local .env is often a
fallback convenience.

In other words: if your CI exports IMAGE_TAG, it will typically override .env. If your
systemd unit runs Compose with a mostly empty environment, it will ignore what your interactive shell had.

Operational rule: render-time variables must be explicit. Either pass them via CI in a controlled
way or supply an explicit env file via --env-file. Don’t let random shells decide.

Runtime precedence (what the container gets)

Container environment is built from Compose service definitions:

  • environment: entries are explicit and visible in the Compose file.
  • env_file: loads key/value pairs from a file into the container environment.
  • Some variables can be passed through from the host if you reference them in environment: without a value, depending on syntax.

Practical rule: treat environment: as an API for what the container expects and
treat env_file as an implementation detail for how you feed it values. When debugging,
always check what actually arrived in docker inspect.

Project name is secretly part of your environment story

COMPOSE_PROJECT_NAME feels like “just naming.” It’s not.
It changes network names and volume names. If you tie your data to volumes and your monitoring to container names,
project name is a production variable, whether you admit it or not.

Interpolation and parsing: the sharp edges in .env and Compose

The .env format looks like shell. It is not shell. It’s a key/value file with just enough flexibility
to make you overconfident.

Whitespace: the silent killer

Many parsers treat KEY =value as a different key than KEY=value, or they reject it.
Either way, you end up with “KEY not set” and Compose quietly substitutes an empty string.

Don’t “be tolerant.” Be strict. For production env files:
no spaces around equals, and keys should match [A-Z0-9_]+.

Quotes: sometimes literal, sometimes stripped, always confusing

In some ecosystems, FOO="bar" means the value is bar without quotes. In others, those
quotes are part of the value. Compose’s behavior can surprise you depending on which parsing path you’re hitting.
The only safe stance: avoid quotes in .env unless you’ve verified the behavior with docker compose config and a running container.

Interpolation defaults: use them, but understand them

Compose supports patterns like:
${VAR:-default} and ${VAR?error} in many contexts.
This is where teams can turn invisible failure into loud failure.

If IMAGE_TAG must exist in prod, make it required. If LOG_LEVEL can default, default it.
Fail fast on anything that changes behavior in ways you can’t see.

Empty is different from unset

Compose interpolation often treats an empty variable as “set,” which can defeat defaults. If a pipeline sets
IMAGE_TAG to an empty string (yes, it happens), your ${IMAGE_TAG:-latest} may or may not behave as you expect.
Test this explicitly in your environment.

.env vs env_file: same syntax vibe, different semantics

The .env file (for Compose) is used for Compose’s variable interpolation and some Compose settings.
The env_file: directive feeds runtime environment to containers.
People mix them up because the file contents look identical. The result is reliably chaotic.

If you want values to influence interpolation, they need to be in the environment Compose uses for rendering
(shell export, explicit --env-file, or your orchestration’s env handling). If you want values inside the container,
they need to be in environment: or env_file:.

Joke #2: A .env file is a lot like a toddler—quiet doesn’t mean fine, it means you should check immediately.

Three corporate mini-stories from the trenches

Story 1: The incident caused by a wrong assumption

A fintech team ran a customer-facing API on a pair of VMs with Docker Compose. They had a .env in the repo
and a separate prod.env stored on the host. In their heads, “Compose loads env from env_file.” They were
half right and entirely doomed.

The Compose file used ${IMAGE_TAG} to pin the API image. The container runtime variables came from
env_file: ./prod.env. A new release candidate needed a hotfix, so an engineer updated IMAGE_TAG
in prod.env, ran docker compose up -d, and expected the new image to roll out.

It didn’t. Compose interpolation didn’t look at env_file for rendering the image: field.
The containers stayed on the old tag. Meanwhile, the engineer also updated a runtime variable in prod.env
and assumed the container picked it up; it didn’t, because Compose didn’t recreate the container. So now they had
old code, old env, and a new belief.

Two hours later, the API was throwing errors that looked like a regression in the hotfix. It wasn’t. The hotfix
never deployed. Their monitoring showed “deployment succeeded” because the job completed; it didn’t validate
docker compose config output or check the running container image IDs.

The fix was boring: make image tags a required render-time variable and set it explicitly in the deployment command,
verify with docker compose config, then force recreate or roll containers properly. They also stopped
using prod.env as a magic file that “controls everything.” It controls exactly what you wire it to.

Story 2: The optimization that backfired

A media company wanted faster deployments. Someone noticed that recreating containers takes time, especially for a
service with many sidecars. They changed the process: update .env, then run docker compose up -d
without forcing recreate, to “avoid downtime.”

For a while, it seemed to work—because most changes were image tag changes, and Compose would pull and restart
services when it detected a new image. But environment variables are not images. A critical configuration change
toggled a feature flag for request routing. Half the fleet updated (new nodes where containers happened to be recreated),
half didn’t. The result was a split-brain behavior where requests took different paths depending on which VM
got them.

The debugging was painful because the Compose file looked correct, the .env looked correct, and the
containers were all “up.” The bug was in process: they optimized away the one step that reliably applies env changes.
They had introduced non-determinism into configuration rollout.

The recovery playbook ended up being blunt: if env changed, containers are recreated. If you want zero-downtime,
you do it with load balancers and rolling restarts, not by hoping Compose will infer your intent.

Story 3: The boring but correct practice that saved the day

A B2B SaaS team ran Compose-based stacks for internal services: metrics, job runners, and a legacy database.
They were allergic to “clever.” Their production deployment required three checks:
render the config, validate the running image IDs, and record the effective environment checksum.

One Friday, a change merged that introduced a new variable RATE_LIMIT_MODE used in Compose interpolation
to select a sidecar image. The developer added it to .env.example but forgot the production env source.
The CI pipeline wasn’t exporting it either.

The deployment job failed early because their Compose file used ${RATE_LIMIT_MODE?must be set}.
That’s the whole trick: they turned silent empty interpolation into a hard stop. No partial deploy, no mystery behavior.

They fixed the pipeline, deployed on Monday, and nobody got paged. It was so uneventful that it annoyed the team.
That’s how you know it was correct.

Common mistakes: symptom → root cause → fix

1) Symptom: image tag becomes blank or “latest” unexpectedly

Root cause: Render-time variable missing or empty, Compose interpolates to empty string; or CI exports an empty variable overriding .env.

Fix: Use required interpolation: image: myapp:${IMAGE_TAG?set IMAGE_TAG}. In CI, fail if IMAGE_TAG is empty. Validate with docker compose config.

2) Symptom: “I updated .env but the container didn’t change behavior”

Root cause: Container wasn’t recreated; running container keeps old environment.

Fix: Apply changes with docker compose up -d --force-recreate (or docker compose restart if appropriate, but recreation is safer for env changes). Verify with docker inspect ... Config.Env.

3) Symptom: prod uses dev settings even though prod.env exists

Root cause: Compose is reading .env from the current working directory, not the intended path; or --env-file not supplied in automation.

Fix: In systemd/CI, run from the project directory or specify --env-file /srv/app/.env. Add a guard that prints the env file checksum during deploy.

4) Symptom: password authentication fails, but the value “looks right”

Root cause: CRLF or trailing whitespace in .env injects hidden characters (often \r) into the value.

Fix: Normalize line endings (sed -i 's/\r$//'), and validate by printing repr or hexdump of the value in a controlled test container.

5) Symptom: database “lost” data after a redeploy

Root cause: Project name changed (directory name change, COMPOSE_PROJECT_NAME change), creating a new volume with a different name.

Fix: Pin project name explicitly for production. Audit docker volume ls and docker inspect mounts before cleanup. Treat volume names as part of state.

6) Symptom: variables in container don’t match what’s in .env

Root cause: Confusing .env (Compose render-time) with env_file (container runtime); or host environment overriding values.

Fix: Decide which source is authoritative. For critical runtime values, use environment: keys explicitly and source them from a controlled env file. For render-time, pass via --env-file and validate config output.

7) Symptom: a variable with quotes behaves oddly

Root cause: Quotes treated literally or stripped differently than expected; different parsers in toolchain.

Fix: Remove quotes in .env unless necessary. When necessary, validate with docker compose config and inspect inside container.

8) Symptom: service doesn’t start, port mapping is nonsense

Root cause: Interpolation produced an invalid port string (empty, non-numeric, includes whitespace), but YAML still parses.

Fix: Require variables and validate ports in CI by grepping rendered config. Use defaults only for safe dev values.

9) Symptom: “works locally, fails in CI” with the same Compose file

Root cause: Local shell exports variables and CI does not; or CI sets different locale/newlines; or CI runs from a different directory.

Fix: Make the env source explicit in CI. Print docker compose config (or at least the relevant lines) and ensure it’s deterministic.

10) Symptom: secrets show up in logs or support bundles

Root cause: Storing secrets in .env and printing rendered config or container env during debugging; env vars leak easily via process listings and crash dumps.

Fix: Use Compose secrets where possible, or file-mounted credentials with tight permissions. In incident tooling, redact env outputs by default.

Checklists / step-by-step plan for production

Checklist A: Make Compose interpolation deterministic

  1. Pin the env source: In automation, always run with an explicit --env-file path and a fixed working directory.
  2. Require critical variables: Use ${VAR?message} for image tags, external endpoints, and project names.
  3. Stop exporting random variables: Clean the environment in CI jobs. If it’s needed, set it explicitly.
  4. Render and diff: Store the output of docker compose config as a build artifact and diff it against previous deployments.

Checklist B: Make container runtime env auditable

  1. Document the contract: List required runtime env vars per service (names, meaning, allowed values).
  2. Prefer explicit environment: keys: It makes the contract visible in code review.
  3. Use env_file for bulk values, not mystery behavior: Keep it minimal and structured. Avoid mixing “dev” and “prod” in the same file.
  4. Recreate on env changes: If runtime env changed, containers must be recreated. Plan downtime/rolling behavior accordingly.

Checklist C: Don’t let state drift (volumes/networks)

  1. Pin project name: Set name: in the Compose model or COMPOSE_PROJECT_NAME in a controlled env source.
  2. Declare volumes explicitly: Use named volumes for stateful services; avoid accidental anonymous volumes.
  3. Audit before cleanup: Always inspect mounts and container references before deleting volumes.

Checklist D: Treat .env as production code

  1. Permissions: chmod 600 .env if it contains sensitive material.
  2. Normalize line endings: Enforce LF in CI.
  3. Lint rules: No spaces around =, no tabs, no trailing whitespace, predictable key patterns.
  4. Change control: Require review for env changes, and keep a history (even if the file is stored securely outside Git).

Operational guidance that prevents most .env incidents

Use defaults only for developer ergonomics, not for production safety

Defaults like ${LOG_LEVEL:-debug} are fine for local work. In production they can turn missing config
into surprising behavior. Prefer explicit values in production env sources and required variables for anything that
changes data integrity, auth, or routing.

Fail early on the host, not late in the container

If a variable is required, fail at render-time. You want the deployment to stop before it pulls images, before it
touches volumes, before it restarts anything. It’s cheaper and safer.

Stop treating secrets as “just env vars”

Environment variables leak. They leak into crash reports, debug endpoints, process tables, accidental support
bundles, and human screenshots. They also stick around in container metadata longer than you expect.
Use secret mechanisms when you can. If you can’t, at least separate secrets from non-secrets and design your
diagnostic commands to redact by default.

Make configuration observable

Your system should report the effective configuration version without dumping secrets. A config checksum,
a git SHA, an image digest, and a non-secret “mode” variable are usually enough to confirm the system is what you
think it is.

FAQ

1) Does Compose automatically load .env?

Typically, yes—.env in the project directory is used as a convenient source for Compose variable
interpolation and certain Compose settings. But “project directory” depends on where you run the command and how
you reference the Compose file. If you run from the wrong directory, you can silently load the wrong .env
or none at all.

2) Is .env the same as env_file?

No. .env commonly influences Compose render-time interpolation. env_file injects
variables into the container at runtime. The files look similar; the semantics are different. Confusing them is a
classic failure mode.

3) Why didn’t my .env change apply after docker compose up -d?

Because containers don’t magically absorb new environment variables. If Compose doesn’t recreate the container,
the running environment stays the same. Use docker compose up -d --force-recreate when env changed,
and verify via docker inspect.

4) Which wins: exported shell variables or .env?

In many common setups, exported variables in the environment running Compose override values from .env.
This is why “works on my machine” happens: your shell exports something that CI doesn’t, or vice versa. Make the env
source explicit in automation.

5) Can I have multiple env files?

Yes, but be intentional about purpose: one for render-time (passed with --env-file) and possibly one
or more for runtime injection (env_file: per service). Avoid layering so many files that nobody can
predict the result.

6) Why does my app see quotes in values?

Because your parser might treat quotes literally. The .env format is not a universal standard and
different tools interpret quotes and escapes differently. If you require special characters, test the exact path:
render-time via docker compose config and runtime via docker inspect.

7) How do I prevent empty variables from slipping into production?

Use required interpolation (${VAR?message}) for critical values and add CI checks that fail if
rendered config contains blank image tags, blank ports, or empty hostnames. This is one of the highest leverage
fixes you can ship.

8) Why did redeploying create new volumes and “wipe data”?

Likely a project name change. Compose prefixes volume and network names with the project name, which comes from
directory name, explicit configuration, or environment. Pin it for prod so volumes remain stable. Then confirm the
DB container is attached to the intended volume before cleanup.

9) Is it safe to print docker compose config in CI logs?

Not always. If you inline secrets in the Compose file or interpolate them into fields shown in output, you can leak
credentials. If you must print config, redact sensitive keys or print only targeted lines (image references, ports,
non-secret settings).

10) When should I use Compose secrets instead of env vars?

Use secrets when you can: credentials, API tokens, private keys, anything you’d regret seeing in a log or crash dump.
Env vars are fine for non-sensitive configuration and feature toggles. If you must use env vars for secrets, lock
down permissions and reduce where they’re displayed.

Next steps you can do this week

  1. Add a “render check” to CI: run docker compose config and fail on empty critical fields
    (image tags, ports, hostnames). Save the rendered config as an artifact with secrets redacted.
  2. Make critical variables required: convert ${VAR} to ${VAR?set VAR} for
    production-critical interpolation points.
  3. Pin project name in production: stop accidental volume and network drift. Treat it like state.
  4. Standardize deployment execution: fixed working directory, explicit --env-file,
    and a policy: env changes require recreate or rolling restart.
  5. Stop storing secrets in casual .env files: move them to a secrets mechanism or file mounts and
    adjust diagnostic tooling to avoid leaking them during incidents.

Docker Compose is fine. It’s the unspoken assumptions around .env that are not fine.
Make variables explicit, rendered config observable, and container env verifiable.
Then the next “mystery regression” becomes a five-minute diff instead of a weekend.

← Previous
Google Search Console “Page with redirect”: When It’s Fine and When It Hurts
Next →
ZFS ZVOL vs Dataset: The Decision That Changes Your Future Pain

Leave a comment