上个月公司要做信创改造,业务系统整体往国产 ARM 服务器(鲲鹏 920 / 飞腾 S2500 这类 aarch64 平台)上迁。我的机器还是原来那台 x86_64 的云主机,于是顺手把 Dockerfile 推上去跑了个遍构建过程没报错,容器也起来了,结果 docker ps 一看,状态直接是 Exited (126),日志里一行 exec format error。
这个问题说实话不新鲜,但它坑的地方在于「构建通过、运行失败」这个组合。镜像能拉、能存、能导出,看着一切正常,只有真正 exec 那个二进制的那一刻才炸。而且报错信息短得不像话,很多人第一反应是「镜像坏了」或者「权限不对」,其实方向完全错了。
如果你正在把服务往 ARM 服务器上迁,或者需要在国产化环境里把现有镜像跑起来,先看看这几家的裸金属与物理服务器报价和技术说明,选一台机器先把编译环境搭起来往往比反复换镜像省事:zji(独立服务器 / 裸金属)
先分清架构:uname -m 那行字才是真的
排查的第一步永远是确认目标机器的真实架构,不要相信控制台面板上写的,也别信云厂商镜像列表里的标签,直接在机器上敲:
uname -m
输出 x86_64 就是 Intel/AMD 生态,输出 aarch64(老内核里叫 arm64)才是 ARM。国产 ARM 服务器基本都是 aarch64。
然后把镜像的架构也查出来,注意命令里那个 --format 参数不能少:
docker image inspect 镜像名:标签 --format '{{.Architecture}}/{{.Os}}'
如果这里出来的是 amd64/linux,而目标机器是 aarch64/linux,那问题基本就定位了。ELF 文件头里的 machine 字段决定了能不能被当前内核执行:x86_64 的 ELF 头是 0x3E,aarch64 的是 0xB7,内核在 execve 的时候做了一次比对,不匹配就直接返回 ENOEXEC,翻译成人话就是 exec format error。

原因一:基础镜像本身就是 x86 的
最常见的一种。写 Dockerfile 时习惯性用了 FROM node:18、FROM python:3.11 这类标签,没带架构后缀。Docker Hub 上这些官方镜像绝大多数同时维护了 amd64 和 arm64 两个版本,理论上拉的时候会自动挑对应架构的那个但前提是你的 Docker 客户端和容器运行时确实支持。
我那次出问题,是因为 Dockerfile 里有一行 FROM registry.internal/base:1.4,这是我们自己的私有镜像,用 Dockerfile FROM 指令构建出来的,中间某次 CI 只在 x86 机器上 build 了一次,产物里就固化成 amd64 了。这种「内网基础镜像」是最容易漏掉的,因为它的标签看着人畜无害。
解决办法有两种。一种是显式指定:
FROM --platform=linux/arm64 registry.internal/base:1.4
另一种更彻底,直接用 buildx 出多架构镜像,一次构建两个平台都带上:
docker buildx build --platform linux/amd64,linux/arm64 -t 你的仓库/app:1.0 --push .
如果 buildx 报「multiple platforms feature is currently not supported」,说明 Docker 版本太老或者没装 buildx 插件,先 docker buildx version 确认一下。

原因二:多架构清单(manifest list)被压成了单架构
这个坑更隐蔽,也是我查了最久的那个。
带 manifest list 的镜像,导出的时候如果你用的是 docker save 而不是 docker buildx imagetools create 加 --push,很容易只导出一个平台的镜像文件。我把镜像 docker save 成 tar 包传到 ARM 机器上 docker load,结果 docker image inspect 显示出来还是 amd64load 的时候 Docker 根本没做架构匹配,它只负责把 layers 解出来塞进本地存储。
更坑的是,如果你用 docker pull 拉一个多架构镜像,Docker 会根据客户端所在机器的架构去选对应 manifest。问题在于客户端和真正跑容器的 daemon 可能不是同一台CI 机器是 x86,ARM 服务器上的 daemon 是 aarch64,pull 的时候按 x86 选了一个 amd64 的 manifest,再往 ARM 节点上推过去,自然对不上。
验证方法很直接,列出这个镜像到底带几个平台:
docker buildx imagetools inspect 你的仓库/app:1.0
正常应该输出 Manifests: 2,下面分别是 linux/amd64 和 linux/arm64。如果只有一个,多半是构建流程里少了 --push,或者中间某个 registry 代理把 manifest list 拍平了。

原因三:依赖需要原生编译,ARM 上没有现成的二进制
前两个原因解决之后,我以为完事了,结果又撞上一个:某个 Python 依赖在启动时报 Illegal instruction,还有一个 Node 项目直接构建失败。
这类问题的共性是:镜像架构对了,但里面某个第三方库没有 arm64 的预编译产物。常见的有这么几种:
1. Python 里有 C 扩展的包(cryptography、numpy、lxml、grpcio 这类),pip 在 ARM 上找不到对应的 wheel,就会退回源码编译。源码编译本身不是问题,但 ARM 机器上经常缺 gcc、python3-dev、libffi-dev,于是构建直接失败。
2. Node 的原生模块(node-gyp 那一套),需要 node-gyp、python3、make、g++ 齐全,缺一个就 fail。
3. 少数老库压根就没做 aarch64 支持,源码里有 x86 专属的汇编(比如 cpuid 指令),编出来一跑就 Illegal instruction。这种只能换实现,或者用 QEMU 模拟跑 x86 版本。
我的处理办法是在 Dockerfile 里补齐编译链,做成一个多阶段构建的 builder 阶段,最后只把产物拷进精简的运行镜像:
FROM --platform=linux/arm64 python:3.11-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3-dev && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
另外提醒一句:国产化环境下包管理器可能是 yum/dnf 而不是 apt,麒麟、统信这些系统还有自己的包名差异。我那次在麒麟机器上装 openssl-devel 就是踩了坑,国产系统里叫 openssl-devel 或 libssl-dev,得按目标系统单独写一份安装脚本,别试图用一份 Dockerfile 打天下。
顺带说说 QEMU 模拟:能跑,但别当生产方案
实在没有 ARM 硬件时,可以用 QEMU 用户态模拟跑 x86 镜像:
docker run --platform linux/amd64 -e QEMU_EMULATE=1 你的镜像
配合 tonistiigi/binfmt 那个镜像注册一下 binfmt_misc,Docker 就能自动识别。这种方式适合临时验证,别用于生产模拟执行的性能损失通常在 5 到 20 倍,一个 Go 服务的启动时间能从 200 毫秒变成 3 秒都是常事。我拿它跑过一次完整的构建流程,纯粹是救急,真上线还是得在真 ARM 机器上跑一遍。

最后留一份排查清单
整个过程我整理成了四步,下次再遇到类似问题可以按顺序走:
1. uname -m 确认目标机器架构,别信控制台标签。
2. docker image inspect --format '{{.Architecture}}' 确认镜像架构。
3. docker buildx imagetools inspect 确认 manifest list 是否完整。
4. 报错是 Illegal instruction 就往原生依赖方向查,报错是 Exec format error 就往镜像架构方向查,两者定位完全不同,别混着浪费时间。
整个迁移从踩坑到跑通大概花了三天,其中一半时间在排查那个 Illegal instruction 上。现在想想,最开始真该做的一步就是先把架构这件事写进 CI 检查项里每次构建完自动 inspect 一下架构标签是否匹配目标平台,能省下后面大半天试错。
如果你也在做这类跨架构迁移,多架构基础镜像的选择、目标机器的机型配置、以及能不能直接上物理机做验证,这三件事建议一起考虑,别等迁移卡住了再回头换机器。需要远程登录多台 ARM 服务器做验证的话,lightlayer(独立物理服务器 / 多种机型与配置) 这类可以按预算选机型的方案也可以纳入比较。


喜欢
顶
难过
囧
围观
无聊





