Docker Compose Empty Variables: Trace the Effective Config

Separate the two stages that can create an empty value

When a Compose environment problem appears, it is tempting to inspect one .env file and stop there. Compose interpolation, which replaces ${VARIABLE} in the Compose file, is separate from the later step that builds the environment delivered to a container. If interpolation already produced an empty string, inspecting a secret file or an application default inside the container starts at the wrong layer.

Docker documents the current shell as the first interpolation input. When --env-file is not set, Compose next checks .env in the current working directory; it then uses the file selected with --env-file or .env in the project directory. This selection rule must not be confused with the full precedence table for the environment inside a container. First establish which input Compose read and which model it will send to the Engine. Only then inspect what the process in the container received.

Record state without printing the secret

LayerEvidence to captureDecision
Launching shellWhether the variable exists and whether it is emptyDetermine whether the shell masks a file value.
Interpolation inputWorking directory, --env-file, and project-path .envConfirm that the command options and launch location select the intended file.
Rendered modelThe output of docker compose configSee whether the service environment sent to the Engine is empty.
ContainerThe environment received by the running processDistinguish a file edit from an actual container recreation.
ApplicationSecret loader and default-value behaviorMove here only after the Compose value is correct.

config --environment can print values, and config --variables can print defaults. Neither redacts secrets. Instead of saving the complete model, filter the state of one variable as below. These commands require Bash and Python 3. Replace app and DB_PASSWORD with the actual service and variable names.

python3 -c 'import os; v=os.environ.get("DB_PASSWORD"); print("shell:", "unset" if v is None else "set" if v else "empty")'
set -o pipefail
docker compose config --format json |
python3 -c 'import json,sys; e=json.load(sys.stdin)["services"]["app"].get("environment",{}); v=e.get("DB_PASSWORD"); print("model:", "unset" if v is None else "set" if v else "empty")'

The first line checks only the exported environment of the shell that launches Compose. model: empty means interpolation still produced an empty string. JSON passes through the pipe without sending the full values to a terminal or file. If parsing fails, stop collecting output and check the service name and Compose syntax. Debug tracing or full-model logging would defeat this protection.

A correct model does not change an existing container. If several working directories share a project name, record the working directory, -f, --env-file, and project name to avoid recreating another deployment.

Observed empty-value behavior in a separate environment

On 2026-09-06 KST, this minimal model was checked on Linux x86_64 with Compose 5.5.1. The .env file contained DB_PASSWORD=synthetic-file-value, not a real password. This tested config interpolation only, without starting Docker Engine or an application.

services:
  app:
    image: busybox:1.37
    environment:
      DB_PASSWORD: ${DB_PASSWORD}
shell variable absent + .env set  -> model: set   (exit 0)
shell variable exported empty    -> model: empty (exit 0)
required expression + empty shell -> config rejected (exit 1)
--environment                    -> synthetic value appeared in output

The last failure check changed the expression to ${DB_PASSWORD:?DB_PASSWORD is required}. The error included required variable DB_PASSWORD is missing a value. The same inputs demonstrated both why a present file can still produce an empty value and how to reject that input before container creation. This does not establish the precedence of an image-specific secret loader.

When the file changes but the existing container does not

Take the check one step beyond a correct interpolated model. On 2026-09-13 KST, containers were actually run on a separate Linux lab host with Docker Engine 28.3.3, Compose 2.39.4, and BusyBox 1.37.0. This is a separate record from the September 6 interpolation test. Networking was disabled, no volumes were attached, and every printed value was a dummy string assigned to DEMO_VALUE.

Save the following compose.yaml in an empty, dedicated directory. Do not run it in a production project. sleep keeps this test container alive briefly; it is not a repair for a real service that exits.

services:
  app:
    image: busybox:1.37.0
    network_mode: none
    command: ["sh", "-c", "exec sleep 600"]
    environment:
      DEMO_VALUE: ${DEMO_VALUE}

The Bash sequence below expresses the recorded operations using the standard Docker CLI. In the experiment, a Python runner called the same subcommands with a dedicated Docker socket. Fix the dedicated project name and remove any same-name shell variable. Use printenv and full-model output only in this directory containing dummy values. For real passwords, use the state filter in the earlier section.

unset DEMO_VALUE
printf 'DEMO_VALUE=from-file-v1\n' > .env
docker compose -p nole-env-20260913 up -d app
docker compose -p nole-env-20260913 ps -q app
docker compose -p nole-env-20260913 exec -T app printenv DEMO_VALUE

printf 'DEMO_VALUE=from-file-v2\n' > .env
docker compose -p nole-env-20260913 config --format json
docker compose -p nole-env-20260913 restart app
docker compose -p nole-env-20260913 ps -q app
docker compose -p nole-env-20260913 exec -T app printenv DEMO_VALUE

docker compose -p nole-env-20260913 run --rm --no-deps app printenv DEMO_VALUE
docker compose -p nole-env-20260913 exec -T app printenv DEMO_VALUE
docker compose -p nole-env-20260913 up -d --no-deps --force-recreate app
docker compose -p nole-env-20260913 ps -q app
docker compose -p nole-env-20260913 exec -T app printenv DEMO_VALUE
docker compose -p nole-env-20260913 down
CheckObserved resultInterpretation
Initial creationID 1dd2fc162a31…, from-file-v1Container received the first file value
File changed to v2Rendered model: from-file-v2Only the model for the next creation changed
exec after restartSame ID, from-file-v1Restart did not replace the environment
run –rmOne-off container: from-file-v2A result from a newly created container
exec in existing app immediately afterwardfrom-file-v1A successful run did not update the existing app
exec after force-recreateID d22c707ca6e7…, from-file-v2Replacement container received the new value

The execution commands above all returned exit code 0. In particular, restart succeeded while the value remained v1. Compare the container ID with the exec result to detect this boundary. Passing an empty shell variable to the same test model also produced an empty string. Replacing the expression with ${DEMO_VALUE:?DEMO_VALUE is required} made config --quiet reject it with exit code 1 and required variable DEMO_VALUE is missing a value.

This establishes the boundary at which Compose delivers configuration to a container. Database connections, password rotation, and application secret-file precedence were not tested. Check each container when a service has multiple replicas. Images without a shell cannot use this example’s sh and printenv unchanged. First decide whether the situation allows recreation or requires observation only.

Branch on the first layer where the value becomes empty

  1. Inspect the current shell. A variable that exists with zero length is still an explicit value. A CI job or scheduled task can export an empty variable that is selected before the project .env.
  2. Confirm the Compose project path. Running from another directory or combining multiple -f files can select a different .env from the one an operator opened. Record both the command and its working directory.
  3. Read the rendered model. Inspect the target service environment in docker compose config, not only the original YAML.
  4. Make required input fail early. When an empty value is invalid, syntax such as ${DB_PASSWORD:?DB_PASSWORD is required} makes Compose stop before creating the container instead of silently rendering an empty value.
  5. Recreate the container. A plain restart after editing the Compose file or its variables does not apply the changed environment. Docker explicitly documents this limitation of restart.

Recreate one service and verify the boundary

Preserve the current service definition and volume paths, then recreate only the affected service. Naming the service avoids unnecessarily cycling a database or another dependency.

docker compose config --quiet
docker compose up -d --no-deps --force-recreate app
docker compose ps -a app
docker compose exec -T app sh -c 'if [ -n "$DB_PASSWORD" ]; then echo set; else echo empty; exit 1; fi'

exec checks the currently running service container. run --rm creates a new one-off container and cannot prove that the existing container received the change. The final command requires sh and returns exit code 1 for an empty value. For shell-free images, use an application status check that does not reveal values; do not print the entire docker inspect environment array.

This checks the container environment, not every application setting. PID 1 can change its own environment after startup, or the application can prefer a separate secret file. Verify an actual successful connection and the absence of the original application error afterward.

Rollback boundary

If the application fails after recreation, restore the previous Compose and env files, then recreate the same service again. Do not leave both the old secret path and the new environment variable active. Two live input paths make it ambiguous which value the application actually consumed.

This procedure traces delivery of an empty value; it does not define the precedence rules of every application. If the rendered Compose model is correct but the application still treats the value as empty, continue with the image entrypoint and the application’s configuration-loader documentation.

Official references

Earlier verification notes preserved from this article

Verification level: official documentation checked

The boundary between Compose interpolation and the container environment was checked against Docker documentation. No specific application image was reproduced, so image-specific secret-loader behavior remains outside this scope.

Writing and correction policy · Report an error