I finally got multi-platform / multi-arch Docker builds to work!
In principle, if you want to provide a single Docker image for both amd64 and arm64, all you need to do is run docker buildx build --platform linux/amd64,linux/arm64 ….
However, in order for this to actually work, there are several hoops to jump through.
The following how-to is mostly reconstructed from short-term memory and shell history, so you may want to double-check what I'm writing with the documentation.
Docker Storage Format
First, if you're using Docker version 28 or earlier, you need to change the storage format to one that supports multi-platform containers.
In my case, merging the following content into /etc/docker/daemon.json was sufficient:
{
"features": {
"containerd-snapshotter": true
}
}
Not Recommended: binfmt / qemu
I first tried multi-platform builds by installing qemu-system-aarch64 and binfmt-support – in this case, Docker will use (slow!) software emulation for any selected, non-native platform.
However, I never got that to work – the non-native part would always fail at random places, and given its slow progress I was not very keen on debugging it.
Instead, I opted for a setup with two different, native build hosts: one for amd64 and one for arm64. In my case, I have an amd64 VM as the main build host, and an aarch64 / armv8 SoC as build host for arm64. I want to run all commands on the amd64 VM, and the arm64 host should be fully remote-controlled.
Adding Contexts and Builders
In order to make the amd64 VM aware of the arm64 build host, we need two aspects: a context and a builder.
docker context create raspi4 --docker 'host=ssh://user@host'
docker buildx create --use --name arm64_raspi4 --platform linux/arm64 raspi4
(adjust user and host according to your setup)
(Note: the second line may not be required if all you want / need is a single multi-platform builder – see below)
At this point, we can build either for amd64 or arm64, but not yet both at the same time.
If we tried to run docker buildx build --platform linux/amd64,linux/arm64 … now, it would fall back to software emulation via qemu for one of the two platforms, depending on which of the two builders (default / arm64_raspi4) is currently selected.
Note: In principle, you can also adjust the arm64 host's systemd docker invocation to include -H tcp://0.0.0.0:port, and then use --docker 'host=tcp://ip:port' when creating the context.
However, that will give anyone on the local network docker (and, thus, root) access to the arm64 host.
An SSH connection is a much better choice.
Adding a Multi-Platform Builder
Luckily, Docker has a concept of builders that consist of multiple endppoints:
docker buildx create --use --name multiarch default
docker buildx create --append --name multiarch raspi4
As docker buildx ls shows, we now have a builder that supports two sets of platforms:
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
multiarch* docker-container
\_ multiarch0 \_ unix:///var/run/docker.sock running v0.29.0 linux/amd64, linux/amd64/v2, linux/386
\_ multiarch1 \_ raspi4 running v0.29.0 linux/arm64, linux/arm/v7, linux/arm/v6
And, as the asterisk indicates, it has been selected as builder for all subsequent docker commands. At this point, building works as intended. In my case, I'm using the following commandline to also tag and push the multi-platform image to docker hub:
docker buildx build --push --platform linux/amd64,linux/arm64 --tag derfnull/db-fakedisplay:${VERSION} --tag derfnull/db-fakedisplay:latest --build-arg=dbf_version=${VERSION} .