In modern containerized applications, simultaneous support for both x86 (AMD64) and ARM64 architectures has become an essential requirement. Stop using fragmented tag management approaches like image:latest and image:arm-latest! By leveraging Docker's official buildx plugin in conjunction with QEMU, we can elegantly build true cross-platform multi-architecture images with a single command.
Below is the complete installation and usage guide.
1. Basic Environment Installation
To enable a standard x86 machine to compile code for the ARM architecture, you must first install the QEMU emulator and then install the Docker buildx plugin.
# 更新源并安装 qemu 静态执行文件和 buildx 插件
sudo apt-get update
sudo apt-get install -y qemu-user-static docker-buildx
2. Initialization and Builder Configuration
Docker's default legacy builder does not support multi-architecture builds. We need to create a new build instance based on BuildKit and set it as the default for the current environment.
# 创建一个名为 mybuilder 的新构建实例,并立即切换使用它
docker buildx create --use --name mybuilder
# 启动该构建器并检查其状态 (bootstrap 会拉取必要的 buildkit 容器)
docker buildx inspect --bootstrap
3. Check QEMU and Supported Architectures
You can check at any time which CPU architectures the current builder supports. Thanks to the QEMU installation you just completed, you should see a long list of supported architectures.
# 查看当前构建器支持的平台 (Platforms)
docker buildx ls
# 预期输出中应包含类似:linux/amd64, linux/arm64, linux/riscv64 等
4. Daily Management: Listing, Viewing, Cleaning, and Deleting
Managing your buildx builder is as simple as managing containers.
- List instances: View the builders available in the system
docker buildx ls - View details: Inspect the resource limits and policies of a specific builder
docker buildx inspect mybuilder - Prune cache: Free up discarded layer caches generated during the build process
docker buildx prune -a - Remove instance: Destroy it anytime when it is no longer needed
docker buildx rm mybuilder
5. Practical Use: One-Click Compilation of Dual-Architecture Images
Once everything is ready, replace the previous docker build with docker buildx build and add the --platform parameter.
# 同时构建 amd64 和 arm64 镜像
# 注意:多架构构建通常需要配合 --push 直接推送到镜像仓库,才能合并生成 Manifest 列表
docker buildx build --platform linux/amd64,linux/arm64 -t hub.yourdomain.com/my-app:latest --push .
6. Deep Dive: Why Can a Standard x86 PC Compile ARM Images?
You might find it magical: my CPU clearly uses the AMD64 instruction set, so why can I successfully run RUN apt-get install inside an ARM64 container?
This is actually QEMU user-mode emulation and the Linux kernel's binfmt_misc mechanism pulling a trick. After installing qemu-user-static, it registers a handling rule in the kernel: if the kernel encounters an ARM binary that cannot be executed natively, instead of throwing an error and blocking it, it hands it over to the QEMU translator. QEMU then translates ARM instructions into x86 instructions in real time in the background for execution.
So, although it is slightly slower than native compilation, your x86 machine uses this "real-time instruction translation" to perfectly deceive the container, making it believe it is running on actual ARM hardware.
7. The True Nature of Hybrid Images and Push Logic
In practical commands, we strongly recommend using --push to push directly. Why? Because a so-called "multi-architecture mixed image" is not simply cramming the files of two systems into the same large compressed package.
When you execute docker buildx build --platform linux/amd64,linux/arm64 -t my-app:latest --push ., the following actually happens:
- BuildKit independently compiles a pure AMD64 image and a pure ARM64 image in the background, separately.
- It uploads the actual content layers (Layers) of these two images to the image registry.
- Finally, it generates a Manifest List (or OCI Index). This acts like a "menu," recording
amd64 corresponds to hash A, arm64 corresponds to hash B, and pushes this menu to the registry with thelatesttag.
When other clients execute docker pull my-app:latest, the Docker engine first downloads this menu, checks it against the CPU architecture of the host machine, and downloads only the corresponding set of image layers. There is no redundancy whatsoever.
8. Verify Identity: Viewing Online Multi-Architecture Images with regctl
To verify whether the pushed image is truly multi-architecture, we can use the open-source tool regctl (a powerful CLI for interacting with image registries).
# 查看远程镜像的 manifest 列表
regctl image manifest hub.yourdomain.com/my-app:latest
You will see a JSON structure that clearly contains a manifests array, with entries marked as platform: linux/amd64 and platform: linux/arm64, along with their respective digests. This confirms that your multi-architecture image has been successfully published.
9. Export Limitations: Differences Between Hybrid and Standard Images
Standard images, once built, are by default stored in your docker images list (via the implicit --load parameter).
Multi-architecture images cannot be loaded directly into the local image list in traditional Docker engines. This is because the traditional local Docker daemon structure does not support storing Manifest Lists; it can only store a single architecture.
If you do not want to --push immediately but still need to export a multi-architecture image, you must export it as an OCI-format tar package:
# 将多架构镜像打包到本地文件,而不是加载到 docker daemon
docker buildx build --platform linux/amd64,linux/arm64 -o type=oci,dest=/tmp/my-app-multi.tar .
(Note: If your Docker is configured with the latest containerd image storage backend, you can use --load directly. However, this is not yet the default configuration on many servers.)
10. Cross-architecture testing: Forcibly running ARM images on x86 machines
After compilation, if you want to test the newly packaged ARM version on a local x86 machine, how should you proceed? Thanks to QEMU, you simply need to force the platform specification when running run:
# 强制拉取并运行 ARM64 架构的镜像
docker run --rm -it --platform linux/arm64 hub.yourdomain.com/my-app:latest uname -m
If the output is aarch64, congratulations, your x86 computer has successfully run an ARM system environment!
11. Pitfall Guide: What Happens if the Dockerfile Contains Dependencies Specific to a Particular Architecture?
Suppose your Dockerfile starts with RUN apt-get install -y gcc-x86-64-linux-gnu (a package that is obviously only available in AMD64 software repositories), or that you download a precompiled binary that only exists for x86.
When using buildx to build for dual architectures, the engine will execute the two build lines concurrently.
- The AMD64 line will execute successfully.
- The ARM64 line will hit an error at this step and crash directly, either because the package cannot be found in the ARM apt repositories, or due to binary format incompatibility.
The conclusion is: if the build fails for even one architecture, the entire buildx task is considered failed, and no image will be pushed. This forces us to ensure the universality of commands when writing multi-architecture Dockerfiles (for example, by using system variables to download packages for the corresponding architecture), thereby guaranteeing true full-stack compatibility at the source code level.
12. Advanced Black Magic: Elegantly Injecting QEMU Dependencies in CI/CD Pipelines
In Step 1 of this article, to enable a standard x86 machine to compile cross-platform code such as ARM, we executed sudo apt-get install qemu-user-static on the host.
However, in real-world CI/CD pipelines (such as GitLab Runner or GitHub Actions) or on cloud servers, we often face a very practical issue: we typically only have permission to run Docker, and we do not want to, should not, or simply cannot pollute the underlying host operating system environment (for example, by running apt install all over the place).
In this case, we need to employ the most core and elegant "black magic" in cross-platform builds:
# 在宿主机内核中一键注册多架构 QEMU 翻译器
docker run --privileged --rm tonistiigi/binfmt --install all
This extremely concise command is essentially the "Docker-containerized advanced alternative" to qemu-user-static.
What is the origin of this container?
- Authentic Official Lineage: The name
tonistiigicarries significant weight. He is Tõnis Tiigi, a core maintainer of Docker, and the primary architect behind BuildKit (the underlying engine for buildx). Therefore, this image is absolutely not some random third-party script, but rather an official, first-class solution. - Its Essence: This is a utility image that packages the latest version of
qemu-user-staticbinaries for all architectures, along with an automated registration script.
What Is It Used For? Unveiling How It Works
When you run this command, it accomplishes four major tasks in just a few seconds:
- Privilege escalation and mounting (
--privileged): Because it needs to modify the behavior of the Linux kernel, the container must run in privileged mode. It quietly mounts the host's/proc/sys/fs/binfmt_miscdirectory. - Registering kernel "translators" (
--install all):binfmt_miscis a feature of the Linux kernel. This container writes a set of rules into it, instructing the host kernel:
"Hey! Kernel buddy, from now on, if you encounter a binary executable file starting with ARM, MIPS, or even LoongArch (Loongson) feature codes, don't throw an error! Please hand it directly to the injected
qemu-aarch64translator or similar to run!"
- Leaving no trace (
--rm): After configuring the kernel rules and injecting the static translator files, this container immediately "commits suicide" and exits, ensuring it never occupies any persistent memory or processes on the host. - Reporting the results (JSON output): After execution, it prints a JSON table to the console. When you see
linux/arm64,linux/s390x, or evenlinux/loong64in thesupportedlist, it is reporting to you: the host kernel has "enlightened" itself and now fully possesses the capability to run cross-platform containers!
Why is this approach strongly recommended?
The core reason comes down to two words: clean.
With this approach, your CI server doesn't need to have any additional deb/rpm packages pre-installed. Regardless of whether your Runner node is running on Ubuntu, CentOS, or a lightweight Alpine, as long as it has Docker installed, running this single command instantly transforms it into a fully capable multi-architecture build machine. It's ready-to-use, highly portable, and leaves no trace behind.
Summary: A Perfect CI/CD Initialization Script
Combining the buildx builder dynamic allocation and the binfmt magic mentioned in this article, an elegant and highly secure CI pipeline initialization script (for example, the before_script in .gitlab-ci.yml) should look like this:
before_script:
# 1. 无侵入注入多架构 QEMU 支持
- docker run --privileged --rm tonistiigi/binfmt --install all
# 2. 为当前 Job 创建专属的、隔离的 buildx 构建器
- docker buildx create --use --name builder-${CI_JOB_ID}
- docker buildx inspect --bootstrap
Mastering these concepts will not only enable you to effortlessly build dual-architecture images locally, but also give you full control over complex, enterprise-grade multi-architecture automated release pipelines!
💡 Common Question: Why is there an extra, unfamiliar container in docker ps?
After running docker buildx create ... --bootstrap, if you run docker ps on your local machine, you will notice an additional container named buildx_buildkit_xxx using the moby/buildkit image running in the background.
Don't panic, this is completely normal!
Traditional docker build directly invokes the Docker Daemon on the host machine (which is the native driver marked as default* in your screenshot) to perform the build. However, the native Docker engine has limitations when handling multi-architecture mixed output (Manifest List), advanced concurrency, and complex external caching.
To overcome these limitations, Docker's official solution is: launch a standalone container running the latest BuildKit engine and delegate all build tasks to this container. The buildx_buildkit_mybuilder0 you see is the dedicated build engine you just "woke up" with the --bootstrap command. As long as it is running in the background, your cross-platform builds can continuously benefit from its fast caching.
- Will it consume resources? In an idle state, it uses almost no CPU, and its memory footprint is very small.
- Can I turn it off? Yes. If you rarely build and find it distracting, you can run
docker buildx stop mybuilderto put it to sleep, or simply rundocker buildx rm mybuilderto destroy it completely (this will also clear its local build cache). You can simply create a new one next time you need cross-platform builds.
Comments (0)
No comments yet. Be the first to leave one!