You don't need QEMU, GitHub Actions, or expensive CI services to build multi-architecture Docker images. With just two VPSs (one cheap ARM machine and one cheap x86 machine) you can build and push native linux/amd64
and linux/arm64
containers without emulation, without performance penalties, and without a monthly surprise bill. My setup costs just €14/month in total and reliably builds for both architectures using Docker's buildx
feature.
The key is combining docker buildx
with SSH-based Docker contexts: each VPS becomes a node in a custom builder instance. The x86 VPS builds amd64 images locally, while the ARM VPS handles arm64 images natively. BuildKit takes care of the rest: parallel builds, multi-platform manifests, and direct pushes to the registry. It's clean, fast, and yours. No cloud CI, no opaque runners; just you, your servers, and full control over your builds.
Set up the local amd64 docker node:
docker buildx create --name multiarch --use --platform linux/amd64
Set up the remote arm64 docker node:
docker context create arm-vps --docker "host=ssh://[email protected]"
Append the arm64 remote builder:
docker buildx create --append --name multiarch --platform linux/arm64 arm-vps
Inspect:
docker buildx inspect rawpair-builder --bootstrap
Name: multiarch
Driver: docker-container
Last Activity: 2025-04-26 16:31:34 +0000 UTC
Nodes:
Name: multiarch0
Endpoint: unix:///var/run/docker.sock
Status: running
BuildKit daemon flags: --allow-insecure-entitlement=network.host
BuildKit version: v0.20.2
Platforms: linux/amd64*, linux/amd64/v2, linux/amd64/v3, linux/amd64/v4, linux/386
Labels:
org.mobyproject.buildkit.worker.executor: oci
org.mobyproject.buildkit.worker.hostname: baab70c53c2d
org.mobyproject.buildkit.worker.network: host
org.mobyproject.buildkit.worker.oci.process-mode: sandbox
org.mobyproject.buildkit.worker.selinux.enabled: false
org.mobyproject.buildkit.worker.snapshotter: overlayfs
GC Policy rule#0:
All: false
Filters: type==source.local,type==exec.cachemount,type==source.git.checkout
Keep Duration: 48h0m0s
Max Used Space: 488.3MiB
GC Policy rule#1:
All: false
Keep Duration: 1440h0m0s
Reserved Space: 9.313GiB
Max Used Space: 93.13GiB
Min Free Space: 47.5GiB
GC Policy rule#2:
All: false
Reserved Space: 9.313GiB
Max Used Space: 93.13GiB
Min Free Space: 47.5GiB
GC Policy rule#3:
All: true
Reserved Space: 9.313GiB
Max Used Space: 93.13GiB
Min Free Space: 47.5GiB
Name: multiarch1
Endpoint: arm-vps
Status: running
BuildKit daemon flags: --allow-insecure-entitlement=network.host
BuildKit version: v0.20.2
Platforms: linux/arm64*, linux/arm/v7, linux/arm/v6
Labels:
org.mobyproject.buildkit.worker.executor: oci
org.mobyproject.buildkit.worker.hostname: aba4522bbd7d
org.mobyproject.buildkit.worker.network: host
org.mobyproject.buildkit.worker.oci.process-mode: sandbox
org.mobyproject.buildkit.worker.selinux.enabled: false
org.mobyproject.buildkit.worker.snapshotter: overlayfs
GC Policy rule#0:
All: false
Filters: type==source.local,type==exec.cachemount,type==source.git.checkout
Keep Duration: 48h0m0s
Max Used Space: 488.3MiB
GC Policy rule#1:
All: false
Keep Duration: 1440h0m0s
Reserved Space: 9.313GiB
Max Used Space: 93.13GiB
Min Free Space: 47.5GiB
GC Policy rule#2:
All: false
Reserved Space: 9.313GiB
Max Used Space: 93.13GiB
Min Free Space: 47.5GiB
GC Policy rule#3:
All: true
Reserved Space: 9.313GiB
Max Used Space: 93.13GiB
Min Free Space: 47.5GiB
As you can see, the local node builds amd64 images, the remote node builds arm64 images.
I hope you find this useful.