Docker Compose env_file vs ${VAR}: Fix Interpolation
If ${TAG} is empty in a Docker Compose file even though a service has env_file: .env, the two settings are being used at different stages. A service-level env_file supplies variables to the container; it is not the normal source for interpolation while Compose parses the model, as Docker’s Compose interpolation guide explains.
For interpolation, provide the value through the shell, a project .env, or the CLI’s --env-file option. Then inspect the rendered model before starting containers:
docker compose --env-file ./config/.env.dev config --environmentdocker compose --env-file ./config/.env.dev configThe first command shows the variables Compose used for interpolation. The second prints the resolved Compose model. Run them from the same directory and with the same -f files as CI or deployment.
Why env_file does not fill ${VAR}
Consider this file:
services: web: env_file: - ./runtime.env image: "my-web:${IMAGE_TAG}"runtime.env can populate the environment inside the web container, but Compose must resolve image while it is constructing the model. If IMAGE_TAG is not available from the interpolation sources, the image can become my-web: or produce an unset-variable warning.
This is why changing a service’s env_file can appear to do nothing for an image tag, volume path, port, or build argument. Those fields were already parsed before the container environment existed.
Use the right source for interpolation
Use a project .env for the default
Place a file next to the Compose project and reference the variable in the model:
IMAGE_TAG=2026.08services: web: image: "my-web:${IMAGE_TAG}"When no explicit --env-file is supplied, Compose looks for .env using its project-directory rules. Those rules can change when you use --project-directory or multiple -f files, so verify the result instead of assuming the current shell directory owns the file.
Use --env-file for CI or a named environment
Keep environment files outside the default path when the workflow has explicit stages:
IMAGE_TAG=2026.08-rc1PUBLIC_ORIGIN=https://staging.example.comdocker compose --env-file ./config/.env.dev configdocker compose --env-file ./config/.env.dev up -dThe --env-file path is an input to the Compose CLI. It is not the same as services.<name>.env_file, and it should be named in the CI command so a later working-directory change does not silently select another file.
Use the shell for an intentional override
Compose gives shell variables higher precedence than the CLI env file and the project .env for interpolation, according to Docker’s precedence guide:
IMAGE_TAG=2026.08 docker compose --env-file ./config/.env.dev configUse this for a deliberate one-off override. If the value is part of the deployment contract, put the source in the workflow configuration and print only the variable name and resolved non-secret values during verification.
Fail early when a value must exist
An empty substitution can survive parsing and fail later as an invalid image, URL, or host path. Use the required-value form when a default would be unsafe:
services: web: image: "my-web:${IMAGE_TAG:?Set IMAGE_TAG before starting Compose}" environment: PUBLIC_ORIGIN: "${PUBLIC_ORIGIN:?Set PUBLIC_ORIGIN before starting Compose}"Use a default only when the default is a valid and intentional environment:
services: web: environment: LOG_LEVEL: "${LOG_LEVEL:-info}"The distinction between ${VAR:-default} and ${VAR?error} matters: the former treats an empty value as missing, while the latter checks whether the variable is set. Choose the form that matches the deployment contract.
Keep container environment and model interpolation separate
A service can use both mechanisms, but each has a different job:
services: web: image: "my-web:${IMAGE_TAG:?Set IMAGE_TAG}" env_file: - ./runtime.env environment: PUBLIC_ORIGIN: "${PUBLIC_ORIGIN:?Set PUBLIC_ORIGIN}"Here IMAGE_TAG and PUBLIC_ORIGIN must be available while Compose parses the file. runtime.env can provide additional values to the running container. If the same key appears in several sources, check the documented precedence for the specific field instead of treating every environment file as interchangeable.
For a modular Compose project, the include path and env_file guide covers how an included file gets its own project directory and interpolation inputs. This article is narrower: it explains why a service-level environment file cannot retroactively fill a ${VAR} in the Compose model.
Verify the same model CI will run
Use a safe, non-secret configuration check in CI:
set -euo pipefail
docker compose --env-file ./config/.env.ci config --environmentdocker compose --env-file ./config/.env.ci config >/tmp/compose.resolved.yamlrg 'image:|PUBLIC_ORIGIN:|LOG_LEVEL:' /tmp/compose.resolved.yamlDo not dump the full resolved file when it contains passwords, tokens, or connection strings. A better pipeline prints the image reference and a boolean for each required variable, then starts the service only after config succeeds.
Docker documents .env interpolation as a Compose CLI feature. If the same YAML is passed to docker stack deploy, do not assume the Swarm command supports the same substitution behavior; test that deployment path separately.
Takeaway
Use --env-file, project .env, or the shell to resolve ${VAR}; use service env_file for the container’s runtime environment. Run docker compose config --environment and docker compose config before up so a missing value fails at the parsing boundary.
FAQ
Q: Can a service env_file be used for ${VAR} in the same Compose file?
A: Do not rely on it. A service env_file supplies the container environment, while Compose interpolation happens when the model is parsed. Use a project .env, --env-file, or the shell for ${VAR}.
Q: Which Compose interpolation source wins?
A: For the documented interpolation sources, the shell has higher precedence than a CLI --env-file, which has higher precedence than the project .env fallback. Verify the exact result with docker compose config --environment.
Q: Why does docker compose config show an empty value instead of failing?
A: An unset variable can be substituted as an empty string. Use the required form such as ${IMAGE_TAG:?Set IMAGE_TAG} when an empty value would create an invalid or unsafe configuration.
References:
Docker Docs: Compose variable interpolation
Docker Docs: Environment variables precedence
Docker Compose issue: Variables set by
env_fileand interpolation
Report a typo or broken link, or suggest a related topic.