Use depot/build-push-action to build AMD64 and ARM64 Docker
images in GitHub Actions. Depot sends the linux/amd64 build to a native x86 builder and the linux/arm64 build to
a native Arm builder. Both builds run in parallel, without QEMU emulation, and are published under one image tag.
The workflow only needs to specify both platforms:
- uses: depot/build-push-action@v1
with:
project: <your-depot-project-id>
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/example/my-app:latestDepot automatically gives each architecture a persistent Docker layer cache on fast NVMe storage. There's no QEMU setup step and no registry cache configuration to maintain.
A complete GitHub Actions workflow
This example authenticates to Depot with GitHub Actions OIDC, builds both architectures, and pushes the result to GitHub Container Registry:
name: Build multi-platform image
on:
push:
branches: [main]
jobs:
build:
runs-on: depot-ubuntu-24.04
permissions:
contents: read
id-token: write
packages: write
steps:
- uses: actions/checkout@v4
- uses: depot/setup-action@v1
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push AMD64 and ARM64 image
uses: depot/build-push-action@v1
with:
project: <your-depot-project-id>
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}Before running the workflow, create a Depot project and add a GitHub OIDC trust relationship for the repository. The
id-token: write permission lets GitHub issue a short-lived identity token to Depot, so the workflow doesn't need a
long-lived Depot API token.
The example uses a GitHub Actions runner managed by Depot as well, but the container build is remote. You can run
depot/build-push-action from a GitHub-hosted runner if you prefer. The AMD64 and ARM64 image builds still run on
Depot's native builders.
Build the same image locally
You can run the same multi-platform build from your laptop with depot build:
depot build \
--project <your-depot-project-id> \
--platform linux/amd64,linux/arm64 \
.The build still runs on Depot's native x86 and Arm builders. It doesn't consume your laptop's CPU and memory or use QEMU on an x86 machine.
Use the same Depot project ID locally and in depot/build-push-action. The project's AMD64 and ARM64 layer caches are
shared across every build. A developer can build an image locally, push the change, and let the GitHub Actions build
reuse those layers. CI can warm the same cache before another developer runs depot build.
There's no cache archive to copy between the laptop and GitHub Actions. Depot attaches the project's persistent NVMe cache to each remote builder automatically. See local development with Depot for CLI setup and loading a built image into the local Docker daemon.
Why native builders matter
A typical GitHub Actions job runs on an AMD64 machine. Building an ARM64 image on that machine usually means installing
QEMU and emulating Arm instructions during RUN steps in the Dockerfile.
That works, but emulation can turn compilation and dependency installation into the slowest part of the workflow. It also adds another moving part when architecture-specific commands behave differently under emulation.
The difference can be massive. Depot's PostHog multi-platform benchmark builds linux/amd64 and
linux/arm64 images with both depot/build-push-action and docker/build-push-action on every commit. The Docker job
uses QEMU for ARM64. Depot runs each build on its native architecture. Across the latest 10 commits, Depot is currently
32.9x faster.
Depot removes that layer. It starts one native builder for each requested architecture:
- The AMD64 image builds on an x86 machine.
- The ARM64 image builds on an Arm machine.
- Both builds execute at the same time.
- BuildKit combines the results into one multi-platform image.
Each builder has 16 CPUs and 32 GB of memory by default. Larger builders are available when the Dockerfile can use more parallelism.
One tag, two platform images
A multi-platform image isn't one filesystem that somehow runs on both processors. The registry stores an AMD64 image and an ARM64 image, then points one tag at a manifest list, also called an OCI image index.
When someone runs this command:
docker pull ghcr.io/example/my-app:latestDocker reads the manifest list and pulls the variant that matches the machine. An x86 server receives the
linux/amd64 image. An AWS Graviton instance or other Arm server receives the linux/arm64 image.
You can inspect the published platforms with:
docker buildx imagetools inspect ghcr.io/example/my-app:latestThe output should include manifests for both linux/amd64 and linux/arm64.
Push the multi-platform result
Use push: true when the goal is one image tag that supports both architectures. The registry is where the manifest
list and both platform images live together.
Loading is different. A normal Docker daemon expects a local tag to resolve to an image for the current machine, and standard Buildx multi-platform loads commonly fail with:
docker exporter does not currently support exporting manifest listsDepot has an enhanced --load implementation that can select and load the image matching the local machine. That's
useful for integration tests, but it doesn't put both platform variants into a registry under one tag. Push the image
when downstream AMD64 and ARM64 machines both need to pull it.
If the workflow only needs to test the image locally, build and load the runner's architecture instead:
- uses: depot/build-push-action@v1
with:
project: <your-depot-project-id>
context: .
platforms: linux/amd64
load: true
tags: my-app:testKeep both architecture caches warm
Depot persists the Docker layer cache for the project automatically. Future AMD64 builds reuse AMD64 layers, and future ARM64 builds reuse ARM64 layers. The cache is available without downloading and uploading a cache archive at the start and end of every GitHub Actions job.
Dockerfile structure still decides how useful that cache is. Put stable dependency inputs before frequently changing source code:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run buildA source change can then reuse the dependency layer on both architectures instead of running npm ci from scratch.
Check the Dockerfile for architecture assumptions
The workflow can request both platforms, but every image and binary in the Dockerfile must support them.
- Use base images that publish both AMD64 and ARM64 variants.
- Don't download an AMD64-only binary into the ARM64 image.
- Use BuildKit's
TARGETARCHargument when an install URL or build command differs by architecture. - Run integration tests against each architecture when native dependencies or compiled code are involved.
For example:
ARG TARGETARCH
RUN curl -fsSL "https://example.com/tool-linux-${TARGETARCH}" -o /usr/local/bin/tool \
&& chmod +x /usr/local/bin/toolWith the workflow configured this way, one push produces a native AMD64 image, a native ARM64 image, and the manifest list that lets Docker choose between them automatically.