885 words
4 minutes

Docker Compose env_file vs ${VAR}: Fix Interpolation

2026-08-17
DevOps
Docker
/
DevOps
/
Containers
/
Troubleshooting

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:

Terminal window
docker compose --env-file ./config/.env.dev config --environment
docker compose --env-file ./config/.env.dev config

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

.env
IMAGE_TAG=2026.08
services:
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:

config/.env.dev
IMAGE_TAG=2026.08-rc1
PUBLIC_ORIGIN=https://staging.example.com
Terminal window
docker compose --env-file ./config/.env.dev config
docker compose --env-file ./config/.env.dev up -d

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

Terminal window
IMAGE_TAG=2026.08 docker compose --env-file ./config/.env.dev config

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

Terminal window
set -euo pipefail
docker compose --env-file ./config/.env.ci config --environment
docker compose --env-file ./config/.env.ci config >/tmp/compose.resolved.yaml
rg 'image:|PUBLIC_ORIGIN:|LOG_LEVEL:' /tmp/compose.resolved.yaml

Do 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_file and interpolation

Docker Compose env_file vs ${VAR}: Fix Interpolation
https://laplusda.com/en/posts/docker-compose-env-file-interpolation/
Author
Zero
Published at
2026-08-17
License
CC BY-NC-SA 4.0
Was this article useful?

Report a typo or broken link, or suggest a related topic.