cli icon indicating copy to clipboard operation
cli copied to clipboard

Parameter expansion when using `docker run --env-file ....`

Open MarvinGravert opened this issue 2 years ago • 8 comments

Description

Hi, when running docker run --env-file env.list --rm ubuntu, parameter expansion is not applied to elements in env.list. The entry VAR=$USER is not expanded thus the env VAR will be assigned the literal string "$USER" in the container. This is inconsistant with the behavior of environment files when used with docker compose.

I hope this the correct repo and label. Sorry for any inconvenience.

Reproduce

  1. echo VAR=\$USER > env.list
  2. docker run --env-file env.list --rm -it ubuntu env | grep VAR
  3. => VAR=$USER

Expected behavior

The expected outcome should be the expanded value. In my case VAR=marvin. This would be in line with the behavior of docker compose.

services:
  test:
    image: "ubuntu"
    env_file:
      - ./env.list
    command: "bash -c 'env | grep VAR'"

docker compose -f docker-compose.yml up results in VAR=marvin

docker version

Client:
 Version:           24.0.2
 API version:       1.43
 Go version:        go1.20.4
 Git commit:        cb74dfc
 Built:             Thu May 25 21:51:00 2023
 OS/Arch:           linux/amd64
 Context:           default

docker info

Client:
 Version:    24.0.2
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.10.5
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.18.1
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Additional Info

No response

MarvinGravert avatar Jun 13 '23 13:06 MarvinGravert

Hm, ISTR this was by design, and --env-file would not do variable expansion, and only straight NAME=value (literal) with the exception of NAME (without =) which would be propagated with the corresponding env-var from the CLI's environment (if present);

  • https://github.com/moby/moby/pull/4174

If compose is expanding environment variables for these, that looks like a bug (or at least incompatible behavior)

/cc @glours @tianon (if you recall more context on this)

thaJeztah avatar Jun 13 '23 23:06 thaJeztah

Oh! I guess compose's --env-file is to specify the custom path for the .env file (and not the equivalent of docker run --env-file); those are separate things (and not equivalents), but (unfortunately) named confusingly.

edit: Hm.. but env_file is the equivalent, so if that expands env-vars, that sounds like a potential bug

thaJeztah avatar Jun 13 '23 23:06 thaJeztah

Came accross something related. Seems like the parameter expansion capabilities of compose were extended relatively recently. Building on the orignal example consider:

docker compose version

echo VAR=\${USER_VAR:-default} > env.list

cat <<EOF > docker-compose.yml
services:
  test:
    image: "ubuntu"
    env_file:
      - ./env.list
    command: "bash -c 'env | grep VAR'"
EOF

docker compose up
# v2.6.0: VAR=:-default}
# v.2.10.2: VAR=default

USER_VAR=42 docker compose up
# v2.6.0: VAR=42:-default}
# v.2.10.2: VAR=42

Did install the different compose versions via

sudo apt-get install docker-compose-plugin=2.6.0~ubuntu-focal

and

sudo apt-get install docker-compose-plugin=2.10.2~ubuntu-focal

,respectively.

With docker run --env-file env.list --rm -it ubuntu env | grep VAR there is no parameter expansion in any case.

Looks like the default value handling for parameter expansion was added with this PR which ended up in v2.7.0

So docker run --env-file and compose file env_file are inconsistent, but compose behavior seems to be intended.

sply88 avatar Aug 02 '23 14:08 sply88

I think I just duplicated these issue here

So I'm begging you to put here as much attension as you can, because it confuses very much and I think these feature can be extremely relevant and useful!

pryhodkin avatar Oct 27 '23 23:10 pryhodkin

Hey @thaJeztah,

What do you think, is there a possibility to implement such functionallity? Maybe you have some feedbask from developers team?

pryhodkin avatar Oct 31 '23 20:10 pryhodkin

I want to give you the summary of what the behavior is like now when using docker compose.

Given:

% cat foo.env 
FOO=foo
BAR=${FOO}_bar
% cat bar.env 
BAZ=${FOO}_baz

Given:

services:
  test1:
    image: alpine
    command: ["sh","-c","echo test env_file; echo FOO=$$FOO; echo BAR=$$BAR; echo BAZ=$$BAZ"]
    env_file:
      - foo.env
      - bar.env
    
  test2:
    image: alpine
    command: ["sh","-c","echo environment; echo FOO=$$FOO; echo BAR=$$BAR; echo BAZ=$$BAZ"]
    environment:
      - FOO=${FOO}
      - BAR=${BAR}
      - BAZ=${BAZ}
    depends_on: 
      - test1
  test3:
    depends_on: 
      - test2
    image: alpine
    command: ["sh","-c","echo both; echo FOO=$$FOO; echo BAR=$$BAR; echo BAZ=$$BAZ"]
    environment:
      - FOO=${FOO}
      - BAR=${BAR}
      - BAZ=${BAZ}
    env_file:
      - foo.env
      - bar.env

Now try both docker compose up and docker compose up --env-file foo.env --env-file bar.env

% docker compose up                                       
WARN[0000] The "FOO" variable is not set. Defaulting to a blank string. 
WARN[0000] The "BAR" variable is not set. Defaulting to a blank string. 
WARN[0000] The "BAZ" variable is not set. Defaulting to a blank string. 
WARN[0000] The "FOO" variable is not set. Defaulting to a blank string. 
WARN[0000] The "BAR" variable is not set. Defaulting to a blank string. 
WARN[0000] The "BAZ" variable is not set. Defaulting to a blank string. 
WARN[0000] Found orphan containers ([temp-test-1]) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up. 
[+] Running 3/3
 ✔ Container temp-test1-1  Created                                                                        0.0s 
 ✔ Container temp-test2-1  Recreated                                                                      0.1s 
 ✔ Container temp-test3-1  Recreated                                                                      0.0s 
Attaching to test1-1, test2-1, test3-1
test1-1  | test env_file
test1-1  | FOO=foo
test1-1  | BAR=foo_bar
test1-1  | BAZ=foo_baz
test1-1 exited with code 0
test2-1  | environment
test2-1  | FOO=
test2-1  | BAR=
test2-1  | BAZ=
test2-1 exited with code 0
test3-1  | both
test3-1  | FOO=
test3-1  | BAR=
test3-1  | BAZ=
test3-1 exited with code 0
% docker compose --env-file foo.env --env-file bar.env  up
WARN[0000] Found orphan containers ([temp-test-1]) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up. 
[+] Running 3/3
 ✔ Container temp-test1-1  Created                                                                        0.0s 
 ✔ Container temp-test2-1  Recreated                                                                      0.1s 
 ✔ Container temp-test3-1  Recreated                                                                      0.0s 
Attaching to test1-1, test2-1, test3-1
test1-1  | test env_file
test1-1  | FOO=foo
test1-1  | BAR=foo_bar
test1-1  | BAZ=foo_baz
test1-1 exited with code 0
test2-1  | environment
test2-1  | FOO=foo
test2-1  | BAR=foo_bar
test2-1  | BAZ=foo_baz
test2-1 exited with code 0
test3-1  | both
test3-1  | FOO=foo
test3-1  | BAR=foo_bar
test3-1  | BAZ=foo_baz
test3-1 exited with code 0

Now further run without docker compose, you get this:

This is what it looks like: 

docker run --rm \
  --env-file foo.env \
  --env-file bar.env \
  alpine \
  sh -c 'echo test env_file; echo FOO=$FOO; echo BAR=$BAR; echo BAZ=$BAZ'
test env_file
FOO=foo
BAR=${FOO}_bar
BAZ=${FOO}_baz

Conclusions:

Key Takeaways

  1. Compose env_file: supports interpolation

    • When you list multiple files under env_file: in a Compose service, Compose loads them in order, performing ${…} substitution against variables set by earlier files.

    • Example: with

      env_file:
        - foo.env   # FOO=foo
        - bar.env   # BAR=${FOO}_bar, BAZ=${FOO}_baz
      

      you get BAR=foo_bar and BAZ=foo_baz at container runtime.

  2. docker run --env-file does not interpolate

    • The standalone Docker CLI simply reads each line as a literal KEY=… pair; it does not expand ${FOO} inside the file.
    • That’s why your test showed BAR=${FOO}_bar verbatim.
  3. environment: is resolved at Compose‑parse time

    • Entries under environment: with ${…} are expanded from your shell, from --env-file files passed to the Compose CLI, or from .envnever from a service’s own env_file:.
    • If ${FOO} isn’t present in the Compose parse environment, you’ll get an empty string and that will override any env_file: value.
  4. environment: always overrides env_file:

    • If you list the same variable in both, the environment: value “wins” at container start.
  5. Practical recommendations

    • For simple runtime injection without Compose parsing: use env_file: in Compose, or --env-file with docker run, but ensure files contain only fully‑expanded values (generate them via shell if needed).
    • For interpolation while spinning up Compose services: pass your files to Compose itself (docker compose --env-file foo.env --env-file bar.env up), or define everything under environment: so that ${…} is expanded before the container sees it.
    • If you need both: either merge and pre‑expand your .env files, or rely on Compose’s --env-file flags rather than env_file: alone.

kundeng avatar Jul 19 '25 18:07 kundeng

now if you want to confuse yourself even further, try this:

FOO=A BAR=B docker compose --env-file foo.env --env-file bar.env up 
WARN[0000] Found orphan containers ([temp-test-1]) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up. 
[+] Running 4/4
 ✔ Network temp_default    Created                                                                               0.0s 
 ✔ Container temp-test1-1  Created                                                                               0.1s 
 ✔ Container temp-test2-1  Created                                                                               0.0s 
 ✔ Container temp-test3-1  Created                                                                               0.0s 
Attaching to test1-1, test2-1, test3-1
test1-1  | test env_file
test1-1  | FOO=foo
test1-1  | BAR=A_bar
test1-1  | BAZ=A_baz
test1-1 exited with code 0
test2-1  | environment
test2-1  | FOO=A
test2-1  | BAR=B
test2-1  | BAZ=A_baz
test2-1 exited with code 0
test3-1  | both
test3-1  | FOO=A
test3-1  | BAR=B
test3-1  | BAZ=A_baz
test3-1 exited with code 0

Did you see the "surprises"? FOO==foo but BAR==A_bar in test1!!! Who can explain that one? 👍

kundeng avatar Jul 23 '25 03:07 kundeng

Hi @thaJeztah! I noticed that this issue is still open.. are there any plans to fix it? The inconsistent behavior between the two modes of execution (docker run and docker compose) for the same environment-file option is a bit confusing. I’d suggest updating docker run to match docker compose's environment-file behavior by supporting parameter expansion.

/cc @vvoland

thensg avatar Nov 15 '25 15:11 thensg