CertGrid CertGrid
Hands-on Lab·Docker

Docker Buildx and Multi-Platform Images

One tag serving amd64, arm64 and arm/v7. Three things block this on a fresh host and all three are shown failing first: the default driver cannot emit multi-platform, RUN on a foreign architecture needs emulation, and the builder container has its own localhost.

Dockerfiles and Builds Guide 23 of 46 Advanced

Tested on the versions above. Digests, layer IDs and timings vary per build. Available platforms depend on what emulation is registered on your host. Match the shape of the output, not the exact strings.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. See which builder you are using

    Every docker build runs on a builder. A fresh install has one, named default, using the docker driver. Note what it reports: a single platform group. That default builder is the reason the next step fails.

    bash Example session
    docker buildx lsNAME/NODE     DRIVER/ENDPOINT   STATUS    BUILDKIT   PLATFORMSdefault*      docker \_ default    \_ default       running   v0.32.2    linux/amd64 (+3)

    Expected resultOne builder using the docker driver, marked with an asterisk as current.

    Success conditionYou can see the driver column. docker is the limited one; docker-container is the capable one.

  2. Watch it fail on a foreign architecture

    This is worth running rather than reading about, because the error is specific and you will meet it. The amd64 half succeeds. The arm64 half pulls its base image happily - images for other architectures download fine - and then dies the moment it has to execute something. That single failure actually hides two independent requirements, and conflating them is why this topic frustrates people. First, the docker driver cannot output a multi-platform image at all - it has one image store holding one architecture. Second, any RUN on a foreign architecture needs binary emulation registered with the kernel, normally QEMU via binfmt_misc. A Dockerfile with no RUN needs only the first; one with RUN needs both. Installing emulation (docker run --privileged --rm tonistiigi/binfmt --install all) is a privileged, host-wide change and was deliberately NOT run for this guide - the steps below avoid emulation entirely by using a Dockerfile with no RUN.

    bash Example session
    docker buildx build --platform linux/amd64,linux/arm64 -t cg-bx:multi .#7 [linux/amd64 2/2] RUN uname -m > /arch.txt#7 DONE 0.2s#8 [linux/arm64 2/2] RUN uname -m > /arch.txt#8 0.106 exec /bin/sh: exec format errorERROR: failed to solve: process "/bin/sh -c uname -m > /arch.txt" did not complete successfully: exit code: 255

    Expected resultamd64 completes, arm64 fails with exec format error at the first RUN.

    Success conditionYou reproduced the error. It means exactly what it says: the kernel cannot execute an arm64 binary, because no emulation is registered for it.

  3. Create a builder that can emit multiple platforms

    docker-container runs BuildKit in its own container, which is what lets it assemble several architectures into one image index. --driver-opt network=host matters here and is explained in the step after next.

    bash Example session
    docker buildx create --name cg-builder --driver docker-container --driver-opt network=host --bootstrap#1 creating container buildx_buildkit_cg-builder0 0.4s donecg-builder

    Expected resultA bootstrap block, then the builder name.

    Success conditiondocker buildx ls now lists cg-builder with the docker-container driver. It is a real container - docker ps shows it.

  4. The warning everyone hits

    Build with the new builder and the platforms you want. It succeeds - and then tells you the result went nowhere. The docker-container driver builds outside your local image store, so unless you say where the result should go, it stays in the build cache. This warning is the single most common confusion with buildx.

    bash Example session
    docker buildx --builder cg-builder build --platform linux/amd64,linux/arm64,linux/arm/v7 -t cg-bx:multi .#12 [linux/arm64 2/2] COPY hello.txt /hello.txt#12 DONE 0.0sWARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load

    Expected resultA successful build for three architectures, followed by the no-output warning.

    Success conditionAll three platforms build. docker images will NOT show the result - that is the warning telling you so, not a failure.

  5. Why the builder cannot see your registry

    Adding --push to reach a registry on localhost fails on a default builder, and the message explains itself once you know the builder is a container: its localhost is its own network namespace, not the host's. Creating the builder with network=host - as this guide did - is what makes localhost:5000 resolve to the registry you started.

    bash Example session
    docker buildx --builder cg-builder build --platform linux/amd64,linux/arm64 -t localhost:5000/cg-bx:multi --push .ERROR: failed to push localhost:5000/cg-bx:multi: dial tcp [::1]:5000: connect: connection refused# the builder container has its own loopback - the registry is on the HOST's

    Expected resultA connection refused against ::1, not a permissions or auth error.

    Success conditionYou can explain the error. The alternatives are host networking, or addressing the registry by a name the builder container can resolve.

  6. Push all three architectures under one tag

    With a host-networked builder and a running registry, --push sends every architecture and then a manifest that ties them together. A client pulling this tag gets the variant matching its own platform, automatically.

    bash Example session
    docker buildx --builder cg-builder build --platform linux/amd64,linux/arm64,linux/arm/v7 -t localhost:5000/cg-bx:multi --push .#13 pushing layers 0.2s done#13 pushing manifest for localhost:5000/cg-bx:multi@sha256:d6758ef0f8b16769db2797cc0ce89efeef0d1d719c36af27acf24ce165a8e6b0#13 DONE 0.9s

    Expected resultLayers pushed, then a manifest push with a digest.

    Success conditionA manifest digest is returned. That digest identifies the whole multi-architecture set, not one image.

  7. Inspect what you published

    imagetools inspect reads the manifest list back. The media type is an OCI image index - a list of manifests, one per platform. The unknown/unknown entries are not errors: they are build attestations (provenance and SBOM metadata) that BuildKit attaches by default, each annotated with the manifest it describes.

    bash Example session
    docker buildx imagetools inspect localhost:5000/cg-bx:multiMediaType: application/vnd.oci.image.index.v1+jsonDigest:    sha256:d6758ef0f8b16769db2797cc0ce89efeef0d1d719c36af27acf24ce165a8e6b0Manifests:  Platform:    linux/amd64  Platform:    linux/arm64  Platform:    linux/arm/v7  Platform:    unknown/unknown   # attestation-manifest, not a broken entry

    Expected resultAn image index listing the three platforms plus attestation manifests. Output is abridged.

    Success conditionThree real platforms appear. If only one does, the build ran on the default builder rather than the container one.

  8. Clean up

    The builder is a container and holds its own build cache, so removing it reclaims real disk. Remove the registry too, and the anonymous volume it created.

    bash
    # removes the builder and everything cached inside itdocker buildx rm cg-builderdocker rm -f cg-registrydocker rmi moby/buildkit:buildx-stable-1 registry:3# check docker volume ls for the registry's anonymous volume

    Expected resultThe builder and container removed.

    Success conditiondocker buildx ls no longer lists cg-builder and docker ps -a is clear of cg-registry.

Troubleshooting

Official sources