Intro
Environment variables are the most common way to configure Docker Compose services, yet subtle precedence rules, .env file scope, and shell expansion differences cause frequent outages. This guide explains the architecture behind Docker Compose environment variables, provides practical examples for passing values safely, and gives you diagnostic commands to verify the final configuration before it reaches production.
You will learn:
- How Compose resolves variables from the shell, .env file,
environment:,env_file:, and image defaults, and the exact precedence order. - How to inspect the merged environment of a running container, debug missing or incorrect values, and test changes without redeploying.
- How to structure configuration for multiple environments, keep secrets out of source control, and avoid common mistakes like variable name collisions or using the wrong .env file.
- How to design a recovery plan and verification checklist that makes failures visible before they affect users.
This article is written for developers, DevOps consultants, and technical startup teams who manage Docker Compose in development, staging, or production. We focus on operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify the result, and document how to recover when the expected state is not reached.
Version and Environment Inventory
Before changing any Compose configuration, establish a baseline. Name the relevant components, supported version range, prerequisites, a read-only observation, and the smallest justified change, and the command that verifies the outcome.
Start with these commands to capture the current state:
docker version --format '{{.Server.Version}}'
docker compose version
Expected output examples:
Docker version 24.0.6, build ed223bc
Docker Compose version v2.21.0
Check that your Compose file version is compatible. Since Compose v2, the top-level version: key is optional and ignored, but it can still appear in older projects. Run:
docker compose config --quiet
If the file is valid, there is no output and the exit code is 0. If there is a syntax error, Compose prints a descriptive message and exits with a non-zero code.
List running containers and their status with:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
For a Compose project, use:
docker compose ps
These commands show which services are running, their health, and published ports. They are read-only and safe to run at any time.
Check the logs for a specific service with:
docker compose logs -f app
This streams the latest logs from the app service. Look for environment-related errors such as database connection refused or missing required variable.
Inspect a running container's environment without modifying anything:
docker compose exec app printenv
This prints all environment variables visible inside the container, including values set by Compose, the image, and any runtime overrides. Use it to verify that variables are present and have the expected values.
For file-backed data, confirm where volumes are mounted before making changes. Use:
docker inspect -f '{{ json .Mounts }}' app_container
or for a Compose service:
docker compose exec app sh -c 'df -h /var/lib/data && ls -la /var/lib/data'
A named volume such as app_data:/var/lib/data is managed by Docker and is easier to reuse across container rebuilds. A bind mount such as ./data:/var/lib/data maps a host directory and is useful for local development but can cause permission, portability, and backup issues if the path is not identical on every machine.
A small production-like local test should include a restart test:
docker compose stop app
docker compose up -d app
docker compose exec app printenv DBPASSWORD
If the data disappears or the variable changes after restart, the service was likely writing to the container's writable layer or the variable was set only in a one-off shell session. Confirm persistence with the commands above before relying on the setup.
Safe Configuration Path
Environment variables in Compose are resolved from several sources, and the order of precedence is critical. Compose merges values in this order, from lowest to highest priority:
- Image default environment, if any (set by the Dockerfile
ENVinstruction). - Values from an
env_filelisted in the service definition (multiple files can be listed; later files override earlier ones). - Values from the shell environment where you run
docker compose. - Values from an
.envfile in the project directory (the same directory as the Compose file). - Values set explicitly in the service's
environment:orenvironment:with mapping syntax.
Within environment:, variables set without interpolation (e.g., DBHOST: db) always take precedence over those from shell or .env. If you use the shorthand DBHOST without a value, Compose looks for DBHOST in the shell environment or .env file and uses that value; if it is not found, the variable is removed or set to an empty string depending on the Compose version (in v2, it is removed unless a default is specified).
Here is a concrete example of precedence:
Compose file docker-compose.yml:
services:
web:
image: nginx:1.25
env_file:
- common.env
environment:
GREETING: "hello from compose file"
DBHOST: db
ports:
- "8080:80"
common.env:
GREETING=hello from env_file
DBHOST=old_db
DBPORT=5432
Shell before running docker compose up:
export GREETING="hello from shell"
export DBHOST="shell_db"
.env file in project directory:
GREETING=hello from dotenv
DBHOST=dotenv_db
After docker compose up -d, inspect the container:
docker compose exec web printenv GREETING
Output:
hello from compose file
Because GREETING is set explicitly in environment:, it wins. The DBHOST variable is also explicitly set to db, so it also wins over shell and .env. The DBPORT variable is only present in common.env, so it becomes 5432 inside the container.
Using env_file: is useful for keeping many variables in a separate file, but be aware that it applies to a single service, not the whole project. The .env file at the project root is used only for variable substitution in the Compose file, not automatically passed to containers.
For secrets like passwords and API keys, do not put them in the Compose file or a regular env_file committed to source control. Instead, use Docker secrets (for Swarm) or a dedicated .env file that is gitignored. A safer pattern:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
Then set DB_PASSWORD in your local .env file (not committed) or in your CI/CD secret store. This keeps the secret out of the repository and allows per-environment values.
For production-like local tests, use an override file:
docker compose -f docker-compose.yml -f docker-compose.override.yml up -d
Where docker-compose.override.yml contains environment-specific overrides. Validate the final merged configuration before applying:
docker compose config
This prints the fully resolved Compose file, including all environment variable substitutions and merged files. Review it carefully for leaked secrets or wrong values.
Verification and Diagnostics
The commands in this section help you detect and diagnose environment variable problems without causing further disruption.
To see the environment inside a running container, use docker compose exec:
docker compose exec web env
This shows all environment variables and their values. Filter for a specific one:
docker compose exec web env | grep DBHOST
Inspect the container configuration to see environment variables as recorded by Docker:
docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' web_container
This is useful when the container was started with docker run or via older Compose versions, and exec is not available.
Check logs for environment-related startup failures:
docker compose logs web | grep -i "error\|fatal\|denied"
If the application supports it, try to connect to dependencies from inside the container:
docker compose exec web sh -c 'nc -zv db 5432'
Replace nc with telnet or curl depending on the image. This verifies that the hostname db resolves and the port is reachable, which often depends on DBHOST and DBPORT.
When troubleshooting variable precedence, run docker compose config to see the exact environment block that will be passed to the container:
docker compose config | grep -A 10 "environment:"
This shows the resolved values, helping you pinpoint which source provided a variable. If a variable is missing entirely, check that it is defined in the expected source and spelled correctly (including case).
Finally, test a proposed change before applying it by using docker compose run with a temporary override:
docker compose run --rm -e DBHOST=test_db web sh -c 'echo $DBHOST'
This runs a one-off container with the modified environment and prints the value, without affecting the running service.
Failure Modes and Recovery
Even with careful planning, failures happen. Here are the common failure modes related to environment variables and how to recover.
Missing variable at container startup
Cause: The variable is not set in any source, or the .env file is missing or misnamed.
Detection: The service exits immediately; logs show required environment variable or connection refused.
Recovery:
- Run
docker compose logs <service>to identify the missing variable. - Check
docker compose configto see if the variable appears in the environment block. - Add the variable to the appropriate source (
.env,environment:, or secret store). - Recreate the service with
docker compose up -d --force-recreate <service>.
Wrong value due to precedence
Cause: The variable is set in multiple places, and a higher-priority source overrides the intended value.
Detection: The container runs but behaves unexpectedly (e.g., connects to a staging database in production).
Recovery:
- Run
docker compose exec <service> printenv <VARIABLE>to see the actual value. - Run
docker compose configand trace the variable back to its source. - Remove or adjust the lower-priority source, or explicitly set the variable in
environment:to the correct value. - Restart the service.
Secrets accidentally committed to version control
Cause: A .env file containing secrets was added to Git, or a secret was hardcoded in docker-compose.yml.
Detection: Found during code review, security scan, or after a credential leak.
Recovery:
- Rotate the leaked secret immediately.
- Remove the secret from the file and add appropriate
.gitignoreentries (e.g.,.env,*.env). - Use a secret management tool or Docker secrets for sensitive values.
- Purge the secret from Git history if necessary (e.g., using
git filter-repo).
Inconsistent .env file locations
Cause: Different team members or CI/CD pipelines run docker compose from different directories, so the .env file is not found or a different one is used.
Detection: Configurations differ between environments despite identical Compose files.
Recovery:
- Standardize on placing
.envin the same directory as the Compose file. - Use
--env-fileflag to specify an explicit path:docker compose --env-file ./config/prod.env up -d. - Document the expected file locations in the project README.
Variable name collision between Compose services
Cause: Multiple services define the same variable name with different values, and due to overrides or merge behavior, the wrong value is passed.
Detection: One service fails while another works, and logs show the variable has an unexpected value.
Recovery:
- Inspect each service separately:
docker compose exec <service> printenv <VAR>. - Use unique variable names per service or leverage Compose profiles and override files to segment configurations.
- Update the service definitions and redeploy.
Host environment inadvertently affecting Compose
Cause: The shell where you run docker compose has many exported variables, some of which are used by Compose for substitution in the Compose file, unexpectedly changing values.
Detection: docker compose config shows substituted values that differ from what you expected.
Recovery:
- Unset the offending variable in the shell:
unset <VAR>. - Be explicit in the Compose file: use
${VAR:-default}to provide a fallback. - Use a clean shell or a wrapper script that only exports required variables.
Operations Checklist
Use this checklist to verify and maintain your Docker Compose environment variable setup. Each item includes the command or observation, responsible role, and review frequency.
| # | Task | Command / Observation | Responsible Role | Review Frequency |
|---|---|---|---|---|
| 1 | Verify Docker and Compose versions | docker version && docker compose version | Platform Engineer | Quarterly |
| 2 | Validate Compose file syntax and resolve variables | docker compose config > /dev/null && echo Valid | Developer | Before every merge |
| 3 | Inspect running container environment | docker compose exec <service> printenv | Developer | On each deployment or configuration change |
| 4 | Check that secrets are not in logs or config output | docker compose config | grep -i 'password\|secret\|key' | Security Lead | Weekly |
| 5 | Confirm persistence of data and configuration across restart | docker compose stop && docker compose up -d && docker compose exec ls /data | DevOps Engineer | Monthly |
| 6 | Review .env file contents and access permissions | ls -la .env && cat .env (with caution) | Team Lead | Bi-weekly |
| 7 | Test recovery from missing variable scenario | Intentionally remove a variable in staging, observe failure, then restore and verify fix | QA Engineer | Before each production release |
| 8 | Audit environment variable precedence rules with the team | Review documentation and run a quick precedence test | Technical Writer / Lead | Annually |
Example ownership: Priya Shah, Engineering Lead, is responsible for the final review of environment variable changes before production deployment, and she revisits the checklist outcomes in the monthly ops review.
Common Pitfalls and How to Avoid Them
Here are additional nuances that trip up practitioners.
Using .env for container environment
A widespread mistake is assuming that variables in the project's .env file are automatically passed to all services. They are not. The .env file is only used for interpolation in the Compose file. To pass variables to containers, use environment: or env_file:.
Not quoting variable values in environment:
YAML can interpret values with special characters (like :, #, *) incorrectly. Always quote values, especially if they contain spaces or punctuation.
Mixing env_file and environment without understanding merge order
env_file is applied first, then environment overrides. If you need to override a single value from the file, list it in environment:.
Forgetting that docker compose up does not reload .env changes automatically
If you modify .env or env_file after containers are running, you must recreate the containers for the new values to take effect: docker compose up -d --force-recreate.
Using the same variable name with different meanings in different services
This can cause confusion and accidental overrides. Use a service prefix, e.g., WEB_DBHOST vs WORKER_DBHOST.
Relying on host environment variables in CI/CD
CI runners may have env vars set that interfere. Use explicit --env-file or define all needed variables in the pipeline, and avoid relying on the host's environment.
Conclusion
Understanding Docker Compose environment variable architecture is essential for reliable deployments. By knowing the precedence order, using safe configuration paths, and systematically verifying with commands like docker compose config and docker compose exec printenv, you can prevent many failures.
Adopt the operational practices in this guide: always observe the current state, scope changes to one variable at a time, use placeholders and secret management, and have a recovery plan. Include the operations checklist in your team's routine to keep configurations secure and consistent.
Next step: pick one service in your current Compose project and perform the "Version and Environment Inventory" steps. Compare the actual environment with what you expected, and fix any discrepancies. Then document the correct variable sources for your team.
A reliable technical workflow makes failure visible, protects sensitive values, limits changes to the intended resource, and defines recovery verification before an incident forces the decision.