Multi-Arch Docker Images with Buildx: One Build for amd64 and arm64

Khimananda Oli 9 min read DevOps
Multi-Arch Docker Images with Buildx: One Build for amd64 and arm64

By Khimananda Oli | Last reviewed: September 2026

Shipping software that runs natively on both Intel servers and ARM-based cloud instances requires more than just tagging two separate images. Multi-Arch Docker Images with Buildx: One Build for amd64 and arm64 is now the standard approach for teams deploying to heterogeneous infrastructure like AWS Graviton, Azure Cobalt, or Apple Silicon development environments. Instead of maintaining parallel pipelines or suffering through slow QEMU emulation for every build, modern Buildx workflows leverage native builders and intelligent caching to produce manifest lists efficiently. This guide covers the exact configuration I use in production to keep build times low and compatibility high.

How do you configure Multi-Arch Docker Images with Buildx for amd64 and arm64?

The foundation of any cross-platform container strategy is the Buildx builder instance. The default Docker builder cannot produce multi-platform manifests; it only builds for your host's native architecture. You must explicitly create and bootstrap a new builder context that supports multiple platforms. Before diving into complex CI configurations, verify your local environment supports virtualization, as this is required for the fallback emulation layer when native hardware isn't present. If you are setting up a fresh Ubuntu workstation for this work, follow the steps in install Docker on Ubuntu to ensure the container runtime and kernel modules are correctly configured first.

Creating and inspecting the builder

Run the following command to create a persistent builder named multiarch. The --driver docker-container flag is critical because the default docker driver does not support multi-platform builds or advanced cache exports.

'# Create and activate the multi-arch builder
docker buildx create --name multiarch --driver docker-container --use

'# Bootstrap the builder (pulls the buildkit image)
docker buildx inspect --bootstrap

'# Verify supported platforms
docker buildx ls

When you inspect the builder, look for linux/amd64 and linux/arm64 in the Platforms column. If linux/arm64 is missing on an x86 host, Docker will automatically use QEMU user-mode emulation. While functional, emulation is painfully slow—often 5x to 10x slower than native execution. For local development, this is acceptable for testing; for CI, it is a bottleneck you must eliminate.

Docker CLIbuildx commandBuildKit ContainerSolver / SchedulerQEMU EmulatorNative ExecutorsRegistryOCI Manifest ListCache BackendGHA / S3 / Local
Buildx orchestrates multi-arch builds through a containerized BuildKit instance that manages platform-specific executors and cache backends.

The universal build command

Once the builder is active, the actual build command is straightforward. The key flags are --platform to specify targets and --push to export directly to the registry. Note that you cannot load multi-platform images into the local Docker daemon with --load; the local daemon only supports single-architecture images. You must push to a registry or export to OCI layout.

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag myregistry.io/app:v1.2.0 \
  --push \
  .

Why are my cross-platform Docker builds so slow?

If your arm64 builds on an x86 machine take twenty minutes instead of two, you are hitting the QEMU wall. QEMU translates ARM instructions to x86 at runtime, which introduces massive overhead. In my experience helping teams optimize their build pipeline automation best practices, eliminating QEMU from the critical path is usually the single biggest time saver.

Native builders vs. emulation

The solution is to run builds on native hardware whenever possible. There are three primary strategies:

  • Cloud-native runners: Use AWS CodeBuild with Graviton, GitHub Actions with ubuntu-24.04-arm runners, or GitLab CI with ARM tags. Each runner builds its native platform, and a final step merges the manifests.
  • Remote Buildx nodes: Append a remote ARM builder to your local Buildx instance using SSH. This lets you trigger a unified build command while the heavy lifting happens on native silicon elsewhere.
  • Hybrid matrix builds: Split the build into separate jobs per architecture, push intermediate artifacts, and use docker buildx imagetools create to assemble the manifest list without rebuilding.

For most teams in 2026, GitHub Actions' native ARM runners offer the best balance of cost and simplicity. They eliminate emulation entirely without requiring you to manage separate infrastructure.

Optimizing Dockerfiles for cross-compilation

Even with native hardware, poorly structured Dockerfiles waste time. Multi-stage builds are non-negotiable. More importantly, leverage BuildKit's cache mounts to persist package manager caches and build artifacts across runs. This prevents re-downloading dependencies for each platform.

FROM golang:1.23-alpine AS builder
ARG TARGETARCH
WORKDIR /app

'# Cache Go modules across builds
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

'# Build for the target architecture natively
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /server .

FROM alpine:3.20
COPY --from=builder /server /usr/local/bin/server
CMD ["/server"]

The TARGETARCH argument is automatically injected by Buildx. Using cache mounts here means your second build (for the other platform) benefits from shared module downloads, even if the compilation itself is platform-specific.

Sequential QEMU (Slow)Build amd64Emulate arm64PushTotal: ~25 minCPU-bound translation overheadParallel Native Builders (Fast)x86 RunnerBuild amd64 (2m)ARM RunnerBuild arm64 (3m)Merge Manifestsimagetools createPush FinalTotal: ~4 minTrue parallelism, no emulation tax
Parallel native builds reduce total pipeline time by over 80% compared to sequential QEMU emulation on a single host.

How do you integrate Buildx into CI/CD pipelines?

Local builds are fine for verification, but production images should always originate from CI. The integration pattern depends on your platform, but the principles remain consistent: authenticate securely, maximize cache reuse, and validate before promotion. When designing these pipelines, align them with your broader CI/CD best practices for small teams and solo developers to avoid over-engineering early on.

GitHub Actions with native ARM

GitHub now offers native ARM64 runners. Use a matrix strategy to build each platform in parallel, then merge. This avoids QEMU entirely and leverages GitHub's built-in cache API.

jobs:
  build:
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        include:
          - runner: ubuntu-24.04
            platform: linux/amd64
          - runner: ubuntu-24.04-arm
            platform: linux/arm64
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          platforms: ${{ matrix.platform }}
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}-${{ matrix.platform }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  merge:
    needs: build
    runs-on: ubuntu-24.04
    steps:
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - run: |
          docker buildx imagetools create \
            -t ghcr.io/${{ github.repository }}:${{ github.sha }} \
            ghcr.io/${{ github.repository }}:${{ github.sha }}-linux/amd64 \
            ghcr.io/${{ github.repository }}:${{ github.sha }}-linux/arm64

Caching strategies that actually work

Multi-arch builds double your cache surface area. Misconfigured caching can actually slow things down due to upload/download overhead. Use inline metadata for small projects and GHA/S3 backends for larger ones. Always set mode=max to cache all layers, not just the final one. For monorepos, scope cache keys by service path to prevent cache thrashing between unrelated components.

What are common pitfalls when building multi-platform containers?

Even with correct tooling, subtle issues creep in. These are the failures I see most often in audit trails and incident postmortems.

PitfallSymptomFix
Hardcoded architecture in Dockerfileexec format error on ARM despite successful buildUse TARGETARCH arg; never hardcode amd64 in RUN commands
Missing base image variantBuild fails for arm64 with "manifest unknown"Verify base image supports all target platforms via docker manifest inspect
Incompatible native binariesPrecompiled tools fail at runtime on secondary archDownload binaries conditionally using TARGETARCH or compile from source
Cache poisoningWrong architecture layers served from cacheInclude platform in cache key; use scoped GHA cache scopes
Local --load with multi-platformError: "cannot load multi-platform image"Use --push or --output type=oci; local daemon is single-arch only

Debugging platform-specific failures

When a build passes on amd64 but fails on arm64, don't guess. Run an interactive debug session targeting the failing platform. Buildx supports --progress=plain for full output visibility, and you can attach a shell to the build container for inspection.

'# Debug arm64 build interactively
docker buildx build --platform linux/arm64 --progress=plain --target builder -t debug-arm .

'# Inspect a specific platform's manifest
docker manifest inspect myregistry.io/app:v1.2.0 | jq '.manifests[] | select(.platform.architecture=="arm64")'

Always validate the final manifest list before promoting to production. A missing platform in the manifest means silent failures for users on that architecture. Automated validation gates in your pipeline catch this before customers do.

Start: Need Multi-Arch?Is build time > 10 min?NoYesUse QEMUAcceptable for dev/testBudget for native ARM?NoYesOptimized QEMU + CacheMax cache mounts, minimal layersNative ARM RunnersGHA / CodeBuild / Self-hostedValidate: docker manifest inspect + runtime test on both archsNever promote unverified multi-arch manifests to production
Decision framework for selecting the appropriate multi-arch build strategy based on time budgets and infrastructure investment.

Production Checklist for Multi-Arch Docker Images with Buildx

Getting Multi-Arch Docker Images with Buildx: One Build for amd64 and arm64 working locally is a milestone, but production readiness demands rigor. Before merging your next multi-arch PR, verify these items:

  1. Manifest completeness: Run docker manifest inspect against your tagged image. Confirm both amd64 and arm64 entries exist with correct digests.
  2. Runtime validation: Pull and run the image on actual ARM hardware (or a cloud ARM VM). Emulation tests pass syntax checks but miss runtime segfaults.
  3. Base image parity: Ensure your base image tag resolves to the same version across architectures. Pin digests, not floating tags, for reproducible builds.
  4. Cache hygiene: Audit your cache configuration. Stale caches cause intermittent failures that vanish on retry. Rotate cache keys monthly or on base image updates.
  5. SBOM generation: Generate Software Bill of Materials per platform. Supply chain security requires knowing exactly what shipped in each architecture variant.

Multi-architecture support is no longer optional for teams serving diverse cloud environments. The tooling has matured significantly, but success still depends on understanding the underlying mechanics rather than copying snippets blindly. Start with native runners where budget allows, fall back to optimized QEMU with aggressive caching where it doesn't, and always validate the final artifact on real hardware. If your team needs help architecting a compliant, efficient container pipeline, reach out to discuss your infrastructure.

Frequently Asked Questions

Run docker buildx create --use to initialize a builder instance. Ensure your Docker Engine is version 24.0 or newer and containerd image store is enabled for native multi-platform support in 2026.

Use docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push . This single invocation compiles binaries for both architectures and pushes the manifest list directly to your registry without local storage.

Cross-compilation relies on QEMU emulation when building on x86 hardware. Emulation overhead causes tenfold slowdowns. Use native ARM runners in CI or dedicated ARM build nodes to eliminate emulation latency entirely.

Yes. GitHub provides free ubuntu-24.04-arm hosted runners as of 2026. Configure matrix strategies to run native builds per architecture, then merge manifests using docker/build-push-action with provenance attestation enabled.

Run docker manifest inspect myregistry.com/myapp:latest to view the manifest list. Confirm both amd64 and arm64 entries exist with correct digests before deploying to production clusters.

No. Cache layers are architecture-specific because compiled binaries differ. Configure separate cache backends per platform using --cache-from and --cache-to flags to prevent cache poisoning between amd64 and arm64 builds.

Official images like alpine:3.20, debian:bookworm-slim, and ubuntu:24.04 include multi-arch manifests. Always pin specific tags rather than latest to ensure reproducible cross-platform builds in 2026 environments.

Use TARGETARCH build arguments injected automatically by Buildx. Conditionally install packages or copy binaries based on this variable instead of hardcoding paths, ensuring identical Dockerfiles work across platforms.

Yes. Register emulators via docker run --privileged --rm tonistiigi/binfmt --install all before creating builders. Modern Linux kernels include binfmt_misc, but explicit registration ensures QEMU handlers are active for foreign architectures.

Scanners must analyze each platform variant separately since CVEs can be architecture-dependent. Configure Trivy or Grype to scan all manifest entries, not just the host architecture, for complete coverage.

Not reliably. Local daemon stores only single-platform images. Use --output type=oci,dest=image.tar to export OCI layouts containing all platforms, then load into containerd or push later.

Missing or outdated QEMU binaries. Re-register binfmt handlers and restart the Buildx builder. Verify emulator status with cat /proc/sys/fs/binfmt_misc/qemu-aarch64 to confirm proper configuration.

Parallelize platform builds using matrix jobs, then merge manifests post-build. Enable BuildKit inline caching and use registry-based cache backends to share layers across workflow runs efficiently.

Yes. ARM binaries are typically smaller due to instruction set density. Expect five to fifteen percent size reduction on arm64 variants compared to equivalent amd64 builds for compiled applications.

Yes. Pass --sbom=true to generate SPDX or CycloneDX attestations per platform. These embed into the manifest index, enabling supply chain verification for each architecture variant independently in 2026.