Multi-Arch Images with Docker Buildx
AI generated
FROM
RUN
Docker · Buildx · Multi-Arch · CI/CD
Multi-Arch Images with Docker Buildx
one build, one manifest list, multiple CPU architectures

Multi-arch images solve the problem of a team developing on Apple Silicon laptops but deploying to amd64 servers, or the reverse case of using arm64 instances in production. Docker Buildx produces images for multiple architectures from a single build command and publishes them under one shared tag as a manifest list.

17 min read Buildx · QEMU · Manifest List · CI/CD amd64 · arm64 · Docker 27+

1. Why multi-arch images are mandatory today

Since Apple fully switched to arm64 with its M series chips, and cloud providers such as AWS offer arm64 servers with Graviton instances at significantly lower prices, a single amd64 image is no longer enough. A multi-arch image bundles variants for multiple CPU architectures under one single tag, so Docker automatically selects the matching variant for the local architecture on pull, without developers or deployment scripts having to manually distinguish between amd64 and arm64.

Without multi-arch images, two unpleasant scenarios arise in practice. Either a development team with M series MacBooks experiences subtle differences on production servers, because the locally tested image ran under emulation while the production image ran natively. Or teams maintain two separate image tags, such as app:1.0-amd64 and app:1.0-arm64, which complicates deployment scripts and creates a source of errors from pulling the wrong tag. Docker Buildx solves exactly this problem by producing both variants in a single build run.

For Magento shops with Docker development environments on Apple Silicon laptops and amd64 production servers, a multi-arch image is no longer a nice to have but a basic requirement for locally tested behavior to match production behavior. Buildx has been integrated as a plugin since Docker 19.03 and became the default builder for docker build as of Docker 23.

2. Buildx basics: builder instances and drivers

Buildx extends the classic docker build engine with BuildKit features and several builder drivers. The default docker driver uses the local Docker daemon and does not support multi-platform builds. For multi-arch images you need the docker-container driver, which spins up a dedicated BuildKit container, or the kubernetes driver for distributed builds across multiple Kubernetes pods.

A builder instance is created once with docker buildx create and reused afterwards by name. Multiple builder instances can exist in parallel, for example a local builder for development and a remote builder for native arm64 compilation. The command docker buildx ls shows all registered builders with their supported platforms, and docker buildx inspect --bootstrap starts a builder and checks its capabilities.


# Create a dedicated buildx builder with the docker-container driver
docker buildx create --name multiarch-builder --driver docker-container --use

# Bootstrap the builder and list supported platforms
docker buildx inspect --bootstrap

# List all registered builders
docker buildx ls

# Build and push a multi-arch image in a single command
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag registry.mironsoft.de/shop-app:1.4.0 \
  --push .

3. QEMU emulation: cross-platform builds without foreign hardware

To build a multi-arch image for arm64 on a single amd64 machine, Buildx uses QEMU as a userspace emulator by default. The kernel transparently forwards foreign instructions to QEMU through binfmt_misc handlers, which translates arm64 instructions into native amd64 instructions line by line. This works reliably but is noticeably slower than native execution, typically by a factor of five to ten for compute heavy build steps such as compiler runs.

On plain Linux hosts, the QEMU handlers usually need to be registered first, which happens through the tonistiigi/binfmt image. Docker Desktop already ships with this registration preconfigured. For simple PHP applications without native compiler steps, QEMU emulation is usually performant enough, because the actual bottleneck is the Composer installation and copying files, not CPU intensive compilation.


# Register QEMU handlers for cross-platform emulation on a plain Linux host
docker run --privileged --rm tonistiigi/binfmt --install all

# Verify that arm64 emulation is now available
docker buildx inspect --bootstrap | grep Platforms

# Build only the arm64 variant for local testing, without pushing
docker buildx build --platform linux/arm64 --tag shop-app:arm64-test --load .

4. Native builders instead of emulation for production pipelines

For projects with native compiler steps, for example compiling PHP extensions from source or building Node modules with native bindings, QEMU emulation quickly becomes a bottleneck in the CI pipeline. The much faster alternative is a real native builder: a second Buildx instance running on an actual arm64 host, for example an AWS Graviton instance or an Apple Silicon runner, attached as a remote builder over SSH or through the Kubernetes backend.

With docker buildx create --append, a second node can be added to an existing builder instance, so a single buildx build call builds the amd64 variant natively on the local host and the arm64 variant natively on the attached remote node. Buildx automatically orchestrates which node handles which part of the multi-arch image, without requiring the developer to adjust the command.


# Append a native arm64 remote node via SSH to the existing builder
docker buildx create --name multiarch-builder --append \
  --node arm64-native \
  --platform linux/arm64 \
  ssh://build-user@arm64-runner.internal

# Bootstrap both nodes and verify the combined platform list
docker buildx inspect --bootstrap

# Now the same build command uses native compilation on both architectures
docker buildx build --platform linux/amd64,linux/arm64 \
  --tag registry.mironsoft.de/shop-app:1.4.0 --push .

5. Understanding manifest lists: one tag, multiple architectures

The technical foundation of multi-arch images is the OCI image index specification, also called a manifest list in Docker terminology. Instead of a single image manifest with a layer list, the index contains multiple manifest entries, each annotated with architecture and operating system. A Docker client pulling registry.mironsoft.de/shop-app:1.4.0 first queries the index and then downloads only the manifest and layers matching its own platform.

The decisive advantage: registries such as Docker Hub, GitLab Container Registry or AWS ECR support this structure natively, so no separate deployment logic is needed. With docker manifest inspect registry.mironsoft.de/shop-app:1.4.0, the full list of included architectures can be viewed without actually downloading the image.


# Inspect the manifest list of a multi-arch image without pulling it
docker manifest inspect registry.mironsoft.de/shop-app:1.4.0

# Output shows one entry per architecture, for example:
#   linux/amd64  -> sha256:aaa...
#   linux/arm64  -> sha256:bbb...

# docker pull automatically selects the matching manifest
docker pull registry.mironsoft.de/shop-app:1.4.0

6. Dockerfile adjustments for multi-arch compatibility

Most Dockerfiles work without changes for multi-arch images, as long as the base images themselves are already multi-arch capable, which is standard today for official images like php, node or debian. Buildx automatically sets the built-in arguments TARGETARCH, TARGETOS and TARGETPLATFORM, which allow architecture specific downloads or binaries to be controlled directly inside the Dockerfile.

Dockerfiles only become problematic when they contain hard coded amd64 assumptions, for example a direct download of an amd64-only binary by URL. The correct multi-arch approach uses ARG TARGETARCH to select the right download URL, instead of hard coding the architecture.


FROM --platform=$BUILDPLATFORM php:8.4-fpm AS base

# Buildx sets these automatically for the target platform
ARG TARGETARCH
ARG TARGETOS

# Download an architecture-specific binary based on TARGETARCH
RUN case "$TARGETARCH" in \
      amd64) BIN_ARCH="x86_64" ;; \
      arm64) BIN_ARCH="aarch64" ;; \
      *) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; \
    esac && \
    curl -sSL "https://example.com/tool-${BIN_ARCH}.tar.gz" | tar -xz -C /usr/local/bin

7. Multi-arch builds in GitLab CI and GitHub Actions

In CI pipelines it pays off to use a dedicated docker-container builder inside a Docker in Docker runner, combined with a cache backend such as the registry itself, to avoid repeated QEMU emulation across multiple pipeline runs. GitHub Actions offers docker/setup-qemu-action and docker/setup-buildx-action as prebuilt steps that handle registration and builder creation before the actual build-push-action runs.

GitLab runners with a Docker executor require manual registration of the QEMU handlers through the tonistiigi/binfmt image as a preceding job step, because GitLab does not provide a comparable ready made action building block. For teams with a high build volume, a separate arm64 runner pays off noticeably faster than permanent QEMU emulation in every pipeline run.


# GitLab CI: multi-arch build with QEMU registration and buildx
build-multiarch:
  stage: build
  image: docker:27
  services: [docker:27-dind]
  before_script:
    - docker run --privileged --rm tonistiigi/binfmt --install all
    - docker buildx create --name ci-builder --driver docker-container --use
    - docker buildx inspect --bootstrap
  script:
    - docker buildx build --platform linux/amd64,linux/arm64 \
        --tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" --push .

8. Multi-arch for PHP extensions and native dependencies

PHP extensions with native C code, for example gd, intl or imagick, are compiled from source inside the Dockerfile through docker-php-ext-install and are therefore automatically architecture independent, because the compiler translates for the respective architecture on each target system. A multi-arch image for PHP therefore rarely requires special handling, as long as no precompiled binary packages fixed to one architecture are pulled in.

Things get more critical with Composer packages that ship native extensions, such as libraries bringing their own .so files, or with Node dependencies featuring native bindings like sharp for image processing. These packages run platform specific post install scripts on every npm install or composer install that must run on the target architecture, which is why installation has to happen inside the respective build stage rather than copying artifacts between architectures.

9. QEMU emulation compared to native builders

The choice between QEMU emulation and native builders for multi-arch images depends on build complexity, available infrastructure and team size. The following table compares both approaches along the most relevant practical criteria.

Criterion QEMU Emulation Native Builder
Setup effort Low, a single Docker command Higher, an additional host or runner is needed
Build speed Five to ten times slower Native speed
Infrastructure cost None additional Second host class required
Suitable for Simple PHP/Node builds without compiler load Compiler heavy builds, high CI frequency
Error proneness Rare emulator bugs on exotic syscalls No emulation related errors

For smaller teams and projects without a high build frequency, QEMU emulation is the pragmatic default path, because no additional host has to be managed. Once build times in the CI pipeline become a noticeable burden, a second native builder node pays for itself within a few weeks through saved waiting time and CI minutes.

Mironsoft

Docker build strategies for Apple Silicon teams and arm64 production

Set up multi-arch images for your team?

We set up Buildx builders for amd64 and arm64, choose between QEMU emulation and native remote builders, and integrate multi-arch builds cleanly into your GitLab or GitHub pipeline.

Buildx setup

Configuring builder instances, QEMU registration and native remote nodes

Dockerfile audit

Finding architecture dependent assumptions and fixing them with TARGETARCH

CI integration

Adding multi-arch builds with caching into your existing pipeline

10. Summary

Multi-arch images with Docker Buildx solve the growing problem of differing CPU architectures between development laptops and production servers, by producing variants for amd64 and arm64 in a single build run and publishing them under one shared tag as a manifest list. QEMU emulation enables cross-platform builds on a single machine, but is noticeably slower than native builders running on actual target architecture hardware and attached to the same builder instance through remote nodes.

Most Dockerfiles need no changes for multi-arch images, as long as the base images themselves are multi-arch capable and contain no hard coded amd64 assumptions in the build. For PHP applications the effort is usually low, because native extensions are compiled per target system anyway. The only remaining critical point is handling precompiled binary packages and native Node modules, whose installation must always happen in the respective target stage.

Multi-Arch Images with Docker Buildx — Key Takeaways

Buildx builder

docker buildx create --driver docker-container is required for multi-platform builds, the default docker driver is not enough.

QEMU vs native

QEMU emulates conveniently on one machine, native remote nodes are five to ten times faster under compiler load.

Manifest list

One tag, multiple architecture manifests. Docker automatically selects the matching one on pull.

TARGETARCH

A build argument automatically set by Buildx, controlling architecture specific downloads inside the Dockerfile.

11. FAQ: Multi-Arch Images with Docker Buildx

1What is a multi-arch image?
Bundles variants for multiple CPU architectures under one tag, Docker automatically selects the matching one on pull.
2Why is the default driver not enough?
The docker driver does not support multi-platform builds, docker-container or kubernetes is needed instead.
3What is QEMU emulation?
Translates foreign CPU instructions and enables cross-platform builds on one machine, with a noticeable speed cost.
4When does a native builder pay off?
For compiler intensive builds. Native builders are five to ten times faster than QEMU emulation.
5What is a manifest list?
An OCI image index with multiple architecture manifests under one tag. Docker downloads only the matching one on pull.
6Do I need to adjust my Dockerfile?
Usually not, as long as the base images are multi-arch capable and contain no hardcoded amd64 downloads.
7How do I register QEMU?
With tonistiigi/binfmt --install all. Docker Desktop already ships with this preconfigured.
8Does this work with GitHub Actions?
Yes, through setup-qemu-action and setup-buildx-action before the actual build-push step.
9What is TARGETARCH?
An automatically set build argument holding the target architecture, controlling architecture specific logic in the Dockerfile.
10Are PHP extensions problematic?
Usually not, because docker-php-ext-install compiles from source. Only precompiled binary packages are problematic.