Parameter expansion when using `docker run --env-file ....`
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
-
echo VAR=\$USER > env.list -
docker run --env-file env.list --rm -it ubuntu env | grep VAR - =>
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
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)
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
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.
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!
Hey @thaJeztah,
What do you think, is there a possibility to implement such functionallity? Maybe you have some feedbask from developers team?
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
-
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}_bazyou get
BAR=foo_barandBAZ=foo_bazat container runtime.
-
-
docker run --env-filedoes 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}_barverbatim.
- The standalone Docker CLI simply reads each line as a literal
-
environment:is resolved at Compose‑parse time- Entries under
environment:with${…}are expanded from your shell, from--env-filefiles passed to the Compose CLI, or from.env—never from a service’s ownenv_file:. - If
${FOO}isn’t present in the Compose parse environment, you’ll get an empty string and that will override anyenv_file:value.
- Entries under
-
environment:always overridesenv_file:- If you list the same variable in both, the
environment:value “wins” at container start.
- If you list the same variable in both, the
-
Practical recommendations
-
For simple runtime injection without Compose parsing: use
env_file:in Compose, or--env-filewithdocker 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 underenvironment:so that${…}is expanded before the container sees it. -
If you need both: either merge and pre‑expand your
.envfiles, or rely on Compose’s--env-fileflags rather thanenv_file:alone.
-
For simple runtime injection without Compose parsing: use
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? 👍
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