Can I Build ARM64 Images on an x64 Runner?

Yes, through QEMU emulation of aarch64 instructions. A native ARM64 label such as warp-ubuntu-latest-arm64-8x at $0.012 per minute skips the translation.

Last verified:

Yes, an x64 runner builds linux/arm64 Docker images through QEMU emulation: docker/setup-qemu-action registers binfmt_misc handlers on the host, and BuildKit then executes every RUN layer as translated aarch64 instructions on x86-64 hardware (Docker multi-platform documentation). The same Dockerfile built on a native ARM64 runner such as warp-ubuntu-latest-arm64-8x at $0.012 per minute runs those steps directly on aarch64 hardware instead, so no instruction translation happens at all (cloud runners documentation, checked on 2026-08-13).

Answer

Three routes produce an arm64 image from a GitHub Actions workflow, and only the first one involves translation.

Emulation on an x64 runner. docker/setup-qemu-action installs QEMU static binaries and registers the binfmt_misc handlers, so the kernel routes aarch64 executables through user-mode QEMU. BuildKit runs each RUN layer inside an arm64 container on the x86-64 machine.

A native ARM64 runner. The job lands on aarch64 hardware and the build host architecture matches the target. WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners, and the Linux ARM64 runner catalog lists ten labels from 2 vCPU to 32 vCPU.

A remote Docker builder with arm64 enabled. The workflow job stays where it is and the build executes on a builder virtual machine of the target architecture with a persistent layer cache (Docker builders documentation).

Here is the emulated workflow in full:

name: image-arm64-emulated

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: warp-ubuntu-latest-x64-8x
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-qemu-action@v3
        with:
          platforms: arm64

      - uses: docker/setup-buildx-action@v3

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

      - uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

The native version of the same file changes one line and deletes one step:

 jobs:
   build:
-    runs-on: warp-ubuntu-latest-x64-8x
+    runs-on: warp-ubuntu-latest-arm64-8x
     steps:
       - uses: actions/checkout@v4

-      - uses: docker/setup-qemu-action@v3
-        with:
-          platforms: arm64
-
       - uses: docker/setup-buildx-action@v3

Everything below that point stays identical, including platforms: linux/arm64, the login step, and the tags. Per-minute rates for every label are on the pricing page.

Detail

What emulation translates and what it leaves alone

The overhead lands on the stages where guest code executes. File movement is host work and barely notices the architecture difference.

Build stageRuns as aarch64 code under emulationWhy
COPY and ADDNoBuildKit moves files on the host and executes no guest instructions
Base image pull and layer unpackNoRegistry transfer and extraction are host work
RUN apt-get installYesPackage post-install scripts and dpkg execute as aarch64 binaries
Compilation (gcc, cargo build, go build with cgo, node-gyp)YesThe compiler itself is an aarch64 binary, so every instruction it runs is translated
A test suite run inside the buildYesThe language runtime and the test binary both execute under translation
Layer packing and registry pushNoCompression and upload run on the host

Docker's multi-platform documentation makes the same split: emulation is slower than a native build, and compute-heavy work such as compilation carries most of that difference (Docker multi-platform documentation). A Dockerfile that copies a prebuilt binary into a slim base image sees little of it. A Dockerfile that compiles from source and then runs a test suite sees all of it.

Two secondary effects show up in emulated builds. Host-level probes report the x86-64 host, so any autodetection step that branches on CPU flags or core count takes a path the target hardware never runs. Runtimes that generate machine code at run time behave differently under translation, which is where JIT-heavy language runtimes and native extension builds tend to surprise people. The QEMU emulation glossary entry covers the mechanism in more detail.

Cross-compilation is the way to keep the build on the x64 runner without paying translation at all. The Dockerfile uses --platform=$BUILDPLATFORM on the build stage, reads TARGETARCH, and invokes a cross toolchain, so the compiler runs as native x86-64 code and emits aarch64 objects (Docker multi-platform documentation). It needs a Dockerfile written for it, and it produces no aarch64 test signal.

Pricing a native job against a longer emulated one

Rates come from the cloud runners documentation, checked on 2026-08-13. An emulated build bills on the x64 label it runs on, and a native build bills on the ARM64 label.

Wall clock of the build jobOn warp-ubuntu-latest-x64-8x at $0.016 per minuteOn warp-ubuntu-latest-arm64-8x at $0.012 per minute
5 minutes$0.080$0.060
10 minutes$0.160$0.120
20 minutes$0.320$0.240
40 minutes$0.640$0.480

Compare across rows: the emulated job and the native job sit on different rows, because the emulated one runs a longer clock through the compute-heavy stages. Time your own Dockerfile once on each route and the comparison is minutes times rate. The full ARM64 line runs from warp-ubuntu-latest-arm64-2x at $0.003 per minute to warp-ubuntu-latest-arm64-32x at $0.048 per minute, so a native build can also buy back time by taking a larger size.

Against GitHub-hosted hardware at the same shape: warp-ubuntu-latest-arm64-8x (8 vCPU, 32 GB) costs $0.012 per minute against $0.014 per minute for the 8-core Linux ARM64 larger runner (8 vCPU, 32 GB), which is 14 percent lower list price. GitHub list price checked on 2026-08-13 in the GitHub Actions billing reference, with shapes from the GitHub-hosted runner specifications.

The remote Docker builder route

The remote builder removes the architecture question from the runner entirely: the job keeps its x64 label, the build is sent to a builder virtual machine that carries a persistent layer cache, and the builder profile decides which architectures are available (Docker builders documentation).

Four operational facts govern that route, from the Docker builders documentation. Builder profiles carry amd64, arm64, and multi-architecture options, and an arm64 build against a profile without arm64 enabled fails with exec format error. A multi-architecture build runs each architecture on a separate builder instance, producing one session per architecture, and each session bills independently. The 16 vCPU, 32 GB, 100GB disk profile is $0.06 per minute per session, and arm64 and multi-architecture profiles cap at 64 vCPU. The runner and the builder are separate resources and both are billed, so the total for a job is runner minutes plus builder session minutes.

The QEMU versus native ARM64 guide works through which of the three routes fits which Dockerfile shape.

When emulation is still the right call

Emulation earns its place when the arm64 image is a release artifact rather than a test target, when the build is a handful of COPY steps over a prebuilt binary, or when you need one arm64 image today and rewriting the pipeline can wait. It is a poor place to run a test suite, because assertions execute under translation on a host whose CPU reports the wrong architecture, and the binary you assert against differs from the one running in production on aarch64 hosts.

Which build stages get slower under QEMU emulation?

The stages where guest code executes: package installs, compilation, and any test suite run inside the build. COPY and ADD steps, base image pulls, layer packing, and the registry push run on the host and execute no aarch64 instructions (Docker multi-platform documentation).

Do I still need docker/setup-qemu-action on a native ARM64 runner?

No. The runner is already aarch64, so the binfmt handlers have nothing to translate for a linux/arm64 target. Delete the step and set runs-on to an ARM64 label from the Linux ARM64 runner catalog, such as warp-ubuntu-latest-arm64-8x.

Why does my arm64 build fail with exec format error on a remote Docker builder?

The builder profile does not have arm64 enabled. Enable both architectures on the profile before setting platforms, and each architecture then runs on its own builder instance, producing one session per architecture (Docker builders documentation).

Can I emulate arm64 to build the image and still test on real ARM64 hardware?

Yes, and that split is common. Build and push the image from the x64 job, then add a second job on an ARM64 label that pulls the pushed digest and runs the suite. The QEMU versus native ARM64 guide covers the handoff.

Pick an ARM64 label from the Linux ARM64 runner catalog, price the change against your own build minutes on the pricing page, and read the QEMU emulation glossary entry before you decide which stages to emulate.

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.