Environment variables and configuration
-e, --env-file, the parsing rules Docker applies, and why env vars are not secret.
cd ~/lab && curl -fsSLO https://secopslog.com/lab-files/docker-fund/env.tar.gz && tar -xzf env.tar.gz, which creates ~/lab/env/. SHA-256: 31060397b45ab5c62273d658714fc0e4b45a57c44c7084fccf62ab4b22cd189bSomeone copies the settings block from a deployment shell script into a file, passes it to docker run --env-file, and Docker refuses to start anything: variable 'export NAME' contains whitespaces, exit status 125. The file looked fine to a human. But Docker's env-file format is stricter and simpler than a shell script. Here you'll learn its rules and how -e, --env-file and the image's own defaults combine. You'll also see why none of them is a place for a password.
An environment variable is a NAME=value pair the operating system hands to a process when it starts. Programs read the names they know and ignore the rest. Containers use them for configuration. That way you can run the same image on a laptop, in CI and in production with different settings and no rebuild. Use the main lab VM and unpack the lesson files into ~/lab/env.
One variable at a time with -e
-e NAME=value sets one variable for one container. Repeat the flag for more. The shell inside the container expands $NAME when it runs the echo. Running env instead prints the whole environment. You'll see your two variables, PATH and HOME from the image, and HOSTNAME, which Docker sets to the short container ID.
-e NAME without =value copies the variable from the environment of the docker command. That's handy in CI, where the value already sits in the job's environment and shouldn't be typed into the command line. If that variable isn't set where you run docker, the container doesn't get it at all. Then printenv exits with status 1 because there's nothing to print.
Many variables with --env-file
An env file holds one NAME=value per line. Lines that start with # are comments and blank lines are skipped. Docker reads the file on the client side, before it asks the daemon to create anything.
The format isn't shell syntax, and Docker doesn't try to guess. A space around = or an export in front of the name makes the name contain whitespace. The docker CLI then rejects the whole file with exit status 125, the code for "the docker command itself failed". No container is created.
Other differences are silent, which makes them worse. Quotes are part of the value, so QUOTED="Priya" gives the application a seven-character string with two quote marks in it. A # after the value isn't a comment either. EMPTY= sets a variable to the empty string, which is different from not setting it. A bare name with no = behaves like -e NAME, copying the value from your shell. It's left out if your shell doesn't have it.
Compose reads .env files with its own parser, which does remove the quotes, so the same file can produce different values under docker run and docker compose. "Multi-container apps with Compose" covers Compose's rules. When a file has to work in both, write values without quotes or comments.
Which value wins
An image can carry defaults of its own, set with the ENV instruction in its Dockerfile. The lesson files include a small Node.js program and a four-line Dockerfile for it. "Writing a Dockerfile" goes through every instruction in the next lesson. The program needs two settings. PORT has a default in the image, DATABASE_URL doesn't, and the program refuses to start without it.
const port = process.env.PORT;const dbUrl = process.env.DATABASE_URL;if (!dbUrl) {console.error('FATAL: DATABASE_URL is not set');process.exit(1);}console.log(`listening on ${port}, database ${dbUrl}`);
FROM node:26-alpineENV PORT=3000WORKDIR /appCOPY server.js .CMD ["node", "server.js"]
Run it with nothing set, then with the missing value, then with a different port. The first run is the good kind of failure. The program names the missing variable and exits with status 1, and --rm removes the stopped container. The second run takes PORT from the image and DATABASE_URL from -e. The third overrides the image default without rebuilding anything.
The defaults are stored in the image configuration, next to the one the node base image set (NODE_VERSION). The Node.js 26 image no longer ships Yarn, so there is no YARN_VERSION. Anyone who has the image can read them this way.
When the same name is set in several places, Docker applies them in a fixed order and the last one wins. First comes the image's ENV, then the values from --env-file, then each -e. So a value you give with -e beats the same name in an env file, whatever order the flags appear in.
Environment variables are not secret
Everything above is fine for ports, hostnames and feature flags. It's the wrong home for passwords and tokens. The value is stored in plain text in the container's configuration. Anyone who can run docker inspect on the host reads it, and any process inside the container reads it from its own environment. Values typed after -e also land in your shell history, and applications tend to dump their environment into crash reports and debug pages.
For real credentials, mount them as files and have the application read the file. Many official images support this with a _FILE variant of the variable, for example POSTGRES_PASSWORD_FILE in the postgres image. "Runtime secrets, done right" in Advanced container security covers the options and their trade-offs. Keep in mind that being in the docker group already gives a user root-level control of the host. So docker inspect access is never a small thing to hand out.
When the application ignores a variable
Three checks find nearly every case. Names are case-sensitive, so DATABASE_URL and Database_Url are different variables. The program may never read the name you set, so check its documentation or code. And the container may predate your change, because the environment is fixed at creation. docker exec <name> printenv NAME answers the question from inside the running container in one command. docker inspect -f '{{json .Config.Env}}' <name> answers it from outside, also for a stopped container.
Clean up
Hands-on
Test the precedence rule from the other side. Run the greet.env command again, but put -e GREETING=from-cli before --env-file greet.env instead of after it. Predict what printenv GREETING prints, check it against the from-cli result above, then try the same file with no -e at all.
Takeaway
An env file is NAME=value per line and nothing more. A stray export stops the run with exit status 125, while quotes and trailing # text pass silently into the value, as QUOTED="Priya" showed. Keep passwords out of all of it: docker inspect printed DB_PASSWORD=example-only-not-real in plain text to anyone with access to the daemon.
docker run --env-file prod.env myapp exits immediately with status 125 and invalid env file (prod.env): variable 'export DB_HOST' contains whitespaces. What is wrong?GREETING="hello". What value does a program in the container started with docker run --env-file see?ENV PORT=3000. You run it with --env-file app.env -e PORT=8080, and app.env contains PORT=9000. Which port does the application read?