How to build multi-architecture Docker images (amd64/arm64) with GitHub Actions

Docker & Containers · By · Published on · Updated on · 8 min read

Three-dimensional structure of floating cubes in blue and pink tones on a gradient background
Abstract illustration symbolizing modular architecture and containers across different platforms Photo: Steve A Johnson / Pexels

This tutorial walks you through building multi-architecture Docker images (amd64 and arm64) and publishing manifests with GitHub Actions. You’ll learn how OCI manifests work, how to structure a multi-platform Dockerfile, set up Buildx and QEMU in CI, use BuildKit cache, and verify each architecture pulls the correct image.

Multi-architecture and OCI manifests: what you publish

A multi-architecture image is a set of platform-specific images grouped under a single tag. The registry stores an OCI index, also known as a manifest list.

When you pull an image, the client selects the correct variant based on the platform. That way a single tag, like latest, can point to both amd64 and arm64 variants without extra steps for consumers.

With Buildx, publishing to multiple platforms in a single push automatically creates the manifest. You can also create or update manifests explicitly when needed.

Environment prerequisites and registry access

Before starting, ensure you have access to the registry where you’ll publish and a repository with a ready Dockerfile. The GitHub Actions runner can be the default Linux runner.

The workflow will install QEMU and Buildx to enable cross builds. Store your registry credentials as repository secrets for authentication in the pipeline.

Structure the Dockerfile for multi-platform (TARGETARCH/TARGETOS)

BuildKit injects context variables like TARGETOS, TARGETARCH and BUILDPLATFORM. Use these variables to compile binaries for the target architecture and to choose appropriate base images in build stages.

The example below compiles a static Go binary per architecture and produces a minimal final image. Adapt the commands to your project and language. If your build uses native libraries, check cross-compile compatibility.

# Exemplo Dockerfile multiplataforma (Go)
# Estágio de build roda na plataforma do builder
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src

# Dependências de módulos
COPY go.mod go.sum ./
RUN go mod download

# Código da aplicação
COPY . .

# Compila binário para a arquitetura alvo
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
    go build -o /out/app ./cmd/app

# Imagem final mínima
FROM alpine:3.20
WORKDIR /app
COPY --from=build /out/app /usr/local/bin/app

# Labels OCI úteis para rastreabilidade
LABEL org.opencontainers.image.source="https://example.com/repo" \
      org.opencontainers.image.description="App multi-arch amd64/arm64"

USER 10001
ENTRYPOINT ["app"]

GitHub Actions workflow: Buildx, QEMU, login and multi-arch push

The workflow installs QEMU and Buildx, authenticates with the registry, and uses the build-push-action to build and publish amd64 and arm64 variants in a single step. BuildKit cache reduces build time between runs.

Adjust tags according to your tagging strategy. The metadata action helps generate tags by branch, release tag and SHA automatically. For GHCR, GITHUB_TOKEN typically works for publishing repository packages.

name: build-and-push-multiarch

on:
  push:
    branches: ["main"]
    tags: ["v*", "v*.*.*"]
  pull_request:

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout
        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: Docker meta (tags e labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=ref,event=tag
            type=sha
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
          labels: |
            org.opencontainers.image.source=${{ github.repositoryUrl }}
            org.opencontainers.image.revision=${{ github.sha }}

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

      # Exemplo opcional: login também no Docker Hub
      # - name: Login Docker Hub
      #   uses: docker/login-action@v3
      #   with:
      #     username: ${{ secrets.DOCKERHUB_USERNAME }}
      #     password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build & Push multi-arch
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Tag and label strategy to avoid confusion

Define clear conventions for each repository event. Use latest only for the primary stable line. For releases, publish immutable tags (semver) and, if appropriate, abbreviated tags for ranges.

Include OCI labels for traceability of origin and commit. Avoid overwriting old version tags. Keep the same list of platforms across all stable tags.

  • Main branches: latest and commit SHA.
  • Releases: vX.Y.Z, vX.Y, vX and latest (optional if it represents the current stable).
  • Pull requests: ephemeral tags (for example, pr-<number>) without promoting latest.
  • Include org.opencontainers.image.source and revision labels for auditing.
Event Recommended tags
Push to main latest, sha-<SHORT>
Tag v1.2.3 v1.2.3, v1.2, v1, latest (optional)
Pull request pr-<number>, sha-<SHORT> (no latest)

Publish and inspect the generated manifest

When you build for multiple platforms with Buildx and push, the manifest is created automatically. Inspect the result to verify that amd64 and arm64 are listed under the same tag.

Use the inspection tool to check platforms, digests and layers. This helps diagnose pulls that fail because a specific variant is missing.

# Inspecione a manifest list publicada
# Ajuste a referência para seu registro e tag
docker buildx imagetools inspect ghcr.io/<owner>/<repo>:<tag>

# Opcional: inspecione um digest específico
# docker buildx imagetools inspect ghcr.io/<owner>/<repo>@sha256:<digest>

How to verify on each architecture (amd64/arm64)

Validate execution by forcing the platform on the client. Each command should print the kernel architecture inside the container. This confirms the correct image was downloaded from the same tag.

If your binary depends on native libraries, run a quick functionality test on each platform to detect runtime issues early.

# Força execução como amd64
docker run --rm --platform linux/amd64 ghcr.io/<owner>/<repo>:<tag> uname -m

# Força execução como arm64
docker run --rm --platform linux/arm64 ghcr.io/<owner>/<repo>:<tag> uname -m

Common errors, diagnostics and cache optimizations

Some issues appear in multi-platform builds. Below is a quick diagnostics and resolution guide. To reduce build time, use BuildKit remote cache and base layers on stable files.

For advanced CI cache techniques, see the dedicated guide: Using BuildKit Remote Cache for Docker CI.

  • no matching manifest: the tag does not include the desired platform.
  • exec format error: binary for the wrong architecture was copied into the final image.
  • QEMU registration failure: the runner didn’t load binfmt; ensure setup-qemu is in the workflow.
  • Cross-compile with native libraries: adjust toolchains and flags for each TARGETARCH.
  • Inefficient cache: reorganize layers and enable BuildKit cache-to/cache-from.
Symptom Likely cause How to fix
no matching manifest for arm64 Tag published only with amd64 Ensure platforms: linux/amd64,linux/arm64 in the build and republish
exec format error at runtime amd64 binary copied into an arm64 image Use TARGETOS/TARGETARCH during build and COPY the correct artifact
Slow builds on PRs Cache not shared or invalid Enable GHA cache and fix COPY steps to maximize reuse
Toolchain errors during cross build CPU-specific dependencies Use static builds or proper multi-platform toolchains

Frequently asked questions

Do I need arm64 runners to build arm64 images?

In most cases, no. Docker Buildx with QEMU lets you build arm64 variants from an amd64 runner. Native builds can be faster and may be required for projects with specific system dependencies, but cross-builds work well for many applications.

Does Buildx create the manifest or do I need to do it manually?

If you use the build-push-action with multiple platforms and push enabled, the manifest is created automatically for the given tag. Manual creation is only needed to compose manifests from existing images or for specific retagging workflows.

How do I choose between Docker Hub and GHCR to publish?

Both work. Consider permissions, repository integration and access control. You can publish to both by authenticating to both registries and using two tag references in the same build. Adjust secrets and permissions according to your team policy.

How can I reduce the final multi-arch image size?

Use multistage builds to keep only the necessary artifact, choose minimal base images, and remove build tools from the final stage. Prefer static binaries when possible and order COPY/RUN to maximize cache reuse. The same Dockerfile can serve amd64 and arm64 without duplication.

Can I have different tags per architecture?

You can, but it's uncommon and increases complexity. The standard approach is a single tag that points to a manifest referencing platform variants. Use architecture-specific tags only if functional differences require explicit separation.

Is QEMU sufficient for projects with native dependencies (e.g., C/C++)?

It depends on the project. QEMU covers many cases, but toolchains and native libraries may need extra setup or native builds. Consider static compilation when possible and maintain per-architecture tests to validate the resulting binaries.

Ready to publish multi-arch images with confidence?

If you want a review of your Dockerfile and multi-platform release workflow in GitHub Actions, send a message with the repository and your tagging goals.

Review my workflow

See also