多彩编程 多彩编程MZPH · CODE BLOG
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:alpineDocker 会自动去 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.v2json 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.v2json, 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 覆盖掉。所以为了方便区分建议拉完之后立刻重新打 tagdocker tag minio/minio minio/minio:arm64如果你用的 Docker Desktop 或 Docker 引擎版本较新也可以设置环境变量 DOCKER_DEFAULT_PLATFORM 来默认指定平台这样后续所有 docker pull 和 docker run 都会自动带上平台参数。比如在 Linux 服务器上临时切换默认平台export DOCKER_DEFAULT_PLATFORMlinux/arm64这个环境变量在 Docker Desktop 4.x 以上的 GUI 设置里也有对应选项但要注意它会影响所有后续命令用完记得取消环境变量或重启 Docker不然容易出现“下一件小事忘记改回来”的尴尬。3.2 第二步验证镜像确实是目标架构拉完镜像之后验证架构这一关绝对不能省略。docker pull 成功不代表你拉到的就是想要的架构尤其是同一台机器来回拉过多个架构的时候镜像名和 tag 一样时极易混淆。验证命令非常轻量docker inspect --format {{.Os}}/{{.Architecture}} minio/minio:latest我的实际环境里输出类似linux/arm64这里的 Os 表示操作系统linuxArchitecture 表示 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 参数指定输出文件名。如果你不写 -odocker 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.2GBgzip 压缩后约 480MB传输时间差距非常明显。如果你追求更高压缩比可以用 zstddocker save minio/minio:arm64 | zstd -o minio-arm64.tar.zstdocker 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 的镜像却指定了 arm64no 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 wasnt detected” 的错这是虚拟化层没开启。Docker Desktop 依赖 Windows 的虚拟化技术来运行 Linux 内核通常是 WSL2 或 Hyper-V 后端。排查步骤是先打开任务管理器切到“性能”页签看 CPU 条目下有没有“虚拟化已启用”。如果显示未启用需要进 BIOS/UEFI 开启 Intel VT-xIntel 平台或 AMD-VAMD 平台不同主板厂商的 BIOS 菜单位置不一样但关键字一般是 VT-x、AMD-V、SVM、Virtualization 之类。开启后重启系统再看任务管理器确认。如果是 Windows 11 用户还要确认 WSL2 功能已经启用。管理员权限打开 PowerShellwsl --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 binfmtno 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启用 WSL2load 之后镜像名为save 时用了镜像 ID 而非 name:tag重新打 tag 再 saveload 文件格式不支持tar 包损坏或格式非标准检查压缩格式确保用 docker save 而非 docker export5. 进阶多架构构建与批量离线归档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 版本并手动打了不同的 tagdocker 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。只要目标机器能连通内网 Registrydocker pull 就能直接拉取省去拷贝 tar 包的痛苦。这个方法在封闭网络里比 tar 转发更灵活缺点是要维护一套 Registry 服务适合长期稳定的大规模交付场景。最后再分享一个个人的操作心得。我在做离线交付时最常被坑的其实不是 save 和 load 本身而是最开始没有把“目标机器是什么架构”这件事钉死。你在一台设备上部署成功不代表同一套镜像在另一台同型号设备上也能跑因为同型号设备的固件更新也可能改变系统架构的呈现方式。所以我现在养成了一个习惯每到一个新环境先 uname -m再 docker info 看 Architecture确认之后才决定执行什么平台参数的命令。另外docker save 出来的 tar 包我总会顺手生成一份 sha256 校验文件同时把拉取平台的参数记录在交付说明里。这样即使半年后这个包被翻出来再部署也不需要重新分析就能知道这个包里装的是什么架构的镜像、怎么用、校验值是多少。这个习惯看着小但在多人协作的交付项目里真的能省掉很多说不清道不明的麻烦。
返回列表