Module 5: Docker in CI/CD

7. Multi-Platform Builds

Overview

So far, all your images get built for a single architecture: linux/amd64 — the one GitHub Actions runners and most cloud servers use. It works. But there are two increasingly common scenarios where you need support for linux/arm64:

  1. Apple Silicon (M1/M2/M3/M4): Your team uses Macs with an Apple Silicon chip. When they download your amd64 image, Docker runs it with Rosetta emulation — slower and with possible incompatibilities.

  2. AWS Graviton / Azure Arm: ARM instances in the cloud are ~20% cheaper and ~30% more energy-efficient. If you deploy on Graviton, you need native arm64 images.

Multi-platform builds build the same image for multiple architectures in a single build. The registry stores both variants under the same tag, and Docker automatically downloads the right one depending on the machine doing the pull.

This capsule is short by design. Multi-platform is a capability, not an obligation. If you deploy only on amd64 (most cases), this capsule is informational. If you need arm64, here's the complete configuration.


When you need multi-platform

You need multi-platform if:

  • 📋 Your team has Macs with Apple Silicon (M1/M2/M3/M4) and you want native images for local development
  • 📋 You deploy on AWS Graviton (t4g, c7g, m7g instances)
  • 📋 You deploy on Azure ARM-based VMs
  • 📋 You distribute public images and you want universal support
  • 📋 Your CI/CD target includes both x86 and ARM

You do NOT need multi-platform if:

  • 📋 You deploy only on x86 servers (most current cloud infrastructure)
  • 📋 Your team uses Linux/Windows x86 for development
  • 📋 Your team uses Macs with Apple Silicon but Docker Desktop with Rosetta works fine
  • 📋 The cost of the extra build time doesn't justify the benefit

The honest decision

Does your whole team deploy on x86?
  → You don't need multi-platform now
  → You can add it later when you need it

Does your team use Apple Silicon for development?
  → Multi-platform improves their experience
  → But it isn't blocking — Rosetta works

Do you deploy on ARM (Graviton, etc.)?
  → You need multi-platform — it's mandatory

How multi-platform works

The concept: manifest lists

When you do docker pull myapp:v1.0.0, Docker doesn't download an image — it downloads a manifest that lists the available variants:

{
  "manifests": [
    {
      "platform": { "architecture": "amd64", "os": "linux" },
      "digest": "sha256:abc123..."
    },
    {
      "platform": { "architecture": "arm64", "os": "linux" },
      "digest": "sha256:def456..."
    }
  ]
}

Docker detects your machine's architecture and downloads the right variant:

Mac M2 (arm64):   docker pull myapp:v1.0.0 → downloads sha256:def456 (arm64)
Linux x86 server: docker pull myapp:v1.0.0 → downloads sha256:abc123 (amd64)

The same tag, the same command, the right image automatically.

QEMU: architecture emulation

GitHub Actions runners are linux/amd64. To build arm64 images on an amd64 runner, you need QEMU — an emulator that lets you run instructions from a different architecture.

docker/setup-qemu-action installs QEMU on the runner:

- name: Set up QEMU
  uses: docker/setup-qemu-action@v3

With QEMU installed, Buildx can build images for linux/arm64 by running the Dockerfile's commands in an emulated environment.


Configuration in GitHub Actions

The basic multi-platform workflow

# .github/workflows/docker.yml
name: Docker Multi-Platform Build

on:
  push:
    branches: [main]

jobs:
  docker:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push (multi-platform)
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

The three key steps

# 1. QEMU — the architecture emulator
- uses: docker/setup-qemu-action@v3
# It installs QEMU to emulate arm64 on the amd64 runner

# 2. Buildx — the advanced builder
- uses: docker/setup-buildx-action@v3
# It configures Docker Buildx with multi-platform support

# 3. Build with platforms
- uses: docker/build-push-action@v6
  with:
    platforms: linux/amd64,linux/arm64
# It builds the image for both architectures

Output in the logs

Run docker/build-push-action@v6
  Building for platforms: linux/amd64, linux/arm64

  #1 [linux/amd64] FROM python:3.12-slim@sha256:...
  #1 DONE 1.2s

  #2 [linux/arm64] FROM python:3.12-slim@sha256:...
  #2 DONE 1.5s

  #3 [linux/amd64 2/5] WORKDIR /app
  #3 DONE 0.1s

  #4 [linux/arm64 2/5] WORKDIR /app
  #4 DONE 0.1s

  ...

  #15 exporting to image
  #15 pushing manifest for ghcr.io/user/repo:abc123
  #15 DONE 3.2s

Buildx builds both variants and pushes them as a manifest list under the same tag.


The impact on build time

Before vs after adding arm64

Scenarioamd64 onlyamd64 + arm64Increase
A simple build (few deps)45s90s+100%
A medium build (fastapi, langchain)120s240s+100%
A heavy build (torch)300s600s++100%+

The build time roughly doubles because each architecture gets built sequentially (QEMU emulates arm64, which is slower than native). With a cache, the increase is smaller in subsequent builds.

Optimization: native builds with a matrix

If the build time is unacceptable, you can use native runners for each architecture:

jobs:
  build:
    strategy:
      matrix:
        include:
          - platform: linux/amd64
            runner: ubuntu-latest
          - platform: linux/arm64
            runner: ubuntu-24.04-arm

    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: ${{ matrix.platform }}
          push: true
          tags: ghcr.io/${{ github.repository }}
          cache-from: type=gha,scope=${{ matrix.platform }}
          cache-to: type=gha,mode=max,scope=${{ matrix.platform }}

This uses a native ARM runner for the arm64 build — no emulation, maximum speed. It requires GitHub Actions to have ARM runners available (increasingly common).


Dockerfile compatibility

Most Dockerfiles work without changes

If your Dockerfile uses a base image that supports multi-platform (like python:3.12-slim, which has amd64 and arm64 variants), you don't need to change anything:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

Docker automatically selects the right variant of python:3.12-slim for each architecture.

When the Dockerfile needs changes

If you install system packages with binaries compiled specifically for one architecture:

# ❌ This can fail on arm64 if the .deb is only for amd64
RUN wget https://example.com/tool_amd64.deb && dpkg -i tool_amd64.deb

# ✅ Use the build variable to select the right binary
ARG TARGETARCH
RUN wget https://example.com/tool_${TARGETARCH}.deb && dpkg -i tool_${TARGETARCH}.deb

TARGETARCH is a variable Buildx injects automatically — it's worth amd64 or arm64 depending on the platform being built.

The available platform variables

ARG TARGETPLATFORM    # linux/amd64 or linux/arm64
ARG TARGETOS          # linux
ARG TARGETARCH        # amd64 or arm64
ARG TARGETVARIANT     # v7 (for arm/v7) or empty

ARG BUILDPLATFORM     # The builder's platform (always linux/amd64 on GH Actions)
ARG BUILDOS           # linux
ARG BUILDARCH         # amd64

Verifying the multi-platform image

With docker manifest inspect

docker manifest inspect ghcr.io/user/ai-api:v1.0.0

The output:

{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:abc123...",
      "size": 1234,
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:def456...",
      "size": 1234,
      "platform": {
        "architecture": "arm64",
        "os": "linux"
      }
    }
  ]
}

If you see both architectures in the list, the image is multi-platform.

In the workflow

- name: Verify multi-platform
  run: |
    docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${{ github.sha }}

Comparisons

Single-platform vs Multi-platform

AspectSingle (amd64 only)Multi (amd64 + arm64)
Build timeFast~2x slower
ComplexityMinimalIt requires QEMU + extra config
Compatibilityx86 onlyx86 + native ARM
Apple SiliconEmulation (Rosetta)Native
AWS Graviton❌ Not compatible✅ Native
When to use itDeploying only on x86Deploying on ARM, or a team with Apple Silicon

QEMU (emulated) vs native runners

AspectQEMU on ubuntu-latestA native ARM runner
Setupdocker/setup-qemu-actionA runner with the ubuntu-24.04-arm label
arm64 speedSlow (emulated, ~2-5x slower)Fast (native)
CostNo extra cost (it uses the same runner)An additional runner
AvailabilityAlways availableIt depends on the GitHub plan
When to use itOccasional builds, small imagesFrequent builds, heavy images

Troubleshooting

1. "exec format error" when running the image locally

Symptom:

exec /usr/local/bin/python: exec format error

Cause: You downloaded the arm64 variant on an amd64 machine (or vice versa).

Solution:

# Force the right platform
docker pull --platform linux/amd64 ghcr.io/user/ai-api:v1.0.0
docker run --platform linux/amd64 ghcr.io/user/ai-api:v1.0.0

2. The arm64 build fails with a "segmentation fault"

Symptom: The arm64 build fails with a segfault during pip install or apt-get install.

Cause: QEMU can have bugs with certain compiled packages. This is especially common with packages that include complex native code (numpy, torch).

Solution:

# Option 1: Use cross-compilation hints
- name: Set up QEMU
  uses: docker/setup-qemu-action@v3
  with:
    platforms: arm64

# Option 2: Install only precompiled packages (wheels)
# In requirements.txt, specify versions with arm64 wheels available

If the problem persists with heavy packages like torch, consider native builds with ARM runners.

3. "no matching manifest for linux/arm64" in the base image

Symptom:

ERROR: no matching manifest for linux/arm64/v8 in the manifest list entries

Cause: The base image has no arm64 variant. Not every image supports multiple architectures.

Solution: Verify that your base image supports arm64:

docker manifest inspect python:3.12-slim | grep architecture
# It should show "amd64" and "arm64"

Official images of Python, Node, Ubuntu, and Alpine support multi-platform. Custom or less common images may not.


Exercises

Exercise 1: Add multi-platform to an existing build

You have this single-platform workflow. Add support for linux/amd64 and linux/arm64:

name: Docker
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          tags: myapp:test
See solution
name: Docker
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build multi-platform
        uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          platforms: linux/amd64,linux/arm64
          tags: myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

The changes:

  • docker/setup-qemu-action@v3 enables arm64 emulation
  • platforms: linux/amd64,linux/arm64 builds for both architectures
  • timeout-minutes: 30 increased because multi-platform takes longer
  • push: false because we're only validating the build (we don't log in to the registry)
  • The cache with type=gha speeds up subsequent builds

Exercise 2: A Dockerfile with TARGETARCH

Create a Dockerfile that downloads a different binary depending on the architecture. The binary gets downloaded from https://releases.example.com/tool-<arch> where <arch> is amd64 or arm64.

See solution
FROM python:3.12-slim

ARG TARGETARCH

WORKDIR /app

RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

RUN curl -fsSL "https://releases.example.com/tool-${TARGETARCH}" \
    -o /usr/local/bin/tool && \
    chmod +x /usr/local/bin/tool

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ ./src/

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

Key points:

  • ARG TARGETARCH automatically receives amd64 or arm64 from Buildx
  • You don't need to declare a default — Buildx injects it
  • ${TARGETARCH} expands to the right value for each platform
  • On the amd64 build: it downloads tool-amd64
  • On the arm64 build: it downloads tool-arm64

Exercise 3: Multi-platform build + push + verify

Create a workflow that builds for amd64 + arm64, pushes to GHCR, and verifies that both platforms are in the manifest.

See solution
name: Docker Multi-Platform
on:
  push:
    branches: [main]

jobs:
  docker:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Verify platforms
        run: |
          echo "=== Manifest inspection ==="
          docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${{ github.sha }}
          
          echo ""
          echo "=== Checking platforms ==="
          PLATFORMS=$(docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${{ github.sha }} --raw | python3 -c "
          import json, sys
          data = json.load(sys.stdin)
          platforms = [f\"{m['platform']['os']}/{m['platform']['architecture']}\" for m in data.get('manifests', [])]
          print(', '.join(platforms))
          ")
          echo "Available platforms: $PLATFORMS"
          
          if echo "$PLATFORMS" | grep -q "linux/amd64" && echo "$PLATFORMS" | grep -q "linux/arm64"; then
            echo "Both platforms present"
          else
            echo "ERROR: Missing platform!"
            exit 1
          fi

Key points:

  • docker buildx imagetools inspect shows the registry's manifest list
  • The Python script parses the JSON to extract the platforms
  • The verification confirms that both variants exist in the manifest
  • If a platform is missing, the step fails

Exercise 4: An informed decision — do you need multi-platform?

For each scenario, say whether you need multi-platform and why:

  1. A team of 3 developers, all with a Mac M2, deploying on an AWS EC2 t3.medium (x86)
  2. A team of 5 developers, all with Linux x86, deploying on an AWS Graviton t4g.medium (ARM)
  3. A public image of a CLI tool that any developer can use
  4. A personal prototype only you use, a Mac M1, deploying on Render (x86)
See solution

1. A Mac M2 + a deploy on EC2 x86:

  • 📋 Recommended but not mandatory. The deploy is x86 → amd64 is enough for production. But the team with M2s benefits from native arm64 images for local development (no Rosetta emulation). If the builds are fast (< 3 min), it's worth it. If they're heavy (torch, cuda), consider amd64 only and have the team use Rosetta.

2. Linux x86 + a deploy on Graviton ARM:

  • 📋 Mandatory. Deploying on Graviton requires arm64 images. Without multi-platform, your image doesn't run in production. The team develops on x86, so you need both platforms: amd64 for local development and arm64 for production.

3. A public CLI image:

  • 📋 Highly recommended. You don't know what architecture your users have. Apple Silicon is ~50% of the developer laptop market. Linux ARM (Raspberry Pi, ARM servers) is growing. For maximum compatibility, include both platforms.

4. A personal prototype, Mac M1, deploying on Render x86:

  • 📋 Not necessary. You're a single user, the deploy is x86. Build amd64 only. If Docker Desktop with Rosetta works fine on your Mac for development, don't invest time in multi-platform for a prototype.

Summary

  • Multi-platform builds the same image for multiple architectures (amd64, arm64)
  • QEMU emulates arm64 on amd64 runners — it works but it's ~2x slower
  • The setup: docker/setup-qemu-action + platforms: linux/amd64,linux/arm64 in the build
  • TARGETARCH in the Dockerfile lets you download architecture-specific binaries
  • The build time roughly doubles — evaluate whether the benefit justifies it
  • Mandatory if you deploy on ARM (Graviton, Azure ARM)
  • Recommended if you distribute public images or your team uses Apple Silicon
  • Not necessary if you deploy only on x86 and local emulation works fine
  • It's a capability, not an obligation — add it when you need it

Additional resources

  1. Docker multi-platform builds — Docker's official guide
  2. docker/setup-qemu-action — The action for installing QEMU
  3. Buildx automatic platform ARGs — The available platform variables
  4. AWS Graviton — Getting Started — AWS's ARM instances
  5. GitHub Actions ARM runners — Native ARM runners
  6. Docker manifest inspect — How to verify platforms in a manifest