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 sizeDiskPer minuteArchitectures
16 vCPU, 32 GB100GB$0.06amd64, arm64, multi
32 vCPU, 64 GB200GB$0.12amd64, arm64, multi
64 vCPU, 128 GB200GB$0.24amd64, arm64, multi
96 vCPU, 192 GB600GB$0.36amd64 only
96 vCPU, 192 GB2TB$0.52amd64 only
192 vCPU, 384 GB600GB$0.72amd64 only
192 vCPU, 384 GB2TB$0.88amd64 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 labelShapeWarpBuildGitHub-hosted ARM64 equivalentGitHubDifference
warp-ubuntu-latest-arm64-2x2 vCPU, 8 GB$0.003ubuntu-24.04-arm, private repositories$0.00540 percent lower list price
warp-ubuntu-latest-arm64-4x4 vCPU, 16 GB$0.0064-core Linux ARM64 larger runner$0.00825 percent lower list price
warp-ubuntu-latest-arm64-8x8 vCPU, 32 GB$0.0128-core Linux ARM64 larger runner$0.01414 percent lower list price
warp-ubuntu-latest-arm64-16x16 vCPU, 64 GB$0.02416-core Linux ARM64 larger runner$0.0268 percent lower list price
warp-ubuntu-latest-arm64-32x32 vCPU, 128 GB$0.04832-core Linux ARM64 larger runner$0.054 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.

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.