Buildx: building multi-arch images

Author: Carmelo C.
Published: Jan 29, 2024 (updated)
docker x86_64 x64 amd64 armv6 armv7 arm64 aarch64 containers

In this post I’ll explore how to build images that can run on multiple architectures. My scenario is composed of the two following hosts:

Docker version being run is 25.0.1.

On x86_64

hello.go:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world!\n")
}

Dockerfile:

FROM golang:alpine3.19 AS builder
COPY hello.go src/
RUN go build -o hello src/hello.go

FROM alpine:3.19
RUN mkdir /app
WORKDIR /app
COPY --from=builder /go/hello ./
CMD ["./hello"]

NOTE: you ought to replace <repo> with your own repository on Docker Hub. In case you’d like to try some ready-to-use images, my repository is carmelo0x63.

user@x86_64 $ docker build -t <repo>/hellogo:1.0 -f Dockerfile.hello .

user@x86_64 $ docker push <repo>/hellogo:1.0

user@x86_64 $ docker run --rm <repo>/hellogo:1.0
Hello, world!

Running the same image on ARM leads to…

user@arm $ docker run --rm <repo>/hellogo:1.0
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm/v7) and no specific platform was requested
exec ./hello: exec format error

Unsurprisingly, because:

user@arm $ docker image inspect --format "{{.Architecture}}" <repo>/hellogo:1.0
amd64

Enter Buildx

user@x86_64 $ docker buildx version
github.com/docker/buildx v0.12.1 30feaa1

user@x86_64 $ docker buildx create --driver docker-container --bootstrap --use --name mybuilder
[+] Building 18.9s (1/1) FINISHED
 => [internal] booting buildkit                                18.9s
 => => pulling image moby/buildkit:buildx-stable-1             17.7s
 => => creating container buildx_buildkit_mybuilder0            1.2s
mybuilder

user@x86_64 $ docker buildx ls
NAME/NODE    DRIVER/ENDPOINT        STATUS  BUILDKIT     PLATFORMS
mybuilder *  docker-container
  mybuilder0 unix:///var/run/doc... running v0.12.4      linux/amd64, linux/amd64/v2, linux/386
default      docker
  default    default                running v0.12.4+...  linux/amd64, linux/amd64/v2, linux/386

running with --load generates an error

user@x86_64 $ docker buildx build -t <repo>/hellogo:2.0 --platform linux/amd64,linux/386,linux/arm/v7,linux/arm/v6 --load -f Dockerfile.hello .
[+] Building 0.0s (0/0)
ERROR: docker exporter does not currently support exporting manifest lists

NOTE: Buildx issue #59

--push works instead

user@x86_64 $ docker buildx build -t <repo>/hellogo:2.0 --platform linux/amd64,linux/386,linux/arm/v7,linux/arm/v6 --push -f Dockerfile.hello .
[+] Building 289.6s (51/51) FINISHED
...

NOTE: this step takes a bit of time. Docker must run QEMU to build the images for the other architectures. More info on Multi-platform images.

The drawback is that we need to pull the image back from Docker Hub.

user@x86_64 $ docker pull <repo>/hellogo:2.0
2.0: Pulling from <repo>/hellogo
8a49fdb3b6a5: Already exists
280c8b627803: Pull complete
4f4fb700ef54: Pull complete
03bc6fb78b91: Pull complete
Digest: sha256:b642d8947aa2ecce374b15a4ac767b7cb4d5f2fe81709702afb1091875e23696
Status: Downloaded newer image for <repo>/hellogo:2.0
docker.io/<repo>/hellogo:2.0

It’s interesting to notice how the image embeds several different flavours

user@x86_64 $ docker buildx imagetools inspect <repo>/hellogo:2.0
Name:      docker.io/<repo>/hellogo:2.0
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:b642d8947aa2ecce374b15a4ac767b7cb4d5f2fe81709702afb1091875e23696

Manifests:
  Name:        docker.io/<repo>/hellogo:2.0@sha256:09198d4c23055f38c447a7e4b511777c42de5761d56007d52ca407492a17365d
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/amd64

  Name:        docker.io/<repo>/hellogo:2.0@sha256:e4a13ef6f4c7d9b2336e5d4de57005b41705770475eae519632021741bba6c3c
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/386

  Name:        docker.io/<repo>/hellogo:2.0@sha256:56a3397cb049baaaf2e8523fe7c1b3c4df2c900aab24a135bbc4c8f1bad6792e
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm/v7

  Name:        docker.io/<repo>/hellogo:2.0@sha256:3ae0b2142f8a43209cdd1d577a309141e4b854c70839d8612621b16ae4ab5e89
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm/v6
...

Of course:

user@x86_64 $ docker run --rm <repo>/hellogo:2.0
Hello, world!

on ARM after Buildx

We’re good to go now. Same image we’d run on AMD/Intel/x86_64 runs seamlessly on ARM.

user@arm $ docker pull <repo>/hellogo:2.0
2.0: Pulling from <repo>/hellogo
e14425cf8fb9: Already exists
4d43510b4c85: Pull complete
4f4fb700ef54: Pull complete
1117a7594d95: Pull complete
Digest: sha256:b642d8947aa2ecce374b15a4ac767b7cb4d5f2fe81709702afb1091875e23696
Status: Downloaded newer image for <repo>/hellogo:2.0
docker.io/<repo>/hellogo:2.0

Of course, the appropriate image get downloaded for the CPU architecture:

user@arm $ docker image inspect --format "{{.Architecture}}" <repo>/hellogo:2.0
arm

Finally:

user@arm $ docker run <repo>/hellogo:2.0
Hello, world!

Also, notice how the image IDs are different on the two architectures:

user@x86_64 $ docker image ls <repo>/hellogo:2.0
REPOSITORY       TAG       IMAGE ID       CREATED         SIZE
<repo>/hellogo   2.0       390a9611350b   6 minutes ago   9.17MB

… and…

user@arm $ docker image ls <repo>/hellogo:2.0
REPOSITORY       TAG       IMAGE ID       CREATED         SIZE
<repo>/hellogo   2.0       2d8c851a5cfb   4 minutes ago   6.59MB