Environment variables
Build one image, then configure it differently for development, staging and production without rebuilding. Environment variables are how you hand that configuration in.
key idea / Docker itselfcontainer / good pathhost / watch outproblem / blockedneutral
What an environment variable is
A named string that the operating system gives a process when it starts. Docker sets them for the container's main process, and your code reads them like any other variable.
PORT=4000
NODE_ENV=production
DB_HOST=postgres
// Node.js
const port = Number(process.env.PORT ?? 4000); // values are always strings
const dbHost = process.env.DB_HOST;
if (!dbHost) throw new Error('DB_HOST is not set');
app.listen(port);
process.env.PORT is "4000", not 4000, and process.env.DEBUG set to false is the truthy string "false". Convert explicitly, and fail fast when a required variable is missing.Where the values come from
Setting them with -e
-e (long form --env) sets one variable. Repeat it for more.
$ docker run -d --name posts \
-e PORT=4000 \
-e NODE_ENV=production \
-e DB_HOST=postgres \
micro-posts
Give only a name and Docker copies the value from your host shell:
$ export PORT=4000
$ docker run -e PORT micro-posts # container gets PORT=4000
| You write | Host has no PORT |
|---|---|
-e PORT | PORT is not set in the container (your default applies) |
-e PORT=$PORT | your shell expands it first, so PORT is set to an empty string |
docker run micro-posts -e PORT=4000 does not set PORT: it replaces the image's CMD with -e PORT=4000.Many at once: --env-file
Put the values in a file, one KEY=value per line, and pass the file.
# app.env
PORT=4000
NODE_ENV=production
DB_HOST=postgres
DB_PORT=5432
$ docker run --env-file app.env micro-posts
| Line in the file | What docker run does |
|---|---|
# comment and blank lines | ignored |
KEY=value | sets KEY to everything after the first =, exactly as written |
KEY="value" | the quotes are kept: the value is "value" with the quote marks |
KEY=$OTHER | not expanded: the value is the literal text $OTHER |
KEY (no =) | copies KEY from the host, like -e KEY |
docker run never reads .env by itself. Only --env-file loads a file. Docker Compose is different: it reads a .env file next to the compose file automatically, and its parsing rules (quotes, ${VAR}) are not the same as --env-file.COPY . . copies everything in the build context, including .env. Add it to .dockerignore (see chapter 04) and to .gitignore. Commit a .env.example with names and fake values instead.Defaults in the Dockerfile: ENV
ENV bakes a default into the image. Every container started from it gets the value unless docker run overrides it.
FROM node:22
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV PORT=4000 \
NODE_ENV=production
CMD ["node", "server.js"]
$ docker run micro-posts # PORT=4000 (image default)
$ docker run -e PORT=5000 micro-posts # PORT=5000 (run time wins)
ENV NODE_ENV=production sat above RUN npm ci, npm would skip devDependencies, and a later build step that needs them (TypeScript, a bundler) would fail. Place ENV where you mean it to take effect.Base images set variables too. node:22 sets NODE_VERSION, and nearly every image sets PATH; you will see them in env output.
ARG versus ENV
ARG VERSION=dev # default, overridable at build time
RUN echo "building $VERSION" # visible here
ENV APP_VERSION=$VERSION # copy it into ENV to keep it at run time
$ docker build --build-arg VERSION=2.0 -t micro-posts .
ARG | ENV | |
|---|---|---|
| Set with | docker build --build-arg | Dockerfile; overridden by docker run -e |
| Build steps see it | yes, after it is declared | yes, after it is declared |
| Running container sees it | no | yes |
| Use for | versions, base-image tags, build switches | runtime defaults your app reads |
One scoping rule: an ARG declared before FROM can only be used in FROM lines. To use it in a later step, declare ARG NAME again (no value) after FROM.
Seeing what a container got
$ docker exec posts env # every variable
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=4f1c2a9e8b7d
NODE_VERSION=22...
PORT=4000
NODE_ENV=production
DB_HOST=postgres
...
$ docker exec posts printenv PORT # just one
4000
$ docker inspect --format '{{json .Config.Env}}' posts # without entering the container
$ docker exec posts echo $PORTYour shell replaces $PORT before Docker runs anything, so you see the host value (often empty).
$ docker exec posts sh -c 'echo $PORT'Single quotes stop your shell; the shell inside the container expands it.
Set once, at creation
A container's environment is fixed when it is created. There is no command to change it on a running container, and docker restart keeps the old values. To change a value, remove the container and run a new one.
$ docker rm -f posts
$ docker run -d --name posts -e PORT=5000 micro-posts
Variables set inside a shell with docker exec ... export X=1 disappear when that shell exits; they never reach the main process.
Secrets
Environment variables are not a secure store. Know where each method leaks:
| Method | Who can read the value |
|---|---|
ENV DB_PASSWORD=... in a Dockerfile | anyone with the image: docker image inspect, docker history |
--build-arg DB_PASSWORD=... | can be recorded in the image history; Docker warns against using build args for secrets |
-e or --env-file | not in the image, but visible to anyone who can run docker inspect on the container |
- Never put real secrets in
ENVorARG. - Local development: an untracked env file with
--env-fileis fine. - A secret needed during the build (a private registry token): BuildKit secret mounts,
RUN --mount=type=secret,id=npmrc ...withdocker build --secret id=npmrc,src=.npmrc. The file is available to that one step and is not saved in a layer. - Production: the platform's secret store (Kubernetes Secrets, Docker Swarm secrets, AWS Secrets Manager, Vault). These usually mount secrets as files rather than variables.
Try it yourself
-
Step 1 · One variable
$ docker run --rm -e GREETING=hello alpine:3.20 printenv GREETING hello -
Step 2 · Pass-through from the host
printenvprints nothing and exits with status 1 when the variable does not exist.$ export TEAM=web $ docker run --rm -e TEAM alpine:3.20 printenv TEAM web $ docker run --rm -e NOT_ON_HOST alpine:3.20 printenv NOT_ON_HOST $ echo $? 1 -
Step 3 · An env file keeps quotes
$ printf '# demo\nMODE="debug"\nLEVEL=2\n' > demo.env $ docker run --rm --env-file demo.env alpine:3.20 sh -c 'echo "$MODE $LEVEL"' "debug" 2 -
Step 4 · ARG, ENV and an override
$ mkdir envdemo && cd envdemo $ cat > Dockerfile <<'DF' FROM alpine:3.20 ARG VERSION=dev ENV APP_VERSION=$VERSION PORT=4000 CMD ["sh", "-c", "echo version=$APP_VERSION arg=$VERSION port=$PORT"] DF $ docker build --build-arg VERSION=2.0 -t envdemo . $ docker run --rm envdemo version=2.0 arg= port=4000 $ docker run --rm -e PORT=5000 envdemo version=2.0 arg= port=5000arg=is empty: the ARG existed only during the build. Its value survived because ENV copied it. -
Step 5 · Whose $PORT?
$ unset PORT $ docker run -d --name api -e PORT=3000 alpine:3.20 sleep 300 $ docker exec api echo $PORT $ docker exec api sh -c 'echo $PORT' 3000 $ docker rm -f api
Recap
- Same image, different
-e/--env-filevalues per environment. No rebuild. - Run-time values replace image
ENVdefaults with the same name. -e KEYcopies from the host; unset on the host means unset in the container.--env-filekeeps quotes and does not expand$VARS;docker runnever loads.envon its own.ARGis build time only;ENVreaches build steps after it and every container.- Environment is fixed at creation: recreate the container to change it.
- No secrets in
ENVorARG;-evalues show indocker inspect.
Quick check
ENV PORT=4000. You run docker run -e PORT=5000 micro-posts. What does the app see, and does the image change?Show answer
PORT=5000. The run-time value wins for this container only; the image still says 4000.
app.env and ran docker restart posts. The app still sees the old value. Why?Show answer
The env file is read once, when the container is created. Remove the container and docker run --env-file app.env ... again.
VERSION, and the running app should print it too. ARG or ENV?Show answer
Both: ARG VERSION to receive --build-arg, then ENV APP_VERSION=$VERSION so it persists into containers.