Intro
Environment variables are the primary way to configure containers in Docker Compose without modifying images. They control database credentials, feature flags, API endpoints, and resource limits. When variables are missing, mistyped, or exposed, services fail at startup or leak secrets into logs. This guide walks through the commands and patterns you need to observe, set, validate, and recover environment variables in Docker Compose projects. It is written for developers, DevOps engineers, and platform teams who run multi-container applications locally and in production.
You will learn how to inspect the current Compose version and project, understand variable precedence and substitution, use env files safely, pass variables at build and runtime, run read-only checks, diagnose common failures, and implement a repeatable operational checklist. Each section includes concrete commands with expected output, failure signals, and recovery steps.
Version and Environment Inventory
Before changing any variable, establish what you are running and where configuration lives. Start with read-only observations. Run docker compose version to confirm the CLI version:
docker compose version
Expected output looks like:
Docker Compose version v2.24.6
Older v1 projects may use docker-compose (with a hyphen). If you see command not found, you are either using an old installation or the plugin is missing. Use docker compose version to confirm v2. For projects that must support v1, the syntax for environment variables is mostly identical, but Compose v2 has better interpolation and named profiles.
Confirm the active project name and configuration files with:
docker compose ls
This lists running Compose projects, their status, and config file paths. If the project name is unexpected, check the COMPOSE_PROJECT_NAME variable or the -p flag. Project name affects network names, volume names, and container names, so it matters for troubleshooting.
Inspect the resolved configuration without applying changes:
docker compose config
This prints the fully merged YAML with all variable substitutions applied. It does not start containers. Compare this against your docker-compose.yml to see exactly which values will be used. For a specific service, run docker compose config <service>. If the command errors out with invalid interpolation or required variable is not set, you have a missing variable that must be addressed before any container starts.
List all environment variables currently visible to a running container:
docker compose exec <service> env
or, without entering the container:
docker inspect <container> --format '{{range .Config.Env}}{{println .}}{{end}}'
This output includes variables from the image, the Compose file, the shell environment, and env files. Use it to audit whether secrets are unintentionally exposed. Never print sensitive values to CI logs; use docker inspect with a filtered template or an env-file diff instead.
Record the Docker and Compose versions, the project name, the config file path, and a timestamp before any change. A simple inventory command sequence might look like:
docker version --format '{{.Server.Version}}'
docker compose version
docker compose ls
docker compose config --quiet
date -u +"%Y-%m-%dT%H:%M:%SZ"
Save this output to a temporary file or shared channel. It is your baseline for rollback.
Safe Configuration Path
Docker Compose resolves environment variables from several sources in a strict order. Knowing this order prevents unexpected overrides.
Precedence, from highest to lowest, in Compose v2:
- Shell environment variables (those already exported in the current session)
- Variables from the
.envfile in the project directory (for--env-filesee below) - Variables set in the
environment:section of the service - Variables defined in an
env_file:list (later files override earlier, and these are injected into the container but not used for interpolation in the Compose file itself) - Dockerfile
ENVinstructions in the image
A practical example:
Given a docker-compose.yml:
services:
web:
image: nginx:alpine
environment:
- API_URL=http://api:8080
- DEBUG=false
env_file:
- ./common.env
And common.env:
API_URL=http://localhost:3000
DEBUG=true
If you run docker compose config, you will see that API_URL resolves to http://api:8080 because environment: overrides env_file:. However, inside the running container, the DEBUG variable will be true from common.env unless it is also in environment:. This duality confuses many operators: environment: is only for interpolation and container runtime, while env_file: only injects values into the container; it does not make them available for ${VAR} substitution in the Compose file.
To make substitution explicit and avoid surprises, use an .env file located next to docker-compose.yml (or referenced via --env-file). The default .env file is used only for interpolation in the Compose file; it is not automatically injected into containers. To have variables available both for interpolation and inside the container, you can either:
- Set them in the shell before running
docker compose up - Reference them explicitly in
environment:using${VAR} - Use multiple
env_file:entries for runtime variables and a separate.envfor Compose file interpolation
Example of a .env file for interpolation:
POSTGRES_VERSION=16.2
APP_PORT=8080
And in docker-compose.yml:
services:
db:
image: postgres:${POSTGRES_VERSION}
ports:
- "${APP_PORT}:5432"
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
Here DB_PASSWORD must also be in the .env file or the shell, otherwise docker compose config fails. If you need to pass sensitive values, use Docker secrets or external secret stores, not plain env files committed to source control.
A safe workflow when changing an environment variable:
- Copy the current
docker-compose.ymland.envto a backup location. - Run
docker compose configto see the resolved output. Redirect it to a file with> /tmp/compose-resolved.ymland inspect for unintended variable replacements or exposed secrets. - Change one variable at a time. If multiple variables interact, document the dependency. For example, changing
DATABASE_URLmay require changingREDIS_URLif they share a hostname. - Apply the change with
docker compose up -d --force-recreate <service>only afterdocker compose configsucceeds. - Verify with a health check or log inspection (see Verification and Diagnostics).
Never edit the .env file directly on a production server without version control. Use a controlled deploy process that applies the same change to Staging first, then Production, with a rollback plan (e.g., revert the file and run docker compose up -d again).
Verification and Diagnostics
After changing environment variables, verify the service actually picked up the new values. Do not assume success from docker compose up -d producing no errors; a service can start and still be misconfigured.
First, check container status:
docker compose ps
Expected healthy output:
NAME COMMAND SERVICE STATUS PORTS
myapp-web-1 "nginx -g 'daemon of…" web running (healthy) 0.0.0.0:8080->80/tcp
If the status is restarting, unhealthy, or exited, inspect logs immediately:
docker compose logs --tail=50 <service>
For a misconfigured environment variable, typical log messages include:
panic: runtime error: invalid memory address or nil pointer dereference(common in Go apps when a required env var is empty)Error: connect ECONNREFUSED 127.0.0.1:5432(database host or port wrong fromDATABASE_URL)The server requested authentication method unknown to the client(Postgres auth variable mismatch)Error: Missing required environment variable: API_KEY
Use docker compose exec to inspect the live variable value inside the container:
docker compose exec web printenv DEBUG
If the container is restarting too quickly, use docker inspect on the container ID:
docker inspect <container-id> --format '{{.State.Status}} {{.State.ExitCode}}'
For PostgreSQL containers, you can also verify the configured password by connecting:
docker compose exec db psql -U postgres -c "SELECT current_user, current_database();"
If authentication fails, the POSTGRES_PASSWORD variable likely did not reach the container. Check docker compose config output again.
To diagnose variable precedence issues, compare the resolved config against the container's actual environment:
# Resolved Compose environment for service web:
docker compose config web | grep -A5 "environment"
# Actual container environment:
docker inspect $(docker compose ps -q web) --format '{{range .Config.Env}}{{println .}}{{end}}'
Any mismatch indicates an override from env_file, shell, or image defaults.
Run a restart test to confirm persistence of environment-driven configuration. For a stateful service like a database, stop and recreate the container, then verify data and configuration remain:
docker compose stop db
docker compose rm -f db
docker compose up -d db
# After start, check a known variable:
docker compose exec db printenv POSTGRES_DB
If the variable is empty or the database fails to start, the variable was not set in the Compose file but perhaps in a previous shell session or the image's default. This is a common failure when moving from local to CI.
Failure Modes and Recovery
Several common mistakes cause environment variable problems in Docker Compose. Understanding why they happen and how to recover saves time and prevents production incidents.
1. Variable interpolation fails during docker compose up
Symptom:
ERROR: The interpolation of '${DB_PASSWORD}' in 'services.db.environment.POSTGRES_PASSWORD' is not valid. Required variable DB_PASSWORD is missing.
Why: You referenced ${DB_PASSWORD} in the Compose file, but it is not defined in the shell environment or in the .env file. Compose v2 only reads the .env file in the project directory (or specified by --env-file), not any env_file: entries.
Recovery:
- Add the variable to
.envin the same directory asdocker-compose.yml. - Or export it in the shell before running Compose:
export DB_PASSWORD=yourpassword. - Or pass it explicitly:
docker compose --env-file ./config/secrets.env up -d. - Avoid committing secrets to
.env; use a CI secret manager and write the file during deployment.
2. Variables from env_file are not used for substitution
Symptom: You set DATABASE_URL=postgres://user:pass@db:5432/mydb in env_file, but the Compose file still shows ${DATABASE_URL} unresolved in cmd or entrypoint.
Why: env_file injects variables into the container at runtime; it is not available to the Compose file parser. The parser only uses shell env vars and the .env file.
Recovery: Move the variable to the .env file if you need it for interpolation, or reference it directly in the command with a hardcoded value. Remember that .env variables are not automatically injected into containers; you must also list them under environment: or env_file: if the application needs them.
3. Sensitive variables leaked via docker inspect or docker compose config
Symptom: Running docker inspect shows passwords, tokens, or API keys in the environment list. These values may end up in CI logs, support bundles, or monitoring dashboards.
Why: Passing secrets through environment: or env_file: makes them visible to anyone with Docker API access on the host. They are also stored in container metadata.
Recovery:
- Use Docker secrets if running in Swarm mode, or bind-mounted secret files for Compose projects. For example:
services:
web:
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
- At minimum, use an external secret manager and inject secrets via the container entrypoint from a mounted file.
- Avoid printing
envoutput in CI. Usedocker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -v 'PASSWORD\|TOKEN\|KEY'to filter sensitive keys, but never trust filters alone; the value may appear in command lines or health checks.
4. Host environment overrides .env unexpectedly
Symptom: In CI or on a developer machine, the same Compose file resolves a variable differently than on another machine. docker compose config shows a different value than what is in .env.
Why: Shell environment variables have higher precedence than .env file variables. If a developer has DEBUG=true exported in their shell, it overrides DEBUG=false in .env. This leads to inconsistent behavior across environments.
Recovery:
- Run
docker compose configlocally and in the environment to compare. If you need a deterministic build, unset conflicting variables or useenv -i docker compose configto see the baseline. - Document which variables are allowed to be overridden by the shell and which must come from
.envor CI secrets. - Consider using a dedicated project directory with no inherited shell variables.
5. Changes to .env not picked up after docker compose up
Symptom: You edit .env, run docker compose up -d, but the running containers still use old values.
Why: Environment variables are fixed at container creation. Updating .env alone does not recreate existing containers. docker compose up -d only recreates containers with changed image or service config; it may not detect a change in the resolved environment if the Compose file itself has not changed.
Recovery:
- Force recreation of the affected service:
docker compose up -d --force-recreate <service>. - Or stop and remove the containers, then start again:
docker compose down && docker compose up -d. - Before forcing recreation, run
docker compose configto confirm the new resolved values.
6. Using :ro or :rw modifiers in env_file paths incorrectly
Symptom: Compose fails to parse an env_file entry like ./env/common.env:ro.
Why: env_file does not support mount modifiers; that syntax is only for volumes. It is a common copy-paste mistake.
Recovery: Remove the modifier: - ./env/common.env. If you need the file to be read-only inside the container, place it in a bind mount and source it via env_file using the container path.
Operations Checklist
Use this checklist before and after any change to Docker Compose environment variables. It assumes a production-like staging environment first, then production. Each item has an owner and a review frequency.
Before change (owner: DevOps engineer or service owner; review: every deploy)
- [ ] Record current
docker compose versionanddocker compose configoutput in a dated log file. - [ ] Identify all services affected by the variable change using
docker compose config --services. - [ ] Confirm the variable is not hardcoded in the image or overwritten by a higher-precedence source.
- [ ] Check for usage of
${VAR}in anycommand:,entrypoint:,healthcheck:, orenvironment:sections. - [ ] Ensure secrets are not currently in
.envorenv_filethat is committed to source control. - [ ] Define the expected value and a validation command for each variable (e.g.,
docker compose exec web env | grep API_URLmust returnhttp://api:8080). - [ ] Back up the current Compose file and
.envto a versioned location (e.g., Git tag or artifact store).
After change (owner: same engineer; verify within 30 minutes of deploy)
- [ ] Run
docker compose configand compare with expected output. - [ ] Start or recreate the service with
docker compose up -d --force-recreate <service>. - [ ] Check service health with
docker compose ps(look forrunning (healthy)). - [ ] Inspect logs for errors related to missing/invalid variables.
- [ ] Run the validation command for the changed variable.
- [ ] Perform a restart test:
docker compose stop <service> && docker compose start <service>and revalidate. - [ ] Confirm no sensitive values appear in
docker inspectoutput or logs. - [ ] Update runbooks or documentation with the new variable and its source of truth.
Rollback plan (owner: on-call engineer; to be executed if post-change checks fail)
- [ ] Revert the
.envand Compose file changes from version control. - [ ] Run
docker compose up -d --force-recreatewith the previous configuration. - [ ] Verify the previous known-good variable values are restored.
- [ ] Notify the team and record the incident timeline.
This checklist is not a substitute for CI tests. Add a pipeline step that runs docker compose config --quiet and a smoke test that asserts a service can fetch a known variable. For example, an Nginx container can expose a header with add_header X-App-Env $APP_ENV;, and the test checks the header equals staging or production.
Common Pitfalls and How to Avoid Them
The following pitfalls appear repeatedly in real-world Compose projects. Each includes a concrete example and a preventive measure.
Pitfall 1: Assuming env_file variables are available for Compose interpolation
As noted, env_file is runtime-only. Developers often write:
services:
app:
image: myapp
env_file:
- ./app.env
environment:
- LOG_LEVEL=${LOG_LEVEL}
They expect ${LOG_LEVEL} to be resolved from app.env. It is not. The correct approach is to put LOG_LEVEL in .env or export it in the shell, and optionally also list it in env_file if the app needs it inside the container. A safer pattern is to avoid env_file for variables used in the Compose file altogether; use .env for interpolation and environment: for explicit runtime values.
Pitfall 2: Mixing development and production .env files
Using a single .env for all environments leads to secrets leaking into development containers or production values bleeding into local tests. Instead, use environment-specific files:
.env.staging
.env.production
Deploy with docker compose --env-file .env.production up -d. Keep these files in a secure store, not the Git repository.
Pitfall 3: Overriding a variable in environment: but forgetting the dependency in another service
Suppose service web and worker both need API_URL. You change API_URL only for web. The worker silently keeps the old URL, causing split-brain behavior. To avoid this, define shared variables in a common .env and reference them in both services, or use YAML anchors and a single source of truth. Run docker compose config and grep for the variable name to see all occurrences:
docker compose config | grep -n "API_URL"
Pitfall 4: Not quoting variable values with special characters
YAML parsing can misinterpret values containing :, #, -, or spaces. For example:
environment:
- GREETING=Hello, World! # fails because comma and space are okay? Actually the comma is fine, but the `!` might cause issues in some YAML parsers. Better to quote:
- GREETING="Hello, World!"
- DSN=postgres://user:pass@db:5432/mydb?sslmode=disable # colon may cause issues? Actually colon is okay if no space after it. But `#` could comment out the rest.
Always quote values that contain special characters, including *, ?, [, ], {, }, #, &, !, |, >, %, @, and backticks. Use single quotes for literal interpretation and double quotes when you need to include interpolation. Example:
environment:
- "PASSWORD=pa$$w0rd" # double quotes prevent $ from interpolating
- 'REGEX=[0-9]+'
Pitfall 5: Relying on default values without verifying
Compose allows default values with ${VAR:-default}. This can hide typos. If you expect POSTGRES_USER to be admin but the .env has POSTGRES_USR=admin (typo), the variable resolves to the default postgres, which may still work but with the wrong user. Avoid defaults for critical variables, or add a validation step:
environment:
- POSTGRES_USER=${POSTGRES_USER:?POSTGRES_USER is required}
The :? syntax forces an error if the variable is empty, making mistakes visible.
Pitfall 6: Storing secrets in docker-compose.override.yml and committing it
The override file is convenient for local overrides but often contains secrets like database passwords. If it is committed to source control, secrets leak. Use a .gitignore for override files that contain secrets, or better, never put secrets in Compose files; use Docker secrets (Swarm) or bind-mounted files from a secure location.
Conclusion
Environment variables are the control plane for containerized applications. In Docker Compose, managing them correctly requires understanding the resolution order, separating build-time from runtime configuration, and verifying changes with observable commands. The commands in this article provide a repeatable workflow: inventory the current state, make one scoped change, validate the resolved configuration, observe the running container, and have a rollback path.
Start with a single low-risk variable change in a non-production environment. Run docker compose config to see the effect, recreate the service, and confirm the container sees the expected value using docker compose exec. As you gain confidence, extend the checklist to all environment-driven configuration.
The core principle is operational safety: never inject secrets from unencrypted files, never change multiple interdependent variables at once, and always verify with a concrete command that the running container matches the declared configuration. With these practices, environment variables become a reliable, auditable, and reversible configuration mechanism.