Chapter 4: Dockerfiles
Topic 25

ARG vs ENV

InstructionConfig

Both ARG and ENV set values in a Dockerfile, and the difference is when they exist. ARG is build-time only — it parameterizes the build and is gone once the image is built, though, dangerously, still visible in docker history. ENV persists into the running container — it is a real environment variable every process in the container can read.

Confusing the two leaks secrets into image history or sets build-only values that vanish exactly when the app needs them. Get the distinction wrong in one direction and a password is extractable by anyone who pulls the image; get it wrong in the other and the variable the app reads at startup simply is not there. Neither instruction is a safe place for a secret — a point this topic returns to.

ARG — Build-Time Parameter

Declared in the Dockerfile and supplied with --build-arg, an ARG is available only during docker build — for example ARG PYTHON_VERSION=3.12 feeding FROM python:${PYTHON_VERSION}. It does not exist in the running container at all; query it from inside a container and you get nothing, because it was consumed and discarded when the build finished.

ENV — Run-Time Environment

ENV sets a variable that is baked into the image config and present for every process in every container started from the image. Driftwood's ENV PORT=8000 is readable by gunicorn at startup, and overridable per-run with docker run -e PORT=9000. This is the right home for non-secret runtime configuration the app actually reads.

When the value exists
ARG
Build-time only — parameterizes docker build and is gone from the running container, yet still visible in docker history.
ENV
Persists into the running container — a real environment variable every process reads, visible via docker inspect.

The docker history Leak

ARG values passed at build time are recorded in the image's build history, so --build-arg DB_PASSWORD=hunter2 is extractable with docker history even though the variable is "gone" from the running container. This is a real-secret footgun: the value never reaches the container, so it feels safe, but it is sitting in plain text in the layer metadata of every copy of the image.

The Real Fix Is BuildKit Secrets

Neither ARG nor ENV is a safe place for a secret. Build-time secrets belong in BuildKit's --mount=type=secret, which exposes the value to a single RUN without ever landing it in a layer or in history; run-time secrets belong in Docker or Compose secrets, which mount the value as a file the running process reads. Both land in Chapters 5 and 10 — this topic names the trap and points to the fix.

Scope and Interaction

An ARG declared before FROM is usable only in the FROM line; an ARG after FROM is usable in the build stage. And an ARG can supply the default for an ENVARG VER then ENV APP_VERSION=$VER — to deliberately bridge a build value into the runtime, which is the one sanctioned crossover between the two.

Scope, and the one sanctioned crossover
ARG PYTHON_VERSION=3.12      # before FROM: usable only in FROM
FROM python:${PYTHON_VERSION}

ARG VER                       # after FROM: usable in this stage
ENV APP_VERSION=$VER          # bridge a build value into the runtime
ENV PORT=8000                 # plain runtime config, overridable with -e

The first ARG picks the base image and then is unreachable below the FROM. The second is bridged into an ENV so the running container can read the version — the only place a build value should cross into the runtime, and only when the runtime genuinely needs it.

ARG vs ENV

ARG — a build-time variable: set with --build-arg, available only during docker build, and absent from the running container — but recorded in docker history, so never a safe place for secrets. Use it to parameterize the build (base version, build flags).

ENV — a run-time variable: baked into the image config, present for every process in the container, and overridable with docker run -e. Use it for configuration the running app reads. For secrets, use neither — use BuildKit and Compose secrets (Ch5, Ch10).

Common Mistakes
  • Passing a secret with --build-arg DB_PASSWORD=... and assuming it is gone from the image — docker history still shows it; the only safe build-time secret is BuildKit's --mount=type=secret.
  • Baking a secret into ENV SECRET_KEY=... in the Dockerfile — it is now in the image config for anyone who pulls it, readable with docker inspect, and shared with every container started from the image.
  • Declaring an ARG and expecting the running container to read it — ARG is build-time only; the app sees nothing, and the value the app needs has to be an ENV or a run-time -e.
  • Putting an ARG after FROM and expecting it to affect the FROM line — pre-FROM and post-FROM ARGs have different scopes, and FROM only sees the ones declared above it.
Best Practices
  • Use ARG for build parameters (base image version, build flags, feature toggles) that the running container has no need to see.
  • Use ENV for non-secret runtime configuration the app reads (PORT, log level, feature flags), overridable per-run with docker run -e.
  • Keep every secret out of both ARG and ENV — pass build secrets with BuildKit --mount=type=secret and runtime secrets with Docker/Compose secrets so nothing lands in a layer or in docker history.
  • Bridge a build value into the runtime deliberately with ARG VER feeding ENV APP_VERSION=$VER only when the runtime genuinely needs that value, not as a habit.
Comparable tools BuildKit --mount=type=secret is the build-time-secret mechanism Docker added precisely because ARG leaks Podman · Buildah honor the same ARG/ENV semantics and history Buildpacks expose build-time and run-time environment as separate, declared phases

Knowledge Check

Which of ARG and ENV exists at build time versus run time?

  • ARG exists only during the build; ENV persists into the running container
  • ENV exists only during the build; ARG persists into the running container
  • Both exist at run time; the difference is only which one docker run -e can override
  • Both exist only at build time and are discarded once the image is built

Why does a secret passed as an ARG survive in docker history?

  • Build-arg values are recorded in the image's build history as plain text, extractable even though the container never sees them
  • ARG values are permanently stored as live environment variables inside the running container's own process space, directly readable at any time with the env command
  • The secret is written into a dedicated file layer on disk and stays in the image filesystem forever once committed
  • Only a cached build leaks the value; a clean rebuild with --no-cache strips it from the recorded build history

Why is ENV SECRET_KEY=... in a Dockerfile unsafe?

  • It bakes the value into the image config, readable with docker inspect by anyone who pulls the image
  • ENV is build-time only and discarded afterward, so the app cannot read the secret when it actually needs it
  • ENV encrypts the value, but the key is shipped in the same image so it offers no protection
  • It writes the secret to the daemon's log on every container start, exposing it in plaintext logs

When should an ARG be bridged into an ENV?

  • Only when the running container genuinely needs a build value, e.g. ARG VER feeding ENV APP_VERSION=$VER
  • Always, since bridging an ARG into an ENV is the standard, recommended way to pass secrets safely
  • Whenever you specifically want the build-time ARG itself to become directly overridable later at run time with a docker run -e flag
  • To make a post-FROM ARG retroactively visible to the FROM line declared above it

You got correct