---
title: "Multi-Architecture Container Images: Buildx, Manifest Lists, and Registry Patterns"
description: "Building container images that run on both x86 and ARM64 — docker buildx workflows, cross-compilation in Dockerfiles, manifest lists, and common mistakes that waste build time."
url: https://agent-zone.ai/knowledge/cicd/container-build-multi-arch/
section: knowledge
date: 2026-02-21
categories: ["cicd"]
tags: ["docker","buildx","multi-arch","arm64","containers","manifest"]
skills: ["multi-arch-builds","container-image-management"]
tools: ["docker","docker-buildx","container-registries"]
levels: ["intermediate"]
word_count: 935
formats:
  json: https://agent-zone.ai/knowledge/cicd/container-build-multi-arch/index.json
  html: https://agent-zone.ai/knowledge/cicd/container-build-multi-arch/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=Multi-Architecture+Container+Images%3A+Buildx%2C+Manifest+Lists%2C+and+Registry+Patterns
---


# Multi-Architecture Container Images

You can no longer assume containers run only on x86. AWS Graviton instances are ARM64. Developer laptops with Apple Silicon are ARM64. Ampere cloud instances are ARM64. A container image tagged `myapp:latest` needs to work on both architectures, or you end up maintaining separate tags and hoping nobody pulls the wrong one.

## Manifest Lists

A manifest list (also called an OCI image index) lets a single tag point to multiple architecture-specific images. When a client pulls `myapp:latest`, the registry returns the image matching the client's architecture.

```bash
# Inspect a manifest list to see what architectures are available
docker manifest inspect alpine:latest
```

The output shows entries for `linux/amd64`, `linux/arm64`, `linux/arm/v7`, and others. When you pull `alpine:latest` on an ARM64 machine, Docker automatically selects the ARM64 variant. Your images should work the same way.

## Setting Up Buildx

Docker buildx is a CLI plugin that extends `docker build` with multi-platform support. Create a builder instance:

```bash
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap
```

The `--bootstrap` flag starts the builder immediately, so you can verify it supports the platforms you need. The default builder usually supports `linux/amd64` and `linux/arm64` out of the box.

## Building Multi-Arch Images

The basic command builds for multiple platforms and pushes to a registry in one step:

```bash
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --push \
  -t ghcr.io/myorg/myapp:latest \
  .
```

The `--push` flag is required for multi-arch builds. Buildx cannot load a multi-platform image into the local Docker daemon because the local image store only holds one architecture at a time. If you omit `--push`, the build completes but the image goes nowhere.

To build for a single platform and load it locally (useful for testing):

```bash
docker buildx build --platform linux/arm64 --load -t myapp:latest .
```

## Build Strategies

There are three approaches to multi-arch builds. They differ dramatically in speed and complexity.

### Strategy 1: Cross-Compilation in Dockerfile (Fastest)

The build runs on your native architecture. The compiler inside the container targets the other architecture. No emulation is involved.

```dockerfile
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
ARG TARGETARCH
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN GOOS=linux GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o /app ./cmd/server

FROM alpine:3.20
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
```

The key is `--platform=$BUILDPLATFORM` on the build stage. This tells buildx to run the build stage on the host architecture (fast, native). The `$TARGETARCH` variable is set automatically by buildx to each target platform (`amd64`, `arm64`). The final stage uses the target platform by default, so the runtime image matches the deployment architecture.

This is the gold standard for Go, Rust, and any language with cross-compilation support.

### Strategy 2: QEMU Emulation (Simplest but Slowest)

Buildx transparently uses QEMU to emulate the target architecture. Your Dockerfile does not need any changes:

```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
CMD ["node", "server.js"]
```

```bash
docker buildx build --platform linux/amd64,linux/arm64 --push -t myapp:latest .
```

The ARM64 build runs under QEMU emulation on your x86 machine (or vice versa). This works but is 5-10x slower than native builds. For interpreted languages (Node.js, Python) this is acceptable because `npm install` and `pip install` are I/O-bound. For compiled languages, especially Go, **QEMU emulation is unreliable** and will crash on `lfstack.push` in Go's runtime.

### Strategy 3: Native Builders (Fastest, Most Complex)

Use ARM64 hardware for the ARM64 build and x86 hardware for the x86 build. Buildx can coordinate remote builders:

```bash
# Add a remote ARM64 builder node
docker buildx create --name multiarch --node arm-builder \
  --platform linux/arm64 \
  ssh://user@arm64-host

# Add a local x86 builder node
docker buildx create --name multiarch --node x86-builder \
  --platform linux/amd64 \
  --append

docker buildx use multiarch
```

Each platform builds on its native hardware. This is the fastest option but requires maintaining builder infrastructure on both architectures.

## Base Image Compatibility

Before building multi-arch, verify your base images support all target platforms. Common base images and their ARM64 support:

| Base Image | ARM64 Support |
|-----------|---------------|
| `alpine` | Yes |
| `debian` | Yes |
| `ubuntu` | Yes |
| `golang` | Yes |
| `node` | Yes |
| `python` | Yes |
| `mattermost/mattermost-team-edition` | No |
| Many vendor-specific images | Check first |

If your base image lacks ARM64 support, the build will fail at pull time for that platform. Check before committing to a multi-arch strategy:

```bash
docker manifest inspect node:20-alpine | jq '.manifests[].platform'
```

## Inspecting Built Images

After pushing, verify the manifest list contains both architectures:

```bash
docker manifest inspect ghcr.io/myorg/myapp:latest
```

If only one architecture appears, the build for the other platform silently failed or was not included.

## CI Integration

In GitHub Actions, the standard pattern combines buildx setup with multi-platform build-and-push:

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-qemu-action@v3
      - 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:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
```

If your Dockerfile uses cross-compilation (Strategy 1), the QEMU action is only needed for the final stage's base image pull. The actual compilation runs natively on x86, keeping build times fast.

## Common Mistakes

1. **Building on emulation when cross-compilation is available.** A Go binary cross-compiled with `GOARCH=arm64` builds in seconds. The same binary built under QEMU emulation takes minutes and may crash.
2. **Forgetting `--push`.** Buildx cannot load multi-arch images into the local daemon. Without `--push`, the build produces nothing usable.
3. **Base image does not support the target architecture.** Always check with `docker manifest inspect` before adding a platform to your build.
4. **Sharing build cache across architectures.** Cached layers from x86 are not valid for ARM64. Use platform-aware cache keys.
5. **Not testing pulls on both architectures.** The manifest list might exist, but the ARM64 image might be broken. Pull and run on actual ARM64 hardware or emulation to verify.

