|
|
2024 年,我在一台 M2 MacBook 上 build 了一个镜像,推到服务器,然后它就崩了。
报错信息简洁有力:。
翻译成人话就是——你在 ARM 上烤的饼,x86 的烤箱不认。这个问题,每一个用 Apple Silicon 的开发者迟早都会遇到。而解决它的最佳方案,就是 Docker 多架构镜像构建。
先搞清楚:什么是多架构镜像?
传统 Docker 镜像是"一个镜像对应一个平台"。你在 x86 机器上 build 的镜像,只能在 x86 上跑;ARM 机器上 build 的,只能在 ARM 上跑。
多架构镜像的本质是一个 manifest list——它本身不包含任何文件层,而是一个"目录",指向不同平台的实际镜像。当你执行时,Docker 会自动根据当前机器的架构,从 manifest list 中选择匹配的那个版本拉取。
用一个生活化的类比:manifest list 就像一个多语言菜单,中国人拿到中文版,日本人拿到日文版,但封面写的是同一个餐厅名字。
涉及的核心架构主要有两种:
| 架构标识 | 典型设备 |
|---|---|
|| 绝大多数云服务器、传统 PC |
|| Apple Silicon Mac、树莓派4、AWS Graviton |
当然还有(老款树莓派)等,但 amd64 和 arm64 覆盖了 95% 的场景。
工具准备:Docker Buildx 与 QEMU
Docker Buildx 是 Docker 官方提供的增强构建工具,从 Docker 19.03 开始内置。它基于 BuildKit,支持多平台构建、缓存导出等高级特性。
第一步:确认 Buildx 可用。
如果输出版本号,说明已经就绪。
第二步:创建并启用一个支持多平台的 builder 实例。
- docker buildx create --name multiarch-builder --driver docker-container --bootstrap --use
复制代码
这里有个关键参数:- --driver 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 不需要任何修改就能支持多架构构建——因为和本身就是多架构镜像,Docker 会自动拉取对应平台的基础镜像。
一条命令搞定构建并推送:
- docker buildx build \
- --platform linux/amd64,linux/arm64 \
- -t your-registry/your-app:latest \
- --push \
- .
复制代码
几个要点需要注意:
参数指定目标平台,逗号分隔。
是必须的。多平台镜像无法直接到本地 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
复制代码 的值会是、等,则是完整的格式。这两个变量不需要在命令中手动传入,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 架构。这个 action 帮你完成了之前手动执行那一步。
缓存策略很重要。和使用了 GitHub Actions 的缓存后端(),能显著加速后续构建。没有缓存的情况下,QEMU 模拟构建 ARM 镜像比原生构建慢 3-5 倍,缓存能把这个痛苦降到最低。
性能优化:QEMU 太慢怎么办?
QEMU 模拟构建的速度确实感人(反义)。如果你的镜像构建涉及大量编译操作,比如 C/C++ 项目或大型 Node.js 依赖安装,QEMU 模拟可能慢到令人怀疑人生。
方案一:交叉编译替代模拟执行。
Go、Rust 等语言天然支持交叉编译。以 Go 为例,设置环境变量就能编译出目标架构的二进制文件,完全不需要 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 阶段使用构建机器的原生架构运行,只在最终产物上做交叉编译。这样以原生速度运行,只是输出了不同架构的二进制文件。
方案二:使用原生 ARM runner。
GitHub Actions 已经提供了 ARM runner(),你可以用矩阵策略在不同架构的 runner 上分别原生构建,然后合并 manifest。不过这会增加工作流复杂度,适合构建时间超过 20 分钟的大型项目。
常见踩坑清单
1. 基础镜像不支持多架构。 不是所有镜像都提供了 ARM 版本。构建前先用- docker buildx imagetools inspect
复制代码 检查基础镜像支持的平台。
2.安装的包架构不对。 这种情况通常发生在你用- --platform=$BUILDPLATFORM
复制代码 做交叉编译时,多阶段构建的运行阶段会自动使用目标架构,不用担心。
3. 本地无法测试另一个架构的镜像。 其实可以。只要 QEMU 已注册,直接- docker run --platform linux/arm64 your-image
复制代码 就能在 x86 机器上运行 ARM 镜像,速度虽然慢但功能完全正常。
4. 构建缓存失效。 多平台构建的缓存是按平台分别存储的,切换 platform 列表顺序不会影响缓存命中。但如果你更换了 builder 实例,缓存会全部丢失。
━━━━━━━━━━━━━━━━━━━━
从到一条命令搞定全平台兼容,Docker 多架构构建的学习曲线其实并不陡。核心就三步:装好 QEMU,创建 Buildx builder,构建时指定。
剩下的,交给 BuildKit 就好。 |
|