skip to content

In a Dockerfile, what do BUILDPLATFORM and TARGETARCH mean, and what does FROM --platform=$BUILDPLATFORM do?

level: middleimportance: should knowfreq 44%

answer

  1. Two machines, not one
  2. Where the build runs versus what it produces
  3. The variables exist but the stage must declare one
  4. Go-style names, not uname names
  5. Pin the toolchain stage, leave the runtime stage alone

basics

~20 s

BUILDPLATFORM describes the machine running the build; TARGETARCH describes the architecture currently being produced. Pinning a stage with FROM --platform=$BUILDPLATFORM keeps that stage on the builder's own architecture so its toolchain runs natively and cross-produces output for TARGETARCH.

solid answer

~40 s

BuildKit predefines two families of build arguments. `BUILDPLATFORM`, `BUILDOS`, `BUILDARCH` describe the machine doing the building; `TARGETPLATFORM`, `TARGETOS`, `TARGETARCH`, `TARGETVARIANT` describe the platform currently being produced, and the whole Dockerfile is evaluated once per entry in `--platform`. They exist without being passed with `--build-arg`, but a stage must re-declare a bare `ARG TARGETARCH` before referring to it, otherwise the variable expands to empty. By default every stage is built *for* the target platform, so on a cross-architecture build the compiler itself runs emulated. Writing `FROM --platform=$BUILDPLATFORM <toolchain> AS build` pins that one stage to the builder's native architecture; the stage then uses `$TARGETOS`/`$TARGETARCH` to decide what to produce, and only the final, unpinned stage takes the target-architecture base image.

code

dockerfile · 10 lines
dockerfile
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY . .
RUN ./gradlew --no-daemon bootJar

FROM eclipse-temurin:21-jre
ARG TARGETARCH
COPY --from=build /src/build/libs/ingest-batch.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

go deeper

for a junior

Learn the vocabulary first: BUILD* describes the machine doing the work, TARGET* describes the image being produced, and the architecture names are amd64 and arm64 rather than the names uname prints.

for a middle

Be ready to write the pattern from memory: the toolchain stage pinned with FROM --platform=$BUILDPLATFORM, a bare ARG TARGETARCH before any use, and an unpinned final stage that picks up the target's runtime base.

for a senior

Explain what the pin buys in wall-clock terms on a real build, and be able to spot the two failure signatures in review: an empty architecture variable in a download URL, and a compiler stage silently running emulated.

for a principal

Decide whether the organisation standardises on this cross-build pattern in shared base images and templates, or accepts emulation for low-frequency builds, and weigh the maintenance cost of toolchain-specific cross-compilation against builder fleet cost.

## Two platforms in every cross-architecture build When you run `docker buildx build --platform linux/amd64,linux/arm64 .`, there are two distinct machines in play, and confusing them is the root of most multi-architecture Dockerfile bugs: - the **build platform** — the architecture of the machine (or builder container) actually executing the instructions; - the **target platform** — the architecture of the image being produced. BuildKit evaluates the whole Dockerfile once per requested target platform. So the two-value command above runs the file twice, and in each run `TARGETPLATFORM` differs while `BUILDPLATFORM` stays the same. ## The predefined build arguments BuildKit makes these available without any `--build-arg`: | Argument | Meaning | Example value | |---|---|---| | `BUILDPLATFORM` | platform of the builder | `linux/amd64` | | `BUILDOS`, `BUILDARCH`, `BUILDVARIANT` | its components | `linux`, `amd64` | | `TARGETPLATFORM` | platform being produced | `linux/arm64` | | `TARGETOS`, `TARGETARCH`, `TARGETVARIANT` | its components | `linux`, `arm64`, `v7` for 32-bit arm | Two details catch people out. First, the values use OCI/Go-style names: the architecture is `amd64` and `arm64`, never `x86_64` or `aarch64`, so a URL template built from `TARGETARCH` will not match a vendor that publishes `aarch64` assets without a translation step. Second — the scope rule — these arguments are *available* to the build, but a stage must declare `ARG TARGETARCH` (no default, no value) before it can expand `$TARGETARCH`. Forget that and the variable is empty, which typically shows up as a download of `agent-linux-.tar.gz` and a confusing 404 rather than a clean error. `--platform` on a `FROM` line is the only place these variables usually appear outside a `RUN`, and it is resolved by BuildKit itself, so it works there without an `ARG` declaration in that stage. ## What pinning a stage does Without `--platform` on `FROM`, every stage is built *for* the target platform. On a cross-architecture build that means the base image is pulled for the target architecture and every `RUN` in it executes under emulation. For a stage that only copies files that is harmless; for a stage that runs a compiler or a dependency resolver it is the single biggest cost in the build. `FROM --platform=$BUILDPLATFORM <toolchain-image> AS build` overrides that for one stage: the toolchain image is pulled for the *builder's* architecture and its `RUN` steps execute natively at full speed. The stage is then responsible for producing output for the target, which is where `TARGETOS`/`TARGETARCH` come in — you pass them to whatever the toolchain's cross-compilation switch is. The final stage stays unpinned so it still gets the target-architecture runtime base, and `COPY --from=build` carries the artefact across. A nice consequence: when the pinned stage's work does not depend on any `TARGET*` variable, it is identical for both targets, so the builder does the work once and reuses it for both. ## A JVM example, where the payoff is unusually clean A Java batch job is the easiest case of all, because compiled bytecode is architecture-independent. The compile stage never needs to know the target at all — pin it to the build platform and it runs natively for both architectures; only the runtime base (a distroless JRE image, say) differs per architecture. ```dockerfile # syntax=docker/dockerfile:1 FROM --platform=$BUILDPLATFORM eclipse-temurin:21-jdk AS build WORKDIR /src COPY . . RUN ./gradlew --no-daemon bootJar FROM eclipse-temurin:21-jre COPY --from=build /src/build/libs/ingest-batch.jar /app/app.jar ENTRYPOINT ["java", "-jar", "/app/app.jar"] ``` Building this for `linux/amd64,linux/arm64` runs Gradle once, natively, and produces two images that differ only in their JRE base layers. Contrast the naive version without the `--platform` pin, where a 3-minute Gradle build becomes a long emulated one on the non-native target. The moment the image needs anything architecture-specific — a native profiling agent, a JNI library — `TARGETARCH` earns its keep: ```dockerfile FROM eclipse-temurin:21-jre ARG TARGETARCH ADD --checksum=sha256:... \ https://downloads.example.com/agent/2.4.1/agent-linux-${TARGETARCH}.tar.gz /tmp/agent.tgz ``` ## Common mistakes - Using `$TARGETARCH` without the bare `ARG TARGETARCH` in that stage, and getting an empty string. - Assuming `TARGETARCH` gives `aarch64`, because that is what `uname -m` prints on the same box. - Pinning the *final* stage to `$BUILDPLATFORM`, which defeats the whole exercise — that stage is the one that must carry the target architecture's runtime. - Forgetting `TARGETVARIANT`: 32-bit arm needs it, since `linux/arm/v6` and `linux/arm/v7` share `TARGETARCH=arm`.

  • A stage does `RUN echo $TARGETARCH` and prints an empty line. What is wrong?
    The stage never declared it. The predefined platform arguments are available to the build, but each stage must re-declare a bare `ARG TARGETARCH` before `$TARGETARCH` expands inside that stage. Without the declaration it is simply an undefined variable, which the shell expands to nothing rather than failing.
  • Bytecode is architecture-independent, so why does a Java image need a multi-platform build at all?
    Because the image is more than the jar. The JRE base layers, any native libraries or agents, and the recorded architecture of the image itself are all architecture-specific. A tag built only for amd64 will not start on an arm64 host, however portable the bytecode inside it is.
  • What is TARGETVARIANT for?
    It distinguishes revisions within one architecture, most visibly on 32-bit arm: `linux/arm/v6` and `linux/arm/v7` both report `TARGETARCH=arm` and differ only in `TARGETVARIANT`. If you build a URL or a package name from `TARGETARCH` alone on those platforms, you will fetch the wrong artefact.

It is the difference between the workshop and the destination: BUILDPLATFORM is the bench the work happens on, TARGETARCH is the shelf the finished part has to fit.

saying these in an interview costs you the question

  • Using $TARGETARCH without declaring ARG TARGETARCH in the stage
  • Expecting TARGETARCH to be aarch64 or x86_64
  • Thinking BUILDPLATFORM and TARGETPLATFORM are the same thing
  • Pinning the final runtime stage to $BUILDPLATFORM
  • Believing --platform on FROM changes the exported image's architecture
  • Assuming the variables need to be passed with --build-arg

context