Can GitHub Actions Build Multi Architecture Images?
Yes. GitHub Actions builds multi architecture images natively across x64 and ARM64 runners, or in one job on a builder profile with both enabled.
Last verified:
Yes. GitHub Actions builds multi architecture images two ways: one job per architecture on a runner of that architecture, joined afterwards by a manifest list, or a single job that hands the build to a remote Docker builder profile with both architectures enabled. Either route ends at the same line in the workflow file, platforms: linux/amd64,linux/arm64, and the difference between them is where the arm64 layers are actually assembled (Docker builders documentation, checked on 2026-08-13).
Answer
The capability was never the hard part. A docker buildx build --platform linux/amd64,linux/arm64 runs on stock GitHub Actions today. What decides whether it finishes in minutes or hours is which machine executes the aarch64 half.
Route one: a builder profile with both architectures. The workflow stays on one runner and the build goes to a dedicated builder virtual machine that keeps a persistent layer cache between runs.
name: image
on:
push:
branches: [main]
jobs:
build:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push both architectures
uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/acme/api:${{ github.sha }}
profile-name: "multi-arch-builder"Warpbuilds/build-push-action is a drop in replacement for docker/build-push-action, so the with: block keeps the same keys and gains profile-name. The api-key input is only needed when the job runs somewhere other than a WarpBuild runner, and docker/setup-buildx-action can come out of the workflow entirely when it was there only to create a builder.
Route two: native runners plus a manifest list. Each architecture builds on hardware that matches it, pushes by digest, and a small final job stitches the digests into one tag. No emulation is involved at any point.
name: image-native
on:
push:
branches: [main]
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: warp-ubuntu-latest-x64-8x
suffix: amd64
- platform: linux/arm64
runner: warp-ubuntu-latest-arm64-8x
suffix: arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
outputs: type=image,name=ghcr.io/acme/api,push-by-digest=true,name-canonical=true,push=true
- name: Save the digest
run: |
mkdir -p /tmp/digests
echo "${{ steps.build.outputs.digest }}" > /tmp/digests/${{ matrix.suffix }}
- uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.suffix }}
path: /tmp/digests/*
manifest:
needs: build
runs-on: warp-ubuntu-latest-arm64-2x
steps:
- uses: actions/download-artifact@v4
with:
pattern: digest-*
path: /tmp/digests
merge-multiple: true
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create the manifest list
run: |
docker buildx imagetools create \
-t ghcr.io/acme/api:${{ github.sha }} \
$(printf 'ghcr.io/acme/api@%s ' $(cat /tmp/digests/*))A docker pull ghcr.io/acme/api:<sha> against that tag resolves to the variant matching the client architecture, which is the whole point of the manifest list. The multi platform Docker build guide walks through this split job by job.
Pricing on WarpBuild is purely usage based.
Detail
The builder profile has to carry both architectures
This is the configuration step teams skip, because nothing in the workflow file hints at it. The profile is created in the WarpBuild UI and the architecture selection lives there, on the profile, rather than in YAML. Setting platforms: linux/amd64,linux/arm64 on a profile that was created amd64 only does not add the second architecture at run time.
The symptom is exec format error during the arm64 stage of the build (Docker builders documentation). The message reads like a Dockerfile problem, usually pointing at the first RUN in the arm64 leg, and it means the same thing in both routes: a binary compiled for one architecture was handed to a machine that cannot execute it. On a builder profile, the fix is enabling both architectures on the profile. On a single runner with docker/setup-qemu-action, the same error means the binfmt handlers were never registered in that job.
Two more profile level facts change how the build behaves once it works. The builder cache has a TTL of 10 days, so a profile that goes unused for longer than that resets and the next build starts cold. And the recommended minimum for concurrent work is roughly 8 vCPU and 16 GB of memory per build job on the profile, which is the number to size against when several pull requests build at once.
Each architecture is its own builder instance, and its own session
Remote Docker builders bill per session, measured from when the builder action starts until the job completes. Multiple concurrent jobs on the same profile share one session, billed from the first job's start to the last job's completion.
Multi architecture breaks that sharing along the architecture line. Each architecture runs on a separate builder instance, so a multi architecture build opens one session per architecture and the two sessions bill independently until their post action steps finish (Docker builders documentation).
The sizing consequence follows from the same split. Every size below comes from the Docker builders documentation, checked on 2026-08-13.
| Builder size | Disk | Per minute | Architectures |
|---|---|---|---|
| 16 vCPU, 32 GB | 100GB | $0.06 | amd64, arm64, multi |
| 32 vCPU, 64 GB | 200GB | $0.12 | amd64, arm64, multi |
| 64 vCPU, 128 GB | 200GB | $0.24 | amd64, arm64, multi |
| 96 vCPU, 192 GB | 600GB | $0.36 | amd64 only |
| 96 vCPU, 192 GB | 2TB | $0.52 | amd64 only |
| 192 vCPU, 384 GB | 600GB | $0.72 | amd64 only |
| 192 vCPU, 384 GB | 2TB | $0.88 | amd64 only |
The bottom four rows are amd64 only, so arm64 and multi architecture profiles top out at 64 vCPU, 128 GB, 200GB disk. A team that wants a 96 vCPU or 192 vCPU builder has to give up the second architecture on that profile and run arm64 somewhere else. At the 8 vCPU per job guideline, the 64 vCPU ceiling still absorbs about eight concurrent build jobs per architecture. Full sizes and rates are on the remote Docker builder catalog and the pricing page.
What the two routes cost
Take a repository that builds one multi architecture image per merge, 400 builds a month, 6 minutes of build time each.
Builder profile route. The 16 vCPU profile at $0.06 per minute, billed once per architecture: 400 x 6 x $0.06 x 2 sessions = $288.00 per month. The orchestrating runner bills separately, because the GitHub Actions runner and the Docker builder are two independent resources: warp-ubuntu-latest-x64-4x at $0.008 per minute for the same 6 minutes adds 400 x 6 x $0.008 = $19.20, for $307.20 per month.
Native runner route. Two parallel jobs, warp-ubuntu-latest-x64-8x at $0.016 and warp-ubuntu-latest-arm64-8x at $0.012: 400 x 6 x $0.028 = $67.20. The manifest job runs 1 minute on warp-ubuntu-latest-arm64-2x at $0.003 and adds $1.20, for $68.40 per month.
The gap is real, and so is what the higher number buys. The native route rebuilds layers on a fresh ephemeral machine each run unless you wire up registry cache or the Actions cache yourself. Dependency heavy Dockerfiles favor the builder; thin Dockerfiles over a fast base image favor the native split.
The ARM64 leg against GitHub list prices
The native route's arm64 job runs on the standard Linux ARM64 catalog, so it prices against GitHub's ARM64 larger runners directly. WarpBuild rates come from the cloud runners documentation. GitHub rates come from the GitHub Actions billing reference, checked on 2026-08-13.
| WarpBuild label | Shape | WarpBuild | GitHub-hosted ARM64 equivalent | GitHub | Difference |
|---|---|---|---|---|---|
| warp-ubuntu-latest-arm64-2x | 2 vCPU, 8 GB | $0.003 | ubuntu-24.04-arm, private repositories | $0.005 | 40 percent lower list price |
| warp-ubuntu-latest-arm64-4x | 4 vCPU, 16 GB | $0.006 | 4-core Linux ARM64 larger runner | $0.008 | 25 percent lower list price |
| warp-ubuntu-latest-arm64-8x | 8 vCPU, 32 GB | $0.012 | 8-core Linux ARM64 larger runner | $0.014 | 14 percent lower list price |
| warp-ubuntu-latest-arm64-16x | 16 vCPU, 64 GB | $0.024 | 16-core Linux ARM64 larger runner | $0.026 | 8 percent lower list price |
| warp-ubuntu-latest-arm64-32x | 32 vCPU, 128 GB | $0.048 | 32-core Linux ARM64 larger runner | $0.05 | 4 percent lower list price |
Applied to the worked model, the arm64 leg alone is 2,400 minutes a month: $28.80 on warp-ubuntu-latest-arm64-8x against $33.60 on the 8-core Linux ARM64 larger runner, a difference of $4.80 a month and $57.60 over twelve months.
Choosing between the routes
Three questions settle it in practice.
How much of the build is dependency installation and compilation? The more layers that survive between runs, the more the persistent cache on a builder profile is worth, and the closer the two monthly numbers get once build minutes drop.
Does the arm64 image need more than 64 vCPU? If yes, the multi architecture profile cannot serve it and the native split is the only route that scales past that ceiling.
How many pull requests build at once? Concurrent jobs on one profile share a session per architecture, so heavy parallelism spreads the builder cost across more builds. Concurrent jobs on native runners each bill their own minutes with no sharing.
Teams that want the emulation background before deciding will find it in the QEMU compared with native ARM64 builds guide, and the label by label rates for the native route are in the Linux ARM64 runner catalog.
Related Questions
Can one GitHub Actions job build both amd64 and arm64 images?
Yes, on a remote Docker builder profile that has both architectures enabled. Set platforms: linux/amd64,linux/arm64 on Warpbuilds/build-push-action and point profile-name at that profile. The alternative is one job per architecture on a matching runner, joined by a manifest list in a final job, which the multi platform Docker build guide covers end to end.
Why does my multi architecture build fail with exec format error?
The builder profile does not have both architectures selected. Enable amd64 and arm64 on the profile in the WarpBuild UI and rerun (Docker builders documentation). The same error appears on a single x64 runner when a binfmt handler for aarch64 was never registered in that job.
How is a multi architecture Docker builder build billed?
Each architecture runs on a separate builder instance, so one multi architecture build opens one session per architecture and the two sessions bill independently. A 6 minute build on the 16 vCPU profile at $0.06 per minute therefore costs $0.72 rather than $0.36. Sizes and rates are in the remote Docker builder catalog.
What is the largest multi architecture builder profile?
64 vCPU, 128 GB RAM, 200GB disk at $0.24 per minute. The 96 vCPU and 192 vCPU sizes are amd64 only, so arm64 and multi architecture profiles cap at 64 vCPU. Past that ceiling, run the arm64 half on the Linux ARM64 runner catalog instead.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.