When Docker Compose uses the wrong value, the cause is usually not random: several variable sources are being combined at different stages. This checklist separates Compose-file interpolation from the container’s final environment, explains profiles and secrets, and gives you a safe workflow for finding the winning value without printing credentials.
Overview
Docker Compose environment variables can enter a project through your shell, an .env file, an explicitly selected environment file, the environment: section, an env_file: reference, or a command-line option. These sources do not all solve the same problem.
There are two stages to keep separate:
- Interpolation: Compose replaces expressions such as
${API_PORT}while it reads the Compose file. - Container environment: after the file is resolved, Compose passes selected values into the service container.
A value can therefore appear correct in the Compose file but still be absent from the container, or a value can be present in the container without having been used for interpolation elsewhere. Before changing files, identify which stage is wrong.
A useful rule is to treat the project’s configuration as a deliberate contract: use ordinary variables for non-sensitive settings, use explicit files for environment-specific configuration, and use secrets for credentials that should not be exposed through ordinary environment inspection.
Checklist by scenario
Scenario 1: Compose is substituting the wrong value
- Check the terminal session first. A variable exported in the shell can take priority over a value in a local
.envfile. - Check whether the command selects an environment file, such as with
--env-file. Do not assume the file beside the Compose file is the only input. - Inspect the exact project directory and Compose files being used. A different working directory, project name, or
-fargument can select a different configuration context. - Look for duplicate variable definitions. Search the Compose file, referenced
env_filefiles, shell startup files, CI configuration, and wrapper scripts. - Render the resolved configuration with
docker compose config. Use the command’s supported options to inspect interpolation and, where available in your Compose version, omit or redact sensitive values.
For example, this Compose fragment interpolates a port while also passing an application setting into the service:
services:
api:
image: example/api:latest
ports:
- "${API_PORT:-8080}:8080"
environment:
APP_ENV: ${APP_ENV:-development}
The :- form supplies a fallback when the variable is unset or empty. The - form treats an empty value differently. Choose the form intentionally rather than assuming an empty variable means “not configured.”
Scenario 2: The rendered file is correct, but the container is wrong
- Inspect the service’s
environment:entries. An explicit entry can override a value supplied throughenv_file:. - Check whether a command-specific option supplies an environment value, especially when using
docker compose run -e NAME=VALUE. - Confirm that the application reads the variable name you intended.
DATABASE_URLandDB_URLare different settings even if they point to the same concept. - Check the container’s actual environment without broadly dumping secrets. For a non-sensitive variable, use
docker compose exec SERVICE printenv NAMEor an equivalent application diagnostic. - Recreate the service after configuration changes. A running container does not automatically receive a changed environment.
As a practical precedence guide for a service’s final environment, start with the most specific source: a command-line value supplied for that container, then explicit service-level environment entries, then values from an env_file: reference, followed by image-level defaults such as an image ENV. Exact behavior depends on how a value is written and whether it is interpolated, so verify the result with the rendered configuration and a targeted container check.
Scenario 3: You need development and debugging variants
Profiles let one Compose project describe optional services without starting every service in every workflow. A typical file might keep the main application enabled by default and place a mail viewer or debugging tool behind a profile:
services:
api:
image: example/api:latest
mail-ui:
image: example/mail-ui:latest
profiles: [debug]
ports:
- "8025:8025"
Start the default services normally, then activate the optional service with a profile-aware command such as docker compose --profile debug up. Check the profile name carefully: a typo can make a service appear to be missing when it is simply inactive. Keep profile-specific variables close to the service that consumes them, and test both the default and profile-enabled configurations in CI or a repeatable local script.
Scenario 4: You are handling a password, token, or key
Do not treat an .env file as a secret manager. It is convenient for local interpolation, but it can be copied, logged, committed, or exposed through debugging commands. Add local environment files to the project’s ignore rules and provide a safe example file containing names and non-sensitive placeholders.
Where the application supports file-based credentials, use Compose secrets instead:
services:
api:
image: example/api:latest
secrets:
- database_password
secrets:
database_password:
file: ./secrets/database_password.txt
The service receives the secret as a mounted file, conventionally under /run/secrets/, rather than as a normal environment variable. Confirm that the application supports this pattern and that the secret file is protected locally. Do not print the file contents while debugging.
What to double-check
- Variable names: Verify spelling, capitalization, and underscores in the shell, Compose file, environment file, and application code.
- Interpolation syntax: Distinguish
${NAME},${NAME:-fallback}, and a literal dollar sign. Escape a dollar sign when the value must reach the container literally rather than be interpolated by Compose. - Quoting: Quote values containing spaces, punctuation, or characters that have meaning in YAML or a shell. Also check how the application parses quoted values.
- Blank versus unset: An explicitly empty variable is not always equivalent to an absent variable. Test the behavior your application expects.
- File selection: Record the Compose files, project directory, selected profile, and optional environment-file arguments used by the command.
- Lifecycle state: Ask whether you inspected the newly created container or an older container from a previous configuration.
- Logs and diagnostics: Redact tokens, passwords, connection strings, and private keys before sharing output. A rendered Compose file can contain values that should never enter an issue tracker.
For structured settings, render and review the configuration as data rather than relying on visual scanning. If a value is JSON, a JSON formatter and validator can make escaping and nesting errors easier to spot. For an API credential used by a service, the JWT decoder and validator can help inspect non-secret claims, but never paste a live token into an untrusted tool.
Common mistakes
- Assuming
env_file:controls interpolation everywhere. It supplies variables to a service; it is not automatically the source used to resolve every${...}expression in the Compose file. - Using
environment:for every secret. Environment variables are convenient, but they can appear in diagnostics, process inspection, crash reports, or management interfaces. Prefer a supported secret mechanism for sensitive values. - Changing
.envand checking an old container. Recreate the affected service and verify its identity before drawing conclusions. - Debugging with a full environment dump. It may reveal credentials. Query one non-sensitive variable at a time and redact output.
- Mixing profile logic with application logic. A profile controls whether a service is enabled; it does not automatically transform the application’s configuration. Define the required variables explicitly.
- Relying on undocumented local habits. Shell exports, aliases, wrapper scripts, and IDE run configurations can silently change the command’s inputs. Put important choices in documented scripts or CI configuration.
When the source is still unclear, reduce the case to one service and one variable. Remove optional files, use a harmless test value, render the configuration, recreate the service, and compare the result at each stage. This isolates precedence from application-level parsing.
When to revisit
Keep this checklist with the Compose project and use it whenever you add a service, introduce a new environment, enable a profile, move configuration into CI, or change the way credentials are delivered. It is especially useful before a release or a seasonal planning cycle when development, staging, and production workflows are reviewed together.
Recheck the workflow after updating Docker Compose, changing the Compose specification, replacing a base image, or modifying a deployment wrapper. Command options and supported behavior can change, and a project may gradually accumulate multiple sources that were harmless when introduced separately.
Before closing an incident, record four things: the winning source, the rendered non-sensitive configuration, the service recreation command, and the safe secret-handling method. Then remove temporary exports and test values from the shell and local files. This turns a one-time fix into a repeatable developer productivity practice rather than another unexplained configuration exception.