ARG vs ENV
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.
docker build and is gone from the running container, yet still visible in docker history.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 ENV — ARG VER then ENV APP_VERSION=$VER — to deliberately bridge a build value into the runtime, which is the one sanctioned crossover between the two.
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 — 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).
- Passing a secret with
--build-arg DB_PASSWORD=...and assuming it is gone from the image —docker historystill 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 withdocker inspect, and shared with every container started from the image. - Declaring an
ARGand expecting the running container to read it —ARGis build-time only; the app sees nothing, and the value the app needs has to be anENVor a run-time-e. - Putting an
ARGafterFROMand expecting it to affect theFROMline — pre-FROMand post-FROMARGs have different scopes, andFROMonly sees the ones declared above it.
- Use
ARGfor build parameters (base image version, build flags, feature toggles) that the running container has no need to see. - Use
ENVfor non-secret runtime configuration the app reads (PORT, log level, feature flags), overridable per-run withdocker run -e. - Keep every secret out of both
ARGandENV— pass build secrets with BuildKit--mount=type=secretand runtime secrets with Docker/Compose secrets so nothing lands in a layer or indocker history. - Bridge a build value into the runtime deliberately with
ARG VERfeedingENV APP_VERSION=$VERonly when the runtime genuinely needs that value, not as a habit.
--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?
ARGexists only during the build;ENVpersists into the running containerENVexists only during the build;ARGpersists into the running container- Both exist at run time; the difference is only which one
docker run -ecan 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
ARGvalues are permanently stored as live environment variables inside the running container's own process space, directly readable at any time with theenvcommand- 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-cachestrips 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 inspectby anyone who pulls the image ENVis build-time only and discarded afterward, so the app cannot read the secret when it actually needs itENVencrypts 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 VERfeedingENV APP_VERSION=$VER - Always, since bridging an
ARGinto anENVis the standard, recommended way to pass secrets safely - Whenever you specifically want the build-time
ARGitself to become directly overridable later at run time with adocker run -eflag - To make a post-
FROMARGretroactively visible to theFROMline declared above it
You got correct