Docker Compose "variable is not set. Defaulting to a blank string" in CI
Compose interpolates ${VAR} from the environment and .env. When a referenced variable is unset it warns and substitutes an empty string - which can silently produce a bad image tag, a :latest fallback, or an invalid value that breaks up.
What this error means
A docker compose up prints WARN[...] The "TAG" variable is not set. Defaulting to a blank string, then pulls/builds the wrong image (e.g. myorg/api: resolving to :latest) or fails on an empty port/path. Locally it works because the shell or .env had the value.
WARN[0000] The "TAG" variable is not set. Defaulting to a blank string.
# image: myorg/api:${TAG} -> myorg/api: (empty tag pulls :latest or fails)Diagnose it: read the resolved config, not the file you wrote
Compose merges override files, interpolates variables, and applies defaults before it does anything. Most Compose failures in CI are visible in the resolved configuration and invisible in the source file, because the value you are debugging came from an unset variable that quietly became an empty string.
# the fully merged, interpolated configuration Compose will actually run
docker compose config
# fail loudly on unset variables instead of silently interpolating empty
docker compose --env-file .env config --quiet || echo "invalid"
# which override files were picked up
docker compose config --services
Common causes
The variable is unset in the CI environment
Compose reads ${VAR} from the process environment and an .env file in the project dir. In CI neither may define it, so it interpolates to empty.
No .env file present on the runner
A .env that exists locally but is gitignored or not generated in CI means the variables it provided are missing.
No default in the interpolation
Using ${TAG} instead of ${TAG:-default} gives no fallback, so an unset variable becomes blank rather than a safe default.
How to fix it
Provide the variable or a default
Set the variable in the job, or use Compose’s default-value syntax.
# set it explicitly in CI:
TAG=1.4.2 docker compose up -d
# or give a safe default in the compose file:
# image: myorg/api:${TAG:-1.4.2}Pass an explicit env file and require the value
Point Compose at an env file and fail fast if a required variable is missing.
docker compose --env-file ci.env config # renders interpolated values
# fail early on a required var:
: "${TAG:?TAG must be set}"Bind mounts behave differently on a runner
- A relative bind source is resolved against the compose file location, not the working directory of the shell that invoked it.
- The host path must exist before
up. Compose creates missing directories for named volumes but not for bind mounts, and the failure surfaces as a mount error rather than a missing-path error. - On a CI runner the workspace path differs from your machine, so any absolute host path in a compose file is a portability bug waiting for its first CI run.
- Prefer named volumes for anything that does not genuinely need to be read from the host. They remove the whole class of problem.
How to prevent it
- Use
${VAR:-default}or${VAR:?required}instead of bare${VAR}. - Pass an explicit
--env-filein CI rather than relying on a local.env. - Validate interpolation with
docker compose configbeforeup.