The problem
docker run --env-file and the env_file: key of Docker Compose both read
.env files, but they follow different rules. docker run takes every value
literally: quotes, inline # comments and trailing spaces become part of the
value, and lines with an export prefix or with KEY = value are errors.
Compose and Bash strip the quotes, drop inline comments and expand ${VAR}. A
file that works with source .env and with Compose therefore passes
"My App", including the quotes, to a container started with docker run.
The issue is documented:
- docker/cli#3630, "Handle quotes in --env-file values consistently with Linux/WSL2 sourcing", has 134 reactions and 58 comments and has been open since 2022.
- Three pull requests that change the parser are open and unmerged (#3660, #6046, #6805). Changing the parser would alter how existing files are read.
- The workaround described in the issue is a second, unquoted copy of every
.envfile. dotenv-linter checks style. It does not show how each tool reads a value.
How it works
envfile-compat contains a model
of each reader. The docker run model follows the rules of pkg/kvfile in
docker/cli: leading whitespace is removed, # lines are skipped, the key ends
at the first =, and everything after it is the value. A line without =
takes its value from the environment. The Compose model follows the dotenv
parser of compose-go. It handles the export prefix, KEY: value, single
quotes (literal), double quotes (escapes, variable expansion, line breaks) and
unquoted values with # comments. References such as ${VAR} and
${VAR:-default} are resolved as Compose does, first from the calling
environment and then from earlier lines of the file.
envfile-compat check FILE reads the file with both models and lists every
variable whose values differ, plus every line that docker run rejects. The
exit code is 1 when anything differs, so the check can run in CI. With
--bash the file is also sourced in a clean bash with set -a; this
executes the file. envfile-compat docker FILE prints the variables as
Compose reads them, in the literal KEY=value form that docker run expects.
This is the output for a three-line file:
$ cat .env
APP_NAME="My App"
GREETING=hello # say hi
PORT=8080
$ envfile-compat check .env
APP_NAME
docker run '"My App"'
compose 'My App'
GREETING
docker run 'hello # say hi'
compose 'hello'
2 variable(s) differ. `envfile-compat docker .env` writes a file that
docker run --env-file reads the way Compose does.
$ envfile-compat docker .env
APP_NAME=My App
GREETING=hello
PORT=8080
The converted file can be passed directly, for example with
docker run --env-file <(envfile-compat docker .env) myimage. The tool needs
Python 3.10 or later and has no dependencies.
Testing
The unit tests run both models over a corpus file with quoting, comment,
escape and interpolation cases, an export line, KEY = value,
KEY: value, an indented line, trailing spaces and a key without a value.
Further tests cover the interpolation operators (-, :-, +, :+, :?),
multi-line values, unterminated quotes, the Bash reader, and the conversion:
the output of envfile-compat docker must parse back to the same values.
A second group of tests compares the models with the real tools.
docker compose config must return exactly the values of the Compose model
for the corpus, and env inside a container started with
docker run --env-file must return exactly the values of the docker run
model. The converted file must also read back through docker run
unchanged. These tests are skipped when Docker is not available. CI runs the
suite on Python 3.10 to 3.13 with Docker Compose v2 and the alpine:3.20
image.
Limitations
- Values with line breaks cannot be expressed in the format
docker runreads.envfile-compat dockerreports them as an error. check --bashexecutes the file and should only be used on trusted files.- The two readers are reimplementations. Their agreement with Docker is checked for the cases in the test corpus.
The code and installation instructions are at github.com/Jwrede/envfile-compat.