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

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

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

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

```bash
docker compose exec <service> env
```

or, without entering the container:

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

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

1. Shell environment variables (those already exported in the current session)
2. Variables from the `.env` file in the project directory (for `--env-file` see below)
3. Variables set in the `environment:` section of the service
4. 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)
5. Dockerfile `ENV` instructions in the image

A practical example:

Given a `docker-compose.yml`:

```yaml
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 `.env` for Compose file interpolation

Example of a `.env` file for interpolation:

```
POSTGRES_VERSION=16.2
APP_PORT=8080
```

And in `docker-compose.yml`:

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

1. Copy the current `docker-compose.yml` and `.env` to a backup location.
2. Run `docker compose config` to see the resolved output. Redirect it to a file with `> /tmp/compose-resolved.yml` and inspect for unintended variable replacements or exposed secrets.
3. Change one variable at a time. If multiple variables interact, document the dependency. For example, changing `DATABASE_URL` may require changing `REDIS_URL` if they share a hostname.
4. Apply the change with `docker compose up -d --force-recreate <service>` only after `docker compose config` succeeds.
5. 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:

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

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

```bash
docker compose exec web printenv DEBUG
```

If the container is restarting too quickly, use `docker inspect` on the container ID:

```bash
docker inspect <container-id> --format '{{.State.Status}} {{.State.ExitCode}}'
```

For PostgreSQL containers, you can also verify the configured password by connecting:

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

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

```bash
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 `.env` in the same directory as `docker-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:

```yaml
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 `env` output in CI. Use `docker 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 config` locally and in the environment to compare. If you need a deterministic build, unset conflicting variables or use `env -i docker compose config` to see the baseline.
- Document which variables are allowed to be overridden by the shell and which must come from `.env` or 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 config` to 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 version` and `docker compose config` output 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 any `command:`, `entrypoint:`, `healthcheck:`, or `environment:` sections.
- [ ] Ensure secrets are not currently in `.env` or `env_file` that 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_URL` must return `http://api:8080`).
- [ ] Back up the current Compose file and `.env` to a versioned location (e.g., Git tag or artifact store).

**After change (owner: same engineer; verify within 30 minutes of deploy)**

- [ ] Run `docker compose config` and 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 for `running (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 inspect` output 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 `.env` and Compose file changes from version control.
- [ ] Run `docker compose up -d --force-recreate` with 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:

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

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

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

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

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