Chapter 5b of 17 · Visual guide

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);
Always strings. 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

Where a container's environment comes fromImage defaults from Dockerfile ENV, values from --env-file and values from -e are merged by docker run. A run-time value with the same name replaces the image default. The result is the environment your process reads.Dockerfile ENVPORT=4000 NODE_ENV=production--env-file app.envDB_HOST=postgres-e PORT=5000one value on the command linedocker runrun-time value winscontainer envPORT=5000NODE_ENV=productionDB_HOST=postgresread via process.env
The image supplies defaults; docker run adds to them and replaces any with the same name. PORT=5000 beat the image's 4000.

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 writeHost has no PORT
-e PORTPORT is not set in the container (your default applies)
-e PORT=$PORTyour shell expands it first, so PORT is set to an empty string
Flags go before the image name. Everything after the image is the command to run. 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 fileWhat docker run does
# comment and blank linesignored
KEY=valuesets KEY to everything after the first =, exactly as written
KEY="value"the quotes are kept: the value is "value" with the quote marks
KEY=$OTHERnot 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.
Keep env files out of images and Git. 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 also applies to the build steps after it. If 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

When ARG, ENV and -e are visibleA timeline of docker build, image, docker run and container. ARG exists only during docker build. Dockerfile ENV is visible in later build steps, stored in the image and present in every container. Values from -e or --env-file exist only in that one container.docker build--build-argimagestores ENVdocker run-e / --env-filecontainerprocess.envARG: build onlyDockerfile ENV: later build steps, the image, every container-e / --env-file: this container only
ARG ends when the build ends. ENV travels with the image. -e lives in one container.
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 .
ARGENV
Set withdocker build --build-argDockerfile; overridden by docker run -e
Build steps see ityes, after it is declaredyes, after it is declared
Running container sees itnoyes
Use forversions, base-image tags, build switchesruntime 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
Reads your host's PORT
$ docker exec posts echo $PORT

Your shell replaces $PORT before Docker runs anything, so you see the host value (often empty).

Reads the container's PORT
$ 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:

MethodWho can read the value
ENV DB_PASSWORD=... in a Dockerfileanyone 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-filenot in the image, but visible to anyone who can run docker inspect on the container

Try it yourself

  1. Step 1 · One variable
    $ docker run --rm -e GREETING=hello alpine:3.20 printenv GREETING
    hello
  2. Step 2 · Pass-through from the host

    printenv prints 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
  3. 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
  4. 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=5000

    arg= is empty: the ARG existed only during the build. Its value survived because ENV copied it.

  5. 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

Quick check

The Dockerfile has 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.

You changed a value in 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.

Your build needs 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.