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: 5b4dbfdc0d5c7fa89543e04cd30611a9d9ddeeb4d1a4d7e91084cfad926564a1Someone 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. Docker's env-file format is stricter and simpler than a shell script, and this lesson covers its rules, how -e, --env-file and the image's own defaults combine, and 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 because the same image can then run 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: your two variables plus 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, which is handy in CI where the value already sits in the job's environment and should not be typed into the command line. If that variable is not set where you run docker, the container does not get it at all, and printenv exits with status 1 because there is 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 is not shell syntax, and Docker does not try to guess. A space around = or an export in front of the name makes the name contain whitespace, and the docker CLI 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 is not 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, and is left out if your shell does not 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 does not, 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:24-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 ones the node base image set (NODE_VERSION, 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: the image's ENV, then the values from --env-file, then each -e. A value given with -e therefore 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 is the wrong home for passwords and tokens. The value is stored in plain text in the container's configuration, so 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; 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, and docker inspect -f '{{json .Config.Env}}' <name> answers it from outside, also for a stopped container.
Clean up
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?Try this
Work through “Clean up” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
If you keep one thing from environment variables and configuration, keep “Clean up”. Decide now which check you will run when this shows up on a live system, and write it somewhere your team will find it.