Docker Compose configuration becomes easier to troubleshoot when each value has a clear destination. This guide separates Compose interpolation, container environment variables, .env files, build arguments, and Docker Compose secrets, then turns those distinctions into reusable templates and a deployment checklist.
Overview
There are several ways to provide configuration to a Compose project, and they solve different problems. A value may be needed by Compose while it reads the YAML file, by a running container, or only while an image is being built. Treating these as interchangeable is a common cause of missing values, accidental credential exposure, and configuration that works on one machine but not another.
Use this mental model:
- Interpolation: Compose substitutes variables into the Compose file before it creates services. Expressions such as
${APP_PORT}belong to this stage. - Container environment: The service process reads values from its environment, usually through
environmentorenv_file. - Build arguments:
build.argssupplies values to Dockerfile build steps. These are build-time inputs, not a safe runtime secret store. - Secrets: A service reads a sensitive value from a mounted file, commonly under
/run/secrets/, instead of receiving it as an ordinary environment variable.
The right choice depends on who needs the value and how long it should exist. A port number can usually be ordinary configuration. A database password, private key, or API token deserves a more deliberate path and should not be copied into an image or committed to source control.
A configuration decision table
| Need | Prefer | Why |
|---|---|---|
| Change Compose fields between environments | Shell variables or a Compose .env file | Compose can interpolate values while reading the file. |
| Give a running application non-sensitive settings | environment or env_file | The process receives standard environment variables. |
| Pass a non-secret value into an image build | build.args and Dockerfile ARG | The value is available during build steps only. |
| Provide a password or token at runtime | Compose secrets or an external secret mechanism | The application can read a mounted file without placing the value in ordinary environment configuration. |
Checklist by scenario
Scenario 1: Interpolate a value in Compose
Use interpolation when Compose itself needs the value. For example, this changes the host-side port without changing the service definition:
services:
web:
image: example/web:latest
ports:
- "${WEB_PORT:-8080}:8080"
Before running the project, check which value Compose will resolve:
docker compose config
Keep the distinction clear: a variable used to construct ports is not automatically available inside the container. If the application also needs that value, declare it under environment.
Scenario 2: Set ordinary container environment variables
Use environment for a small, visible set of service settings:
services:
api:
image: example/api:latest
environment:
APP_ENV: ${APP_ENV:-development}
LOG_LEVEL: ${LOG_LEVEL:-info}
Use env_file when several variables belong together or when keeping the service definition shorter is useful:
services:
api:
image: example/api:latest
env_file:
- ./config/api.env
Review the file before sharing it. An env_file is convenient configuration; it should not be treated as an encrypted secret store. Keep local files out of version control when they contain credentials, and commit a safe example file with placeholder values if teammates need a template.
Scenario 3: Supply a build argument
Build arguments are appropriate for values such as a package mirror, feature flag, or version selected while building:
services:
frontend:
build:
context: .
args:
NODE_ENV: ${NODE_ENV:-production}
The Dockerfile declares the corresponding argument:
ARG NODE_ENV=production
RUN echo "Building for ${NODE_ENV}"
Do not use build arguments for passwords or private tokens merely because they are not listed under environment. Build instructions, caches, logs, or image history may expose build inputs depending on how the image is created. Use a build-specific secret facility when a build genuinely requires sensitive material, and avoid baking that material into the resulting image.
Scenario 4: Mount a runtime secret
A Compose secret can be declared from a local file and granted only to the service that needs it:
services:
api:
image: example/api:latest
secrets:
- database_password
environment:
DB_PASSWORD_FILE: /run/secrets/database_password
secrets:
database_password:
file: ./secrets/database_password.txt
The application must read the file path supplied by DB_PASSWORD_FILE; it should not expect the secret to appear automatically as DB_PASSWORD. A small entrypoint script can read the file and pass it to an application that only supports a direct value, but check the application documentation first. Restrict file permissions, keep secret files outside the repository, and use the deployment platform's supported secret integration when moving beyond local development.
What to double-check
- Which layer owns the variable? Ask whether Compose, the image build, or the running process needs it. This prevents a variable from being declared in the wrong section.
- What is the source and precedence? Check shell exports, the project
.envfile, explicit--env-fileusage, service-levelenv_file, and inlineenvironmententries separately. Do not rely on memory when values conflict; render the resolved configuration withdocker compose config. - Does an empty value mean something different from an absent value? Defaults such as
${NAME:-fallback}treat an empty value as a reason to use the fallback, while${NAME-fallback}distinguishes empty from unset. Choose intentionally. - Is the syntax being parsed by the intended tool? Compose interpolation syntax and shell expansion can look similar but occur at different times. Quote values containing spaces, dollar signs, or characters meaningful to YAML or the shell.
- Can the service actually consume the value? A variable being present in the container does not guarantee that the application reads it. Confirm the application's exact variable name, file path, format, and restart requirements.
- Could the value appear in logs? Avoid printing complete environment dumps, command lines, or secret file contents during startup diagnostics.
For adjacent configuration work, readable data formats help during review: use a JSON formatter when checking structured application settings, and keep authentication configuration separate from token inspection. The JSON formatter and validator guide and JWT decoder guide provide related debugging workflows.
Common mistakes
- Assuming
.envis automatically injected into every service. A Compose.envfile is commonly used for interpolation. If a process needs a value inside its container, declare it throughenvironmentorenv_file. - Putting secrets in the Compose YAML. Even if the file is private today, it may be copied into a repository, support bundle, or deployment log later. Reference a controlled source instead.
- Using
env_filefor sensitive values without a handling plan. The mechanism is easy to use, but the file still needs access controls, rotation procedures, and exclusion from source control. - Expecting a secret to become an environment variable. A mounted secret is normally a file. Configure the application to read that file or use a deliberate adapter.
- Using build arguments for runtime credentials. Build-time and runtime lifecycles are different. Credentials needed only after startup should not be sent through the image build.
- Debugging the wrong configuration. Inspect the resolved Compose model, then inspect the service's effective environment without exposing sensitive values. Recreate or restart the service when configuration changes require it.
When to revisit
Run this checklist whenever you add a service, introduce a new environment, change a Dockerfile, rotate credentials, or move from local Compose usage to a deployment platform. It is also worth reviewing before a scheduled release or seasonal planning cycle, when old local files and undocumented defaults often surface.
For a practical review, take these actions:
- List every configurable value and label it as interpolation, runtime environment, build input, or secret.
- Search the repository and image-building workflow for credentials, tokens, private keys, and password-like values.
- Render the Compose configuration with the intended variable sources and compare it with the expected service configuration.
- Confirm that each service receives only the values it needs and that secret files are not logged or committed.
- Document the required variable names, safe defaults, secret file paths, and rotation steps in the project README or runbook.
- Recheck the current Compose implementation and deployment platform documentation when upgrading tooling, because supported secret backends and command behavior can change.
A clear separation between Compose interpolation, container variables, build arguments, and secrets makes configuration easier to change without guesswork. Keep that separation visible in the YAML, document the exceptions, and validate the resolved result before deploying.