How do you make the image referenced by a Dockerfile's FROM instruction configurable at build time, and what is the scoping rule for an ARG declared before the first FROM?
answer
- ARG before first FROM = pre-FROM global scope
- only FROM lines see it; re-declare bare ARG inside stage
- bare ARG inherits, ARG with default overrides
- TARGETARCH/BUILDPLATFORM follow the same rule
- build args are visible in docker history, never secrets
basics
~20 sDeclare ARG above the first FROM and use it in the FROM line, for example ARG BASE=debian:bookworm-slim then FROM ${BASE}. Such an ARG lives in a global scope usable only by FROM lines; to use its value inside a build stage you must re-declare a bare ARG with the same name after that stage's FROM.
solid answer
~50 sPut the ARG before the first FROM and interpolate it: ``` ARG BASE_IMAGE=debian:bookworm-slim FROM ${BASE_IMAGE} ``` then override with `docker build --build-arg BASE_IMAGE=...`. The rule people trip over is scope: ARGs declared before the first FROM are in a special **global/pre-FROM scope** consumed by FROM instructions only. Once you enter a stage, that value is not visible to RUN, ENV or COPY unless you re-declare `ARG BASE_IMAGE` (no default needed) inside the stage, which pulls the outer value in. The predefined proxy and platform args behave similarly: `TARGETPLATFORM`, `TARGETARCH`, `BUILDPLATFORM` are available to FROM automatically and must be re-declared to use inside a stage. Used well this parameterises variants, for example a `BASE_IMAGE` switch between a debug and a distroless runtime, or a shared Dockerfile pinned per environment. I keep a sane default so a plain `docker build` still works, and I avoid using it to smuggle secrets, since build args are visible in image history.
code
dockerfile · 11 linesARG BASE_IMAGE=gcr.io/distroless/java21-debian12
ARG APP_VERSION=dev
FROM eclipse-temurin:21-jdk AS build
ARG APP_VERSION
RUN echo "building ${APP_VERSION}" && ./build.sh
FROM ${BASE_IMAGE}
ARG APP_VERSION
LABEL org.opencontainers.image.version="${APP_VERSION}"
COPY --from=build /out/app.jar /app.jargo deeper
Know the shape: ARG above FROM, then FROM ${ARG}, overridden with --build-arg.
State the scoping rule precisely and show the bare re-declaration inside the stage, including for TARGETARCH.
Use it purposefully for variant and mirrored-registry builds, and explain why build args are unsuitable for secrets and how they affect cache lineage and reproducibility.
Decide whether parameterised bases are worth the reproducibility cost across many repositories, versus generated Dockerfiles or a curated internal base catalogue with digest pins.
## The mechanism A Dockerfile's FROM instruction accepts variable interpolation, but only from build arguments that exist before any stage begins. That is why the pattern is: ``` ARG BASE_IMAGE=debian:bookworm-slim FROM ${BASE_IMAGE} ``` At build time, docker build --build-arg BASE_IMAGE=gcr.io/distroless/base-debian12 substitutes the value. Providing a default is good practice so the Dockerfile still builds with no arguments and so readers can see the intended base. ## The scoping rule The Dockerfile grammar defines a small region before the first FROM, sometimes called the global or pre-FROM scope. ARG is the only instruction allowed there (aside from comments, directives such as syntax, and additional ARGs). Values declared there are consumed by FROM lines. Crucially, they do not automatically leak into build stages. After a FROM, you are inside a stage with its own argument scope, and referencing ${BASE_IMAGE} in a RUN there yields an empty string. The fix is to re-declare it inside the stage with a bare ARG: ``` ARG VERSION=1.2.3 FROM alpine:3.20 ARG VERSION RUN echo "building $VERSION" ``` The bare ARG inherits the outer value rather than resetting it; adding a default in the inner declaration would override the outer value when none is supplied. This same rule applies per stage in a multi-stage build: each stage that needs the value re-declares it. ## Predefined args BuildKit supplies platform args automatically in the pre-FROM scope: BUILDPLATFORM, BUILDOS, BUILDARCH (the machine performing the build) and TARGETPLATFORM, TARGETOS, TARGETARCH, TARGETVARIANT (the platform being produced). They can be used directly in FROM lines, which is the basis of cross-compiling multi-platform builds: ``` FROM --platform=$BUILDPLATFORM golang:1.23 AS build ARG TARGETOS TARGETARCH RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app . ``` Note the re-declaration inside the stage, exactly as with user-defined args. Proxy variables (HTTP_PROXY, NO_PROXY and lowercase forms) are also predefined and are excluded from image history. ## Practical uses - **Variant builds from one Dockerfile.** A BASE_IMAGE arg lets CI produce a distroless production image and a busybox-based debug image with identical application layers, which is a common answer to the "how do you debug a shell-less image" problem. - **Pinning per environment.** Some organisations mirror approved bases into an internal registry; a REGISTRY or BASE_IMAGE arg lets the same Dockerfile build from the public base locally and the mirrored, digest-pinned base in CI. - **Version matrices.** Building the same service against several runtime versions (for example JDK 21 and 25) to test compatibility. ## Cautions Build args are not secrets. Values passed with --build-arg are recorded in the image config and visible via docker history, and they are not the mechanism for credentials; BuildKit's --mount=type=secret exists for that. Parameterised bases also weaken reproducibility. If the default in the Dockerfile and the value CI actually passes diverge, a local build reproduces something different from production. Keep the CI value in a checked-in file (a Makefile, a compose file, a workflow) rather than in a person's shell history, and prefer a digest-pinned value for release builds. Finally, cache behaviour follows the argument: changing BASE_IMAGE changes the base layers, so the whole stage rebuilds. That is correct but worth expecting, particularly when a matrix build alternates values and each variant maintains its own cache lineage.
- Why not pass a private registry token with --build-arg instead of BuildKit's secret mount?Build args are persisted in the image configuration and are visible with docker history, so anyone who can pull the image can read the token; they are also visible in build logs and CI configuration. BuildKit's RUN --mount=type=secret exposes the value as a tmpfs file only for the duration of that RUN instruction and never records it in a layer or in history, which is the correct mechanism for credentials.
saying these in an interview costs you the question
- Putting the ARG after the FROM and expecting FROM to interpolate it
- Assuming a pre-FROM ARG is automatically visible inside every stage
- Re-declaring the ARG inside a stage with a new default and then wondering why the --build-arg value was ignored
- Using --build-arg for tokens or passwords because it 'does not appear in the Dockerfile'
- Thinking predefined args such as TARGETARCH need no re-declaration inside a stage