共计 4462 个字符,预计需要花费 12 分钟才能阅读完成。
文章目录 [显示]
2024 年,我在一台 M2 MacBook 上 build 了一个镜像,推到服务器,然后它就崩了。
报错信息简洁有力:exec format error。
翻译成人话就是——你在 ARM 上烤的饼,x86 的烤箱不认。这个问题,每一个用 Apple Silicon 的开发者迟早都会遇到。而解决它的最佳方案,就是 Docker 多架构镜像构建 。
先搞清楚:什么是多架构镜像?
传统 Docker 镜像是 " 一个镜像对应一个平台 "。你在 x86 机器上 build 的镜像,只能在 x86 上跑;ARM 机器上 build 的,只能在 ARM 上跑。
多架构镜像的本质是一个 manifest list——它本身不包含任何文件层,而是一个 " 目录 ",指向不同平台的实际镜像。当你执行 docker pull 时,Docker 会自动根据当前机器的架构,从 manifest list 中选择匹配的那个版本拉取。
用一个生活化的类比:manifest list 就像一个多语言菜单,中国人拿到中文版,日本人拿到日文版,但封面写的是同一个餐厅名字。
涉及的核心架构主要有两种:
| 架构标识 | 典型设备 |
|---|---|
linux/amd64 |
绝大多数云服务器、传统 PC |
linux/arm64 |
Apple Silicon Mac、树莓派 4、AWS Graviton |
当然还有 linux/arm/v7(老款树莓派)等,但 amd64 和 arm64 覆盖了 95% 的场景。
工具准备:Docker Buildx 与 QEMU
Docker Buildx 是 Docker 官方提供的增强构建工具,从 Docker 19.03 开始内置。它基于 BuildKit,支持多平台构建、缓存导出等高级特性。
第一步:确认 Buildx 可用。
docker buildx version
如果输出版本号,说明已经就绪。
第二步:创建并启用一个支持多平台的 builder 实例。
docker buildx create --name multiarch-builder --driver docker-container --bootstrap --use
这里有个关键参数:--driver docker-container。默认的 docker 驱动不支持多平台构建,必须切换到 docker-container 驱动,它会启动一个独立的 BuildKit 容器来执行构建任务。
第三步:检查支持的平台列表。
docker buildx inspect --bootstrap
你会看到类似这样的输出:
Platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/386...
这些平台支持来自 QEMU 用户态模拟器 。简单说,QEMU 允许你在 x86 机器上模拟 ARM 指令集(反之亦然),这样你就能在一台机器上为多个平台编译镜像。
在 Docker Desktop(Mac/Windows)中,QEMU 已经预装好了。如果你用的是 Linux 服务器,需要手动安装:
docker run --privileged --rm tonistiigi/binfmt --install all
这条命令会注册所有支持的二进制格式处理器到内核中。执行一次即可,重启后需要重新执行(除非你做了持久化配置)。
实战:构建你的第一个多架构镜像
假设我们有一个简单的 Go 应用,Dockerfile 如下:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM alpine:3.19
COPY --from=builder /app/server /usr/local/bin/server
EXPOSE 8080
CMD ["server"]
这个 Dockerfile 不需要任何修改就能支持多架构构建——因为 golang:1.22-alpine 和 alpine:3.19 本身就是多架构镜像,Docker 会自动拉取对应平台的基础镜像。
一条命令搞定构建并推送:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t your-registry/your-app:latest \
--push \
.
几个要点需要注意:
--platform参数指定目标平台,逗号分隔。--push是必须的。多平台镜像无法直接--load到本地 Docker(因为本地只能存一个平台的镜像),所以必须推送到远程仓库。- 如果你只是想本地测试单个平台,可以单独指定:
--platform linux/arm64 --load。
构建完成后,验证 manifest list:
docker buildx imagetools inspect your-registry/your-app:latest
你会看到它包含两个平台的 digest,确认构建成功。
进阶:Dockerfile 中的架构感知
有些场景下,你需要在 Dockerfile 内部根据架构做不同处理。比如下载预编译的二进制文件时,不同架构的下载链接不同。
Buildx 会自动注入几个构建参数:
FROM alpine:3.19
ARG TARGETPLATFORM
ARG TARGETARCH
RUN echo "Building for ${TARGETPLATFORM}, arch: ${TARGETARCH}"
RUN if ["${TARGETARCH}" = "amd64" ]; then \
wget -O /usr/local/bin/tool https://example.com/tool-x86_64; \
elif ["${TARGETARCH}" = "arm64" ]; then \
wget -O /usr/local/bin/tool https://example.com/tool-aarch64; \
fi && \
chmod +x /usr/local/bin/tool
TARGETARCH 的值会是 amd64、arm64 等,TARGETPLATFORM 则是完整的 linux/amd64 格式。这两个变量不需要在 docker buildx build 命令中手动传入,BuildKit 会自动设置。
CI/CD 集成:GitHub Actions 实战
在 CI 中实现自动化多架构构建,GitHub Actions 是最常见的选择。以下是一个完整的工作流配置:
name: Build Multi-Arch Image
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
password: ${{secrets.DOCKERHUB_TOKEN}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
your-dockerhub-user/your-app:${{github.ref_name}}
your-dockerhub-user/your-app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
这个配置有几个值得注意的细节:
QEMU 必须单独安装 。GitHub Actions 的 runner 是 x86 机器,需要 QEMU 来模拟 ARM 架构。docker/setup-qemu-action 这个 action 帮你完成了之前手动执行 binfmt 那一步。
缓存策略很重要 。cache-from 和 cache-to 使用了 GitHub Actions 的缓存后端(type=gha),能显著加速后续构建。没有缓存的情况下,QEMU 模拟构建 ARM 镜像比原生构建慢 3-5 倍,缓存能把这个痛苦降到最低。
性能优化:QEMU 太慢怎么办?
QEMU 模拟构建的速度确实感人(反义)。如果你的镜像构建涉及大量编译操作,比如 C/C++ 项目或大型 Node.js 依赖安装,QEMU 模拟可能慢到令人怀疑人生。
方案一:交叉编译替代模拟执行。
Go、Rust 等语言天然支持交叉编译。以 Go 为例,设置 GOARCH 环境变量就能编译出目标架构的二进制文件,完全不需要 QEMU:
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
ARG TARGETARCH
WORKDIR /app
COPY . .
RUN GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -o server .
FROM alpine:3.19
COPY --from=builder /app/server /usr/local/bin/server
CMD ["server"]
关键在第一行的 --platform=$BUILDPLATFORM——它强制 builder 阶段使用构建机器的原生架构运行,只在最终产物上做交叉编译。这样 go build 以原生速度运行,只是输出了不同架构的二进制文件。
方案二:使用原生 ARM runner。
GitHub Actions 已经提供了 ARM runner(ubuntu-latest-arm64),你可以用矩阵策略在不同架构的 runner 上分别原生构建,然后合并 manifest。不过这会增加工作流复杂度,适合构建时间超过 20 分钟的大型项目。
常见踩坑清单
1. 基础镜像不支持多架构。 不是所有镜像都提供了 ARM 版本。构建前先用 docker buildx imagetools inspect 检查基础镜像支持的平台。
2. apt-get install 安装的包架构不对。 这种情况通常发生在你用 --platform=$BUILDPLATFORM 做交叉编译时,多阶段构建的运行阶段会自动使用目标架构,不用担心。
3. 本地无法 docker run 测试另一个架构的镜像。 其实可以。只要 QEMU 已注册,直接 docker run --platform linux/arm64 your-image 就能在 x86 机器上运行 ARM 镜像,速度虽然慢但功能完全正常。
4. 构建缓存失效。 多平台构建的缓存是按平台分别存储的,切换 platform 列表顺序不会影响缓存命中。但如果你更换了 builder 实例,缓存会全部丢失。
从 exec format error 到一条命令搞定全平台兼容,Docker 多架构构建的学习曲线其实并不陡。核心就三步: 装好 QEMU,创建 Buildx builder,构建时指定 --platform。
剩下的,交给 BuildKit 就好。