## 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:

```bash
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:

```bash
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:

```bash
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
```

For a Compose project, use:

```bash
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:

```bash
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:

```bash
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:

```bash
docker inspect -f '{{ json .Mounts }}' app_container
```

or for a Compose service:

```bash
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:

```bash
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:

1. Image default environment, if any (set by the Dockerfile `ENV` instruction).
2. Values from an `env_file` listed in the service definition (multiple files can be listed; later files override earlier ones).
3. Values from the shell environment where you run `docker compose`.
4. Values from an `.env` file in the project directory (the same directory as the Compose file).
5. Values set explicitly in the service's `environment:` or `environment:` 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`:
```yaml
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`:
```bash
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:

```bash
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:

```yaml
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:

```bash
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:

```bash
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`:

```bash
docker compose exec web env
```

This shows all environment variables and their values. Filter for a specific one:

```bash
docker compose exec web env | grep DBHOST
```

Inspect the container configuration to see environment variables as recorded by Docker:

```bash
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:

```bash
docker compose logs web | grep -i "error\|fatal\|denied"
```

If the application supports it, try to connect to dependencies from inside the container:

```bash
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:

```bash
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:

```bash
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:
1. Run `docker compose logs <service>` to identify the missing variable.
2. Check `docker compose config` to see if the variable appears in the environment block.
3. Add the variable to the appropriate source (`.env`, `environment:`, or secret store).
4. 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:
1. Run `docker compose exec <service> printenv <VARIABLE>` to see the actual value.
2. Run `docker compose config` and trace the variable back to its source.
3. Remove or adjust the lower-priority source, or explicitly set the variable in `environment:` to the correct value.
4. 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:
1. Rotate the leaked secret immediately.
2. Remove the secret from the file and add appropriate `.gitignore` entries (e.g., `.env`, `*.env`).
3. Use a secret management tool or Docker secrets for sensitive values.
4. 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:
1. Standardize on placing `.env` in the same directory as the Compose file.
2. Use `--env-file` flag to specify an explicit path: `docker compose --env-file ./config/prod.env up -d`.
3. 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:
1. Inspect each service separately: `docker compose exec <service> printenv <VAR>`.
2. Use unique variable names per service or leverage Compose profiles and override files to segment configurations.
3. 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:
1. Unset the offending variable in the shell: `unset <VAR>`.
2. Be explicit in the Compose file: use `${VAR:-default}` to provide a fallback.
3. 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 <service> 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.