Why Multi-Architecture Docker Images Matter in Your Homelab
Most homelabs start as a single x86_64 server. A Dell PowerEdge, a custom Ryzen build, a repurposed office PC. Then the Raspberry Pi shows up. Then an Orange Pi for a low-power always-on service. Then a Mac Mini M-series running containers. Suddenly you have a mixed-architecture environment and every second docker pull downloads the wrong CPU instruction set.
When you pull nginx:alpine on an arm64 machine without a manifest-aware process, Docker either:
- Pulls the correct arm64 variant automatically (if the image has a manifest list), or
- Pulls amd64 and runs it through QEMU user-mode emulation — silently, and 3–5x slower.
The fix is to stop relying on upstream manifests and build your own multi-architecture images. Docker Buildx wraps BuildKit with multi-platform support, letting you produce a single tagged image that contains native layers for every architecture you target.
How Docker Multi-Architecture Images Work
Under the hood, a multi-arch image is an OCI image index (formerly called a manifest list). It contains references to the actual manifest for each platform:
|
|
When a Docker client pulls the image, it reads the index, matches its own architecture, and downloads only the relevant manifest and layers. No emulation overhead, no wasted bandwidth.
Prerequisites
- Docker Engine 24.0 or later (BuildKit is built in)
- A registry to push to (Docker Hub, GitHub Container Registry, or a self-hosted registry with MinIO)
- Linux kernel with
binfmt_miscsupport (all modern kernels)
Step 1 — Enable Cross-Platform Emulation with QEMU
Docker Buildx uses QEMU user-mode emulation to build arm64 binaries on an amd64 host and vice versa. First, register the binfmt handlers:
|
|
This installs QEMU static binaries and registers them with the kernel’s binfmt_misc subsystem. Verify the builders are registered:
|
|
You should see entries like qemu-aarch64, qemu-x86_64, and others.
Step 2 — Create a Multi-Architecture Docker Builder
Buildx ships with a default docker driver that uses the local Docker Engine — but it does not support multi-platform builds. You need the docker-container driver, which runs BuildKit in its own container:
|
|
Verify the builder supports your target platforms:
|
|
The output should list linux/amd64 and linux/arm64 under the multiarch builder. If arm64 is missing, re-run the binfmt step and restart the builder:
|
|
Set this builder as the default for your shell:
|
|
Step 3 — Write a Cross-Platform Dockerfile
The key to multi-arch Dockerfiles is the built-in BUILDPLATFORM and TARGETPLATFORM build arguments. BuildKit sets these automatically based on the target architecture:
|
|
For interpreted languages like Python or Node.js, the same image works on both architectures — you only need to ensure the base image is multi-arch aware:
|
|
Most official Docker images (alpine, node, python, nginx, postgres) are already multi-arch. The --platform=$TARGETPLATFORM ensures BuildKit resolves the correct variant during the build.
Step 4 — Build and Push Multi-Arch Images
The simplest approach is a single docker buildx build command:
|
|
Important: --push is required when building for multiple platforms. The --load flag only supports a single architecture — loading a multi-platform result into the local Docker image store is not supported.
For projects with multiple images, use a bake file (docker-bake.hcl):
|
|
Build and push everything in one command:
|
|
Step 5 — Verify the Multi-Arch Manifest
After pushing, inspect the image manifest to confirm both architectures are present:
|
|
You should see output listing both platforms:
Name: ghcr.io/yourname/myapp:latest
MediaType: application/vnd.oci.image.index.v1+json
Manifests:
linux/amd64 @ sha256:abc...
linux/arm64 @ sha256:def...
Test the arm64 pull on a Raspberry Pi or any ARM machine:
|
|
Integrating Multi-Arch Builds with Homelab CI/CD
If you run Gitea with Actions, add a workflow that builds and pushes multi-arch images on every tag:
|
|
The same workflow works for GitHub Actions, Woodpecker, or any Drone-compatible runner.
Common Pitfalls
QEMU crashes on certain Go crypto instructions. Some Go packages (especially golang.org/x/crypto with assembly optimizations) use CPU-specific instructions that QEMU user-mode cannot emulate. Workaround: build with CGO_ENABLED=0 or use the -tags=noasm build tag.
BuildKit cache mounts break across architectures. Cache mounts (--mount=type=cache) are not shared between different platform builds. Each architecture gets its own separate cache directory.
--load only stores one platform. If you run docker buildx build --platform linux/amd64,linux/arm64 --load, BuildKit silently builds only the native platform (your host architecture). Always use --push for multi-platform builds.
Emulated builds are slow. Building arm64 on amd64 via QEMU is 3–5x slower than a native arm64 build. For production workloads, use native ARM runners or cross-compilation instead of emulation.
Summary
Docker Buildx turns your single-architecture homelab server into a multi-architecture build station. With one QEMU container, one builder, and one Dockerfile, you can produce images that run natively on everything from an x86_64 Proxmox host to an ARM-based Raspberry Pi cluster.
The workflow is consistent whether you push to Docker Hub, GitHub Container Registry, or your own self-hosted MinIO-backed registry. Combined with a Gitea Actions workflow, every tagged commit produces verified multi-arch images with zero manual effort.