Moving Docker workloads to ARM64 hosting: multi-arch images with buildx
Moving Docker workloads to ARM64 hosting is mostly a question of images. The
container runtime, Compose and Kubernetes all work the same on ARM; what matters
is whether each image you run has an arm64 build. This guide shows how to
check, how to migrate to ARM by building multi-arch images of your own with
buildx, and what to do about the images that still have no ARM version.
VPS GB runs only ARM, and Docker on our ARM VPS is ordinary Docker.
How Docker picks an architecture
An image tag such as postgres:17 usually points not at one image but at a
manifest list (an OCI image index): a small document listing one image per
platform, such as linux/amd64, linux/arm64 and others. When you
docker pull, the client picks the entry matching the machine it runs on. That
is why the same compose.yaml works unchanged on an x86 laptop and an ARM
server, as long as every image in it has both entries.
If an image only has linux/amd64, pulling it on ARM either fails or warns that
the platform does not match, and running it ends in exec format error.
Check what you run
For each image, look at what platforms its tag offers:
docker manifest inspect nginx:stable | grep -A2 '"platform"'
docker buildx imagetools inspect nginx:stable
The second form is easier to read: it lists each platform on its own line. You
want to see linux/arm64 (Docker sometimes shows it as linux/arm64/v8, which
is the same thing).
To check a whole Compose project, list its images with
docker compose config --images and inspect each one. The official images on
Docker Hub are multi-architecture almost without exception, and so are most
well-maintained open source projects. The ones to look at closely are images
from smaller vendors, old pinned tags from before a project added ARM, and
images your own team built.
Build your own images for both architectures
docker buildx builds one image for several platforms and pushes them under a
single tag, as a manifest list:
docker buildx create --name multi --use
docker buildx build --platform linux/amd64,linux/arm64 \
-t registry.example.com/myapp:1.4.0 --push .
On an x86 machine the arm64 half is built under QEMU emulation. Docker Desktop
has that set up already; on a Linux build host, register the emulators once with
docker run --privileged --rm tonistiigi/binfmt --install arm64.
Most Dockerfiles need no changes at all, because the base image is itself multi-arch. Two patterns cover the rest:
Downloading a binary. A line that fetches tool-linux-amd64.tar.gz will
fetch the wrong thing on ARM. buildx sets TARGETARCH for each platform, to
amd64 or arm64:
ARG TARGETARCH
RUN curl -fsSLO "https://example.com/tool-linux-${TARGETARCH}.tar.gz"
Compiling. Emulated compiling is slow, often several times slower than native. For languages that cross-compile, run the compiler on the build machine's own architecture and only target the other one:
FROM --platform=$BUILDPLATFORM golang:1.23 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app ./cmd/app
FROM gcr.io/distroless/static
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
The same idea works for Rust with a cross linker; see porting your application to ARM64.
CI that builds both
Build both architectures on every release, not just when somebody remembers.
With GitHub Actions that is docker/setup-qemu-action,
docker/setup-buildx-action, then docker/build-push-action with
platforms: linux/amd64,linux/arm64. GitLab CI, Azure Pipelines and the rest
run the same docker buildx build command.
If emulated builds get too slow, there are two fixes. Cross-compile as above, or
give buildx a native ARM64 builder: an ARM machine added as a remote node
(docker buildx create --name multi --append ssh://user@arm-host) builds the arm64 half at
full speed while your x86 runner builds the other.
Images with no ARM build
You have a few options, best first:
- Upgrade the tag. Many projects added ARM64 in a later release; an old pinned version may be the only thing missing.
- Build it yourself. If the project is open source, its Dockerfile will
usually build for
linux/arm64with no changes. - Find a maintained alternative image for the same software.
- Ask the vendor. For commercial images, ARM64 is often on a roadmap.
Running an amd64 image under emulation on an ARM host is possible with
--platform linux/amd64 and the binfmt emulators installed. It is fine for a
one-off tool and not something to run a production service on.
Kubernetes on ARM
Every node advertises its architecture in the kubernetes.io/arch label. In a
cluster that mixes x86 and ARM, multi-arch images make placement a non-issue,
and for the odd single-architecture image a nodeSelector on
kubernetes.io/arch: amd64 keeps it on a node that can run it.
Once every image in your stack has an arm64 entry, moving it is a
docker compose up on the new server. If the stack includes a database, read
moving databases to ARM before you copy any
volumes.
