拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

跨架构离线交付:Docker pull 指定架构镜像保存与加载实践

跨架构离线交付:Docker pull 指定架构镜像保存与加载实践

在做离线交付和跨架构部署时,Docker 拉取指定架构镜像再保存到本地,是绕不开的一步。很多人刚开始接触这个操作时,以为 docker pull 拉下来的镜像就一定能在目标机器上跑,结果 docker save 打包带过去,一运行就报 exec format error,或者干脆提示 no matching manifest for linux/arm64。这个问题的根源,就是拉镜像时没有指定目标机器的 CPU 架构,镜像层和运行环境不匹配。这个场景在国产化适配、嵌入式设备、边缘网关、树莓派项目里特别常见,比如给 ARM 设备准备 minio、mysql、nginx 等镜像,或者像 senaite 这类没有官方离线安装包的软件,必须自己用 docker pull 拉取对应架构版本,再通过 docker save 生成 tar 文件。下面我会把整个操作的原理、环境准备、四步实操、典型踩坑和进阶玩法完整拆开讲清楚。

1. 先搞清楚:为什么要手动指定架构拉镜像

1.1 一个镜像背后可能藏着多个架构

Docker 镜像和 CPU 架构是强绑定的。x86_64 机器跑的是 amd64 指令集,ARM 机器跑的是 arm64/aarch64 指令集,它们的二进制文件不能互相执行。这个道理就像你从网上下载的安装包,Windows 版不能装在 Linux 上一样,虽然镜像里装的都是同一个软件,但底层的可执行文件编译目标不同。

现代 Docker Registry 引入了一个叫 manifest list(清单列表)的机制,也叫多架构镜像索引。简单说,同一个镜像名和标签下面,可以挂多个不同架构的 manifest,每个 manifest 指向一组独立的镜像层。你在 x86 的机器上执行 docker pull nginx:alpine,Docker 会自动去 manifest list 里找出 amd64 对应的那组镜像层来拉取;在 ARM 的机器上执行同样的命令,它就会去拉 arm64 的那组。整个过程对用户透明,就像同一个商品在仓库里有不同规格的包装,快递员会根据收货地址自动发对应规格的货。

但问题就出在这个“自动选择”上。默认情况下,Docker 会按照当前运行环境的架构去拉镜像,而不是按照你最终目标机器的架构。所以当你在一台 x86 开发机上为树莓派准备镜像时,如果不加任何参数,拉下来的就是 amd64 版本,打包带到 ARM 设备上自然跑不起来。

1.2 哪些场景必须明确指定架构

根据我实际的工作经验,下面几个场景不做架构指定就一定会出问题:

  • 交叉环境离线交付:开发机是 x86,目标部署设备是 ARM 架构的嵌入式平台或边缘网关。这是最常见的情况,也是 docker pull 指定架构这个需求的核心来源。
  • Apple Silicon Mac 开发:M1/M2/M3 Mac 上跑的是 arm64 架构 Linux 虚拟机,但有些软件只发布了 amd64 版本的镜像。比如某些旧版的中间件、特定版本的数据库驱动,在 Apple Silicon 上直接拉会拿到 arm64 版本,如果镜像没有提供 arm64 版本,就需要手动指定 amd64 拉取,配合 QEMU 模拟运行。
  • 没有现成下载地址的软件离线包:很多开源软件没有提供直接的二进制安装包或离线压缩包,官方只提供 Docker 镜像。比如 senaite 这个实验室信息管理系统,想部署到内网环境就得先在线拉取镜像,再 docker save 成 tar 文件离线搬运。这个场景下必须确认目标环境是什么架构,拉错了等于白拉。
  • CI/CD 多架构交付:流水线在 x86 构建机上产出 arm64 镜像,或者反过来,在 ARM 构建机上产出 amd64 镜像。这要求构建和拉取环节都要明确使用 --platform 参数。

如果你不指定架构,拉下来一个 amd64 镜像,在 ARM 机器上跑就会报 exec format error,这是所有跨架构部署新手都会遇到的第一个大坑。

2. 动手前的环境检查与基础配置

2.1 先确认你的 Docker 版本够不够

指定架构拉镜像依赖两个能力:一是 Docker CLI 支持 --platform 参数,二是镜像仓库的服务端支持 manifest list。Docker 20.10 及以上版本都完整支持这两个特性,建议使用更新版本。太老的版本可能会提示 unknown flag: --platform,那就需要先升级 Docker。

执行 docker version 看一下 Client 和 Server 的版本。注意这里的 Server 版本才是真正执行拉取逻辑的组件,如果 Server 版本太老,即使 Client 是新的也会出问题。Docker Desktop 用户一般不需要太担心,它会随着桌面端自动更新;Linux 上手动安装的 Docker Engine 需要留意一下版本号。

另外还要注意一个容易混淆的点:Docker Client 支持 --platform 不代表 Docker Server 一定能运行非本机架构的容器。拉取可以成功,但运行可能失败,因为运行需要 QEMU 用户态模拟或者对应架构的真实硬件支持。这个我在第四章会详细说。

2.2 先检查目标镜像支持哪些架构

在拉镜像之前,强烈建议先确认目标镜像到底有没有提供你要的架构版本,否则命令执行到一半才报错就浪费时间了。

优先推荐使用 buildx 的 imagetools 子命令,输出非常直观:

docker buildx imagetools inspect minio/minio

你会看到类似这样的输出,其中 Platforms 列表就列出了所有可用的架构:

Name: docker.io/minio/minio:latest MediaType: application/vnd.docker.distribution.manifest.list.v2+json Digest: sha256:xxx Platforms: linux/amd64 linux/arm64 linux/ppc64le linux/s390x

如果你用的是旧版 Docker 没有 buildx,也可以用 docker manifest inspect 命令,它会输出 JSON 格式的 manifest 信息,文件比较长,你需要从中找平台相关的字段:

docker manifest inspect nginx:alpine

输出里会有 platform 字段,标明了 os、architecture 和 variant。比如:

{ "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "platform": { "architecture": "arm64", "os": "linux", "variant": "v8" } }

检查的意义在于提前止损。很多官方镜像(比如 nginx、redis、mysql、minio)都已经做了多架构发布,但也有一些老软件或者小众软件只发布了 amd64,这时候你就得评估替代方案,比如用源码构建或者找其他镜像源。切忌连查都不查就闷头拉,拉完保存带过去了才发现没有目标架构版本,来回折腾的沟通成本非常高。

2.3 拉取速度不理想?先检查镜像加速配置

跨架构拉取本身不会让速度变快或变慢,但因为要拉取多层数据,网络波动对体验影响很大。如果你在内网环境拉 Docker Hub 官方镜像时经常超时、连接中断,建议先配置好镜像加速器。国内云厂商提供的镜像加速地址是合规的公共基础设施,常见的有阿里云容器镜像服务加速器、腾讯云加速器等,配置方式是在 /etc/docker/daemon.json 里加 registry-mirrors:

{ "registry-mirrors": [ "https://your-id.mirror.aliyuncs.com" ] }

改完之后重启 Docker 生效:

sudo systemctl daemon-reload sudo systemctl restart docker

需要说明的是,加速器只对 Docker Hub 官方仓库的公共镜像有效,如果你拉的是自建私有仓库或者第三方独立仓库,加速器是不起作用的,该走的流量一点都不会少。另外,加速器拉回来的镜像本质上还是原仓库的镜像,不会因为走了加速隧道就改变镜像的架构信息,这一点可以放心。

3. 核心四步实操:拉取、验证、保存、加载

3.1 第一步:用 --platform 参数拉取目标架构镜像

拉取指定架构最直接的方式是在 docker pull 命令后面加 --platform 参数。语法格式是操作系统/架构,常见的组合有 linux/amd64、linux/arm64、linux/arm/v7、linux/arm/v6、linux/ppc64le、linux/s390x 等。

拿我实际做过的一个例子来说,我需要给一台 ARM64 的嵌入式网关准备 MinIO 对象存储镜像,具体命令是:

docker pull --platform linux/arm64 minio/minio

执行之后 Docker 会去 manifest list 中匹配 arm64 对应的 digest,然后拉取这组镜像层。注意,拉下来的镜像 tag 仍然是 minio/minio:latest,不会自动在名字里标注 arm64。如果你在同一台机器上先拉 amd64 再拉 arm64,后拉的会把相同 tag 覆盖掉。所以为了方便区分,建议拉完之后立刻重新打 tag:

docker tag minio/minio minio/minio:arm64

如果你用的 Docker Desktop 或 Docker 引擎版本较新,也可以设置环境变量 DOCKER_DEFAULT_PLATFORM 来默认指定平台,这样后续所有 docker pull 和 docker run 都会自动带上平台参数。比如在 Linux 服务器上临时切换默认平台:

export DOCKER_DEFAULT_PLATFORM=linux/arm64

这个环境变量在 Docker Desktop 4.x 以上的 GUI 设置里也有对应选项,但要注意它会影响所有后续命令,用完记得取消环境变量或重启 Docker,不然容易出现“下一件小事忘记改回来”的尴尬。

3.2 第二步:验证镜像确实是目标架构

拉完镜像之后,验证架构这一关绝对不能省略。docker pull 成功不代表你拉到的就是想要的架构,尤其是同一台机器来回拉过多个架构的时候,镜像名和 tag 一样时极易混淆。

验证命令非常轻量:

docker inspect --format '{{.Os}}/{{.Architecture}}' minio/minio:latest

我的实际环境里,输出类似:

linux/arm64

这里的 Os 表示操作系统(linux),Architecture 表示 CPU 架构(arm64)。如果你发现输出是 linux/amd64,而你需要的是 arm64,说明你刚才的命令没有生效,或者镜像被覆盖了,需要重新拉取。

更严谨一点,还可以通过 config 信息进一步确认:

docker image inspect --format '{{json .Config}}' minio/minio:latest | python3 -m json.tool

不过对于绝大多数场景,Os 和 Architecture 两个字段就足够了。我一般在打完 docker tag 之后,会顺手跑一条 docker inspect 加 grep 的组合命令,把这一步固化到自己的操作习惯里,避免后面交付时才发现问题。

3.3 第三步:用 docker save 保存为 tar 包

镜像验证完毕之后,就可以生成离线文件了。docker save 命令的作用是把一个或多个镜像完整导出成 tar 归档文件,包含镜像的分层数据、配置元数据、tag 信息,是离线分发镜像的标准做法。

基本用法:

docker save -o minio-arm64.tar minio/minio:arm64

-o 参数指定输出文件名。如果你不写 -o,docker save 会把 tarball 直接输出到标准输出,这时你可以配合管道进行压缩:

docker save minio/minio:arm64 | gzip > minio-arm64.tar.gz

这里有一个容易混淆的概念必须说清楚:docker save 和 docker export 是不一样的。docker save 导出的是镜像,docker export 导出的是容器运行时的文件系统。用 docker export 打出来的 tar 包,再用 docker import 导入后,会丢失镜像的分层历史、环境变量、默认命令等元数据,导入之后并不是一个完整的镜像。所以离线分发请老老实实用 docker save 和 docker load 组合,不要图方便拿 export 顶替。

对于体积很大的镜像,建议边导出边压缩。我用一个实际镜像测过,原 tar 包 1.2GB,gzip 压缩后约 480MB,传输时间差距非常明显。如果你追求更高压缩比,可以用 zstd:

docker save minio/minio:arm64 | zstd -o minio-arm64.tar.zst

docker load 官方支持加载 .tar、.tar.gz、.tar.zst 等格式,不过引入 zstd 就意味着目标机器上需要安装 zstd 工具,所以现场执行 load 时要注意环境里有没有对应解压工具,否则建议直接用 gzip 格式最通用。

3.4 第四步:在目标机器上加载并运行验证

把 tar 包拷贝到目标机器上之后,加载命令很简单:

docker load -i minio-arm64.tar

加载完成后,docker load 会打印镜像名和 tag(如果 save 时有的话),比如:

Loaded image: minio/minio:arm64

如果加载后提示镜像名不对,或者变成了一个很长的 digest 编号,多半是你用 docker save 导出时就没有把 tag 一起带上,或者用了某种工具对 tar 内的 manifest 做了改写。对此最简单的规避方式是:保存前先确认 docker images 里的 REPOSITORY 和 TAG 都已经打好,再执行 docker save。

加载完成后,在目标机器上跑一下这个镜像,确认可以正常启动:

docker run --rm minio/minio:arm64 --version

如果是 arm64 版本镜像在 arm64 机器上运行,不会有任何问题。如果是在 x86 机器上,没有配置 QEMU 就会报 exec format error;配置了 QEMU 则能跑起来,但性能会打折扣。这一点到第四章细聊。

4. 保存和加载过程中的典型坑与排查

4.1 exec format error:架构不匹配的经典报错

很多人在离线环境中会碰到这样的场景:明明 docker load 已经成功加载了镜像,docker images 也能看到它,但 docker run 一执行就报:

exec format error

这个报错翻译过来就是“这个二进制文件的格式当前系统不认识”,本质上是你在 x86 机器上跑 amd64 镜像没问题,但跑 arm64 镜像,Linux 内核发现这个可执行文件的 ELF 头是 AArch64 格式,而当前 CPU 不是 ARM 架构,直接拒绝执行。

出现这个报错时,先用 docker inspect 确认镜像架构,再看一下当前机器架构:

uname -m

如果输出是 x86_64 而镜像架构是 arm64,基本就是架构不匹配。解决思路有两个:第一,换到真实架构匹配的机器上运行;第二,在 x86 机器上安装 QEMU 用户态模拟,让内核能通过 binfmt_misc 机制动态翻译执行 arm64 指令。

4.2 no matching manifest for linux/arm64:目标镜像没有该架构版本

这个报错出现在 docker pull --platform 指定了不支持的架构时,比如你想拉一个只发布了 amd64 和 arm/v7 的镜像,却指定了 arm64:

no matching manifest for linux/arm64 in the manifest list entries

遇到这种情况,别急着抱怨镜像源,先用 docker buildx imagetools inspect 看看镜像本身支持哪些平台(参考 2.2 节)。如果确实没有你要的架构,方案就变成:

  • 找官方是否提供了其他 tag 或变体版本,比如有些镜像会提供 -arm64 后缀的独立 tag。
  • 使用 buildx 基于源码自己构建目标架构镜像(第五章会讲)。
  • 找第三方构建的多架构镜像替代,但要评估可信度和供应链风险。

4.3 Docker Desktop 启动失败:virtualization support 未检测到

在 Windows 上使用 Docker Desktop 时,如果报类似 “Docker Desktop failed to start because virtualisation support wasn't detected” 的错,这是虚拟化层没开启。Docker Desktop 依赖 Windows 的虚拟化技术来运行 Linux 内核,通常是 WSL2 或 Hyper-V 后端。

排查步骤是:先打开任务管理器,切到“性能”页签,看 CPU 条目下有没有“虚拟化:已启用”。如果显示未启用,需要进 BIOS/UEFI 开启 Intel VT-x(Intel 平台)或 AMD-V(AMD 平台),不同主板厂商的 BIOS 菜单位置不一样,但关键字一般是 VT-x、AMD-V、SVM、Virtualization 之类。开启后重启系统再看任务管理器确认。

如果是 Windows 11 用户,还要确认 WSL2 功能已经启用。管理员权限打开 PowerShell:

wsl --status

如果没有安装或版本过旧,先执行 wsl --update,再在“启用或关闭 Windows 功能”里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”,重启后 Docker Desktop 一般就能正常启动。

4.4 保存的 tar 包加载后找不到镜像或 tag 错乱

这个问题我踩过不止一次。docker save 可以一次导出多个镜像:

docker save -o all.tar nginx:alpine redis:7 minio/minio:arm64

对应的 docker load 会把其中所有镜像都加载进来,docker load 的屏幕输出会列出每个镜像的 name:tag。但有一个很隐蔽的问题:如果你 save 的时候写的是镜像 ID 而不是 name:tag,加载回来的镜像就会变成 : ,在 docker images 里看不到正常名字。

所以保存前一定养成先 docker images 看清楚,再 docker save 的习惯。更稳一点,导出后用下面的命令查看 tar 内容:

tar -tf all.tar | head

在生成的 manifest.json 和 repositories 文件里能看到镜像名和 tag 信息。如果发现不对劲,就重新打 tag 再导出,不要等到目标机器上 load 完再反过来折腾。

4.5 常见问题速查表

报错或现象可能原因排查与解决
exec format error镜像架构与运行环境不匹配用 uname -m 和 docker inspect 确认架构;换机器或启用 QEMU binfmt
no matching manifest for linux/arm64镜像没有提供对应架构版本用 buildx imagetools inspect 查看支持列表;换 tag 或自建镜像
docker pull 提示 unknown flag: --platformDocker 版本过旧升级 Docker 到 20.10 以上
Docker Desktop 提示 virtualization support 未检测到Windows 虚拟化未开启BIOS 开启 VT-x/AMD-V,启用 WSL2
load 之后镜像名为save 时用了镜像 ID 而非 name:tag重新打 tag 再 save
load 文件格式不支持tar 包损坏或格式非标准检查压缩格式;确保用 docker save 而非 docker export

5. 进阶:多架构构建与批量离线归档

5.1 用 buildx 一键构建多架构镜像

除了拉取现成的多架构镜像,还有一种常见需求是自己构建多架构镜像。比如你维护了一个内部工具,需要同时交付 arm64 和 amd64 两个版本,可以用 Docker Buildx 基于 Dockerfile 一次构建多平台镜像。

先创建一个支持多架构的 builder 实例:

docker buildx create --name multiarch --driver docker-container --bootstrap docker buildx use multiarch

然后构建并推送:

docker buildx build --platform linux/amd64,linux/arm64 -t yourname/yourimage:1.0.0 --push .

这个命令会同时在两个架构下跑构建,并把两个架构的 manifest 合并推送到仓库,之后用户拉取时就能自动按架构匹配。buildx 底层依赖 QEMU 做交叉架构的模拟,所以构建机上实际上在做的是模拟一个 arm64 环境来跑构建。如果你的构建镜像里涉及非常底层的原生编译步骤,模拟构建有时会踩坑,这时候建议直接放到对应的 ARM 构建机上跑,或者用交叉编译工具链替代。

buildx 还提供了一个很实用的小功能:把多个不同架构的已有镜像合并成一个多架构 manifest。比如你分别用 docker pull 拉到了 redis 的 amd64 和 arm64 版本,并手动打了不同的 tag:

docker buildx imagetools create \ -t yourname/redis:merged \ redis:amdx64-tag \ redis:arm64-tag

合并之后,你相当于生成了一份本地 manifest list,但注意这只是在本地创建了索引,镜像层并没有改动,目标机器要能解析这个索引,依赖 Docker 版本支持 OCI 索引格式,一般 20.10 以上问题不大。

5.2 批量拉取并保存多个镜像的脚本

如果一次要准备很多个 arm64 镜像离线包,手动一条条执行 docker pull、docker tag、docker save 太容易漏。这里给出一个我在离线交付时常用的脚本模板,你可以按需修改:

#!/usr/bin/env bash set -euo pipefail PLATFORM="${PLATFORM:-linux/arm64}" OUTPUT_DIR="$(pwd)/images" mkdir -p "$OUTPUT_DIR" IMAGES=( "nginx:alpine" "minio/minio:latest" "redis:7" "mysql:8.0" ) for image in "${IMAGES[@]}"; do echo "==> Pulling $image ($PLATFORM)" docker pull --platform "$PLATFORM" "$image" raw_name="${image%:*}" tag="${image#*:}" safe_name=$(echo "$raw_name" | tr '/' '_') archive="$OUTPUT_DIR/${safe_name}_${tag}_${PLATFORM##*/}.tar.gz" echo "==> Saving to $archive" docker save "$image" | gzip > "$archive" sha256sum "$archive" >> "$OUTPUT_DIR/SHA256SUMS" done echo "All done. Files stored in $OUTPUT_DIR"

这段脚本做的事情是:循环拉取指定平台镜像,导出成按镜像名和架构命名的 gzip 压缩包,同时生成 SHA256 校验文件。文件名里带上架构后缀可以避免多个架构的包混在一起分不清。目标机器上先执行 sha256sum -c 校验完整性,再逐一 docker load -i 即可。

有一个细节值得注意:脚本里 docker pull 和 docker save 之间不要省略 docker tag。如果你没打架构标签,第二次循环遇到同名不同 tag 的镜像时,save 的时候要确保引用的是正确的那一个。我在脚本里直接用原始 image 名来 save,前提是同一个环境里不会出现两个架构的同 tag 镜像,如果会,建议在循环里先打上架构 tag,再 save 那个新 tag。

5.3 Docker 原生方案与 skopeo 的边界

Docker 原生的 pull + save 流程简单直观,但有一个不太方便的点:它必须先把镜像放到 Docker 本地的存储驱动里,然后再导出,中间会占用大量磁盘空间。如果你的离线包有好几个 GB,本地磁盘紧张,可以使用 skopeo 这个工具替代,它可以直接把镜像从 Registry 复制到本地目录,不经过 Docker daemon,也不占用容器存储池。

skopeo copy --override-arch arm64 \ docker://docker.io/minio/minio:latest \ docker-archive:minio-arm64.tar:minio/minio:arm64

这个命令直接把 arm64 架构的 minio 镜像复制成本地 tar 包,速度更快,磁盘占用也更可控。skopeo 还可以在镜像仓库之间直接复制(比如从源仓库同步到私有仓库),是运维级离线同步的利器。如果你只是临时拉一两个镜像,要求不高,用 Docker 原生方案完全足够;如果你要做一次几十个镜像的离线仓库同步,建议把 skopeo 加进工具链。

我这里再补充一个实际项目中的做法:离线交付时,镜像除了打成 tar 包,也可以推送到一个内网自建的私有 Registry,比如用 docker/distribution 镜像搭一个最简单的 Registry。只要目标机器能连通内网 Registry,docker pull 就能直接拉取,省去拷贝 tar 包的痛苦。这个方法在封闭网络里比 tar 转发更灵活,缺点是要维护一套 Registry 服务,适合长期稳定的大规模交付场景。


最后再分享一个个人的操作心得。我在做离线交付时,最常被坑的其实不是 save 和 load 本身,而是最开始没有把“目标机器是什么架构”这件事钉死。你在一台设备上部署成功,不代表同一套镜像在另一台同型号设备上也能跑,因为同型号设备的固件更新也可能改变系统架构的呈现方式。所以我现在养成了一个习惯:每到一个新环境,先 uname -m,再 docker info 看 Architecture,确认之后才决定执行什么平台参数的命令。另外,docker save 出来的 tar 包,我总会顺手生成一份 sha256 校验文件,同时把拉取平台的参数记录在交付说明里。这样即使半年后这个包被翻出来再部署,也不需要重新分析就能知道这个包里装的是什么架构的镜像、怎么用、校验值是多少。这个习惯看着小,但在多人协作的交付项目里,真的能省掉很多说不清道不明的麻烦。

返回列表