You must publish one container image tag that runs on both linux/amd64 and linux/arm64. How do you build and publish it with docker buildx, and what are the trade-offs between emulation and native or cross-compiled builds?
answer
- one tag -> image index -> per-platform manifests
- --push (or --output); --load cannot hold multi-platform
- QEMU binfmt = easy but slow; native nodes = fast; cross-compile = fastest
- FROM --platform=$BUILDPLATFORM + ARG TARGETARCH re-declared in stage
- base image must be multi-arch; TARGETARCH is amd64/arm64, not x86_64/aarch64
basics
~20 sUse docker buildx build --platform linux/amd64,linux/arm64 --push, which produces one manifest list (image index) referencing a per-architecture image. The foreign architecture can run under QEMU emulation (simple but slow), on native builder nodes (fast, needs machines of both architectures), or via cross-compilation using BUILDPLATFORM and TARGETARCH (fastest, needs a cross-compiling toolchain).
solid answer
~50 s`docker buildx build --platform linux/amd64,linux/arm64 -t repo/app:1.2.3 --push .` builds both and pushes an **image index** (manifest list): one tag, several per-platform manifests, and the client picks the matching one at pull time. `--load` cannot be used for multi-platform because the classic local image store holds a single platform, so you push, or export with `--output`. Three execution strategies: 1. **QEMU emulation** (`binfmt_misc`, installed via `tonistiigi/binfmt`). One machine builds everything. Simple, but CPU-heavy steps such as compiling can be several times slower, and some toolchains misbehave under emulation. 2. **Native nodes**: `docker buildx create --append` to attach an arm64 builder. Each platform builds on its own hardware, so full speed, at the cost of running a builder fleet. 3. **Cross-compilation**: run the build stage on `--platform=$BUILDPLATFORM` and target `$TARGETOS/$TARGETARCH`. Native speed on one machine, ideal for Go and Rust; harder when native extensions or tests must run on target. I also make sure base images are multi-arch, since a single-platform base breaks the whole build.
code
dockerfile · 12 lines# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.23 AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]go deeper
Know that one tag can hold several architectures through a manifest list and that buildx --platform with --push produces it.
Explain the index versus manifest distinction, why --load fails, and the QEMU emulation setup.
Compare emulation, native nodes and cross-compilation on speed and risk, handle multi-arch bases, TARGETARCH-driven downloads, per-platform cache and testing on target.
Decide the fleet strategy: which architectures to support at all, the builder estate and its cost, cross-compilation standards per language, and the release gate that proves each architecture works.
## What a multi-platform image is A registry tag can point at an **image index** (also called a manifest list) rather than a single image manifest. The index lists entries, each with a platform descriptor (os, architecture, optionally variant such as v8) and a digest of that platform's manifest. When a client pulls the tag, it reads the index, selects the entry matching its own platform, and pulls only those layers. So users on Apple Silicon and users on x86 servers use the same tag and get different bytes. buildx is the CLI that orchestrates producing all of those and assembling the index. ``` docker buildx build --platform linux/amd64,linux/arm64 -t repo/app:1.2.3 --push . ``` The result must go somewhere that can hold an index. A registry can (--push); the classic local image store cannot represent multiple platforms under one tag, so --load fails for a multi-platform build. You can also export with --output type=oci,dest=out.tar. Inspect the result with docker buildx imagetools inspect repo/app:1.2.3. ## Three ways to execute the foreign architecture **Emulation with QEMU.** The kernel's binfmt_misc mechanism routes execution of foreign-architecture binaries to a QEMU user-mode emulator. Registering the handlers is usually docker run --privileged --rm tonistiigi/binfmt --install all, which Docker Desktop does for you. Then a single amd64 machine can run arm64 RUN steps. It is by far the easiest to set up and entirely adequate for image assembly work: installing packages, copying files. It is poor for CPU-bound work: compiling a large codebase under emulation can be three to ten times slower, and occasionally a toolchain or a JIT crashes or hangs under emulation, producing failures that reproduce nowhere else. **Native builder nodes.** buildx supports a builder made of multiple nodes with different platforms: docker buildx create --name multi --platform linux/amd64 ... then --append a node with --platform linux/arm64. Each platform's work is dispatched to hardware that runs it natively. This is the fastest general answer and the one that scales, but it means operating builders on both architectures, which most cloud CI now supports with arm64 runners. **Cross-compilation.** For languages with good cross-compilation, pin the build stage to the builder's own architecture and pass the target through: ``` FROM --platform=$BUILDPLATFORM golang:1.23 AS build ARG TARGETOS TARGETARCH RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app . FROM alpine:3.20 COPY --from=build /out/app /app ``` The compile runs natively on amd64 while emitting an arm64 binary; only the tiny final stage is platform-specific. Note the args must be re-declared inside the stage to be usable there. This is the best of both worlds for Go and Rust; it becomes harder with cgo, native extensions, or when the build runs tests that must execute on the target architecture. ## Practical pitfalls **Single-platform base images.** If your FROM resolves to an image with no arm64 entry, the arm64 build fails outright. Check with docker buildx imagetools inspect. This is also the reason digest pinning must target the index digest rather than one platform's manifest, otherwise you have silently made the Dockerfile single-arch. **Downloading architecture-specific binaries.** A RUN that curls a release asset with a hard-coded x86_64 name breaks on arm64. Use $TARGETARCH to select, remembering the naming mismatch: TARGETARCH is amd64/arm64 while many projects publish x86_64/aarch64. **Caching.** Each platform has its own cache lineage; a cache exported from an amd64 build does not accelerate arm64. Plan cache scope per platform. **Testing.** Building for a platform does not prove it works there. Under emulation a smoke test is possible but slow and not fully representative; the credible answer for production is running the test suite on native hardware of each architecture. **Variants.** arm64 has variants (v8 and newer) and arm32 has v6/v7; being sloppy about variant strings leads to images that pull on a Raspberry Pi but crash. Prefer explicit platform strings. ## Choosing If builds are quick or mostly package installation, emulation is fine and costs nothing to adopt. If compilation dominates and your language cross-compiles cleanly, cross-compile. If neither holds, or you need to run tests on target, invest in native builder nodes. Many pipelines mix them: cross-compile the binary, then assemble and smoke test per platform.
- Why does 'docker buildx build --platform linux/amd64,linux/arm64 --load' fail?The classic local image store keys a tag to a single image manifest and cannot represent a multi-platform index, so there is nothing to load. Push to a registry instead, or export with --output type=oci. If you only need one platform locally, build with a single --platform value and --load, or enable the containerd image store, which can hold multi-platform images locally.
- An arm64 build fails on a RUN step that downloads a release binary, while amd64 succeeds. How do you fix it?The command almost certainly hard-codes an x86_64 asset name. Re-declare ARG TARGETARCH inside the stage and select the asset from it, mapping names where the project uses different labels, since TARGETARCH is amd64/arm64 while many releases publish x86_64/aarch64. Fail loudly on an unknown architecture rather than silently downloading the wrong asset.
- How do you gain confidence the arm64 image actually works, not just that it built?Building for a platform only proves the instructions ran, often under emulation. Run the test suite on native arm64 hardware, for example an arm64 CI runner or a node in the target cluster, and at minimum a startup and health-check smoke test per platform before promoting the tag. Emulated tests are slow and can mask or invent failures, so they are a weak substitute.
The image index is a shelf of the same book in several languages under one catalogue number: readers ask for the title and are handed the edition they can read.
saying these in an interview costs you the question
- Believing --platform on docker run can make an amd64-only image run natively on arm64
- Expecting --load to work for a multi-platform build
- Assuming every base image is multi-arch and not checking the index
- Hard-coding x86_64 asset names in RUN steps
- Treating a successful emulated build as proof the image works on that architecture