QEMU Emulation
QEMU emulation runs binaries built for another processor architecture by translating their instructions, which is how an x64 machine builds an ARM64 image.
QEMU emulation runs a program compiled for one processor architecture on a machine with a different one, by translating the program's instructions into instructions the host CPU can execute. In a Docker build it is the mechanism that lets an x64 machine run the aarch64 binaries inside an arm64 base image, and the translated code runs far slower than the same binaries would on an ARM64 machine.
Two properties of that sentence explain most confusing build logs. The translation applies only to processes executing guest binaries, so parts of the same build stay at full speed. And nothing in the workflow file names the interpreter, so the only visible trace is a setup step near the top of the job.
Definition
QEMU is an open source machine emulator and virtualizer. It has two modes, and the difference decides what a Docker build is paying for.
User mode emulation runs a single foreign binary as an ordinary process on the host. QEMU translates that process's instructions and passes its system calls through to the host kernel. There is no guest kernel and no virtual hardware.
System mode emulation emulates a whole machine: a CPU, memory, devices, and a guest kernel booted on top of them. It is what you use to boot an operating system for another architecture.
Container image builds use user mode. A container shares the host kernel already, so the only thing missing on an x64 machine is a way to execute aarch64 user space binaries.
How the kernel routes a foreign binary
The connection between the two is binfmt_misc, a Linux kernel feature that maps the magic bytes at the start of a file to an interpreter that should run it. Registration writes one file per architecture under /proc/sys/fs/binfmt_misc. Each file names the ELF magic for that architecture, a mask, and the path to the QEMU binary that handles it.
Docker's multi-platform build documentation describes installation as registering the emulators with binfmt_misc, and it documents verifying the result by checking that F appears among the flags in /proc/sys/fs/binfmt_misc/qemu-* (Docker multi-platform builds, checked on 2026-08-13). That F flag makes the kernel open the interpreter at registration time and hold the reference, which is why the handler still works inside a container whose filesystem contains no copy of QEMU.
Once an entry exists, running an aarch64 binary on an x64 machine starts qemu-aarch64 with that binary as its argument. Without the entry the same command returns exec format error.
What the interpreter actually does
Translation happens a basic block at a time. QEMU reads a run of guest instructions up to the next branch, compiles it into host instructions, caches the result, and jumps into the cached form. A hot loop pays the translation cost once and then reuses it, so long compiles behave better than a per instruction model would predict, and still land well short of native speed.
System calls take a separate path. The interpreter catches each one, converts the arguments from the guest ABI to the host ABI, and forwards it to the host kernel. Work that makes many small calls, such as a linker walking thousands of object files, pays that conversion on every call.
Only part of a build passes through the interpreter:
| Part of an emulated image build | What executes it | Translated |
|---|---|---|
| Guest binaries in the target rootfs: compilers, linkers, package managers, tests | QEMU on the host CPU | Yes |
| System calls made by those binaries | Host kernel, arguments converted first | Partly |
The build engine, base image pulls, COPY and ADD | Host CPU, natively | No |
Kernel interfaces such as /proc/cpuinfo and nproc | Host kernel, reporting host values | No |
The last row is a frequent source of surprise. A step that sizes its parallelism from the detected core count reads host x64 information while running guest code, and a library that probes for CPU features can receive an answer the interpreter does not implement, which surfaces as Illegal instruction rather than as a clear emulation error.
Docker documents three strategies for producing multi-platform images: emulation with QEMU, multiple native build nodes, and cross compilation in a multi-stage build. It states that emulation with QEMU can be much slower than native builds, especially for compute heavy tasks like compilation and compression or decompression, and recommends the other two strategies where they apply.
Example
This workflow builds an arm64 image on an x64 machine. ubuntu-latest is GitHub's default hosted Linux label and is an x64 machine, so the docker/setup-qemu-action step is what makes the build possible at all.
name: image
on:
push:
branches: [main]
jobs:
build-arm64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- uses: docker/setup-buildx-action@v3
- name: Build and push the arm64 image
run: |
docker buildx build \
--platform linux/arm64 \
--tag ghcr.io/${{ github.repository }}:${{ github.sha }}-arm64 \
--push \
.After the setup step, the registration is visible in the job:
$ cat /proc/sys/fs/binfmt_misc/qemu-aarch64
enabled
interpreter /usr/bin/qemu-aarch64
flags: OCFEvery RUN step in the Dockerfile now executes inside a rootfs of aarch64 binaries, and each one reaches the CPU as translated code.
The same build on an ARM64 machine drops the setup step entirely. GitHub publishes ubuntu-24.04-arm as a hosted ARM64 label in its hosted runners reference, checked on 2026-08-13:
jobs:
build-arm64:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build and push the arm64 image
run: |
docker buildx build \
--platform linux/arm64 \
--tag ghcr.io/${{ github.repository }}:${{ github.sha }}-arm64 \
--push \
.The Dockerfile, the tag, and the buildx command are identical. What changed is the architecture of the machine underneath, so --platform linux/arm64 now names the host's own architecture and the binaries execute directly.
| Behavior | Emulated build on an x64 machine | Native build on an ARM64 machine |
|---|---|---|
binfmt_misc handler | Required before the build | Unused |
| Guest instructions | Translated by QEMU, then executed | Executed by the CPU |
| System calls | Intercepted and converted | Direct |
nproc and /proc/cpuinfo | Host x64 values | ARM64 values |
| Common failure signatures | exec format error, Illegal instruction, hangs in JIT heavy steps | Ordinary build errors |
A workflow that has to publish both architectures under one tag runs one job per architecture and joins the two results with a manifest list, which keeps the interpreter out of both jobs.
Related Terms
- Moving ARM64 Docker builds off QEMU emulation: which Dockerfile steps absorb the translation cost, the workflow diff for each route off emulation, and how to read the resulting build times.
- Cross compilation, the other way to target a foreign architecture: producing aarch64 binaries from a compiler running natively on x64, and the build shapes where that works.
- Linux ARM64 runners and the labels they register with: ARM64 label shapes, image differences, and the work directory change to watch for.
- Docker multi-platform build documentation: the upstream reference for emulation, native nodes, and cross compilation.
- WarpBuild cloud runners documentation: the runner catalog with sizes, images, and architectures.
- WarpBuild Docker builder documentation: builder profiles and the actions that point a build at a builder outside the job.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What is QEMU emulation in a Docker build?
It is user mode emulation of a foreign processor architecture. QEMU translates the guest instructions of every binary inside the target rootfs into host instructions and forwards the guest syscalls to the host kernel, so an x64 machine can execute the aarch64 binaries in an arm64 base image.
How does the kernel know to hand a binary to QEMU?
Through binfmt_misc, a Linux kernel feature that maps the magic bytes at the start of a file to an interpreter. Registering an aarch64 entry writes a file under /proc/sys/fs/binfmt_misc naming the aarch64 ELF magic and the qemu-aarch64 binary, and the kernel routes matching binaries there instead of failing with exec format error.
Which build steps get slower under QEMU emulation?
The steps that execute guest binaries and burn CPU: compilers, linkers, minifiers, package managers that build from source, and tests run during the image build. Docker's multi-platform documentation states that emulation with QEMU can be much slower than native builds, especially for compute heavy tasks like compilation and compression or decompression.
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.