arm64 openEuler 离线安装 Docker:一键部署与避坑指南 简介面向arm64平台如鲲鹏、飞腾与openEuler操作系统的运维工程师、容器平台实施人员及国产化项目交付团队这是一份经过实际验证的Docker与Docker Compose离线安装资源包主要用于解决内网隔离、无外网环境下容器运行环境搭建难、依赖组件获取不便的问题。资源共4个文件整体压缩包仅54.41MB里面包含docker 18.09.9离线镜像tgz包、用于托管Docker服务进程的service文件、封装了解压注册与启动流程的一键安装shell脚本以及可直接调用的docker-compose独立二进制文件上传解压即可执行无需额外联网拉取。该方案已在openEuler操作系统上实测通过脚本逻辑简单清晰同时配有compose工具拷贝、权限设置和版本校验的辅助说明能有效规避默认仓库不可用、内核兼容性差、服务无法自启等离线安装常见问题方便在arm64服务器上快速复制。目前已有645人学习下载适合需要批量初始化容器主机或搭建基础微服务运行环境的读者直接参考使用。1. arm64 平台上的 docker 离线安装别再把时间花在没网的环境里现场一台 arm64 架构的 openEuler 服务器网线插着但业务网络禁外联yum 源根本同步不到。这时候想装 docker第一反应是找个 docker 安装教程抄一遍结果第一步curl -fsSL ... | sh就卡死。标题里的这套 arm64 离线安装包就是在这种环境里用的docker、docker-compose 和依赖 rpm 全在包里附带一键安装脚本并且已经在 openEuler 系统上完成验证。对做交付和运维的人来说它把“现场查依赖、碰运气装”变成“拷包、执行、看日志”能省下大量时间。这个方案适合两类人一类是要在没外网的 openEuler 机器上快速搭起 docker 环境的实施工程师另一类是自己要制作离线安装包、给别的项目复用的人。2. 离线安装包的内容设计依赖、版本和目录结构怎么定2.1 先分清 docker engine 和 docker-ce离线包该选哪种先解决选型。很多人一上来就说“装 docker”但真正到离线包场景必须分清几个名词docker engine 是容器运行时的核心组件不包含客户端里的商业功能docker-ce 是社区版把 daemon、client、containerd 的安装和升级路径打包好而 openEuler 软件源里虽然有 docker 相关包但通常是系统自己维护的版本更新节奏和上游不一致。离线交付里最常见的选择是 docker-ce 的 rpm 包因为它的 aarch64 包是可单独下载的依赖关系也清楚装完用 systemd 直接管理。候选方案是否建议离线包采用理由docker-ce 系列 rpm建议aarch64 包齐全docker-ce/docker-ce-cli/containerd.io 依赖清晰docker engine 独立安装包有条件用需要自己拼依赖版本选择空间较小openEuler 系统源 docker不建议离线时源不可用且版本可能与预期不符这里要强调一点docker-ce 不是一个单一大包。常见的离线最小集合至少包含 docker-ce、docker-ce-cli、containerd.io 三个核心 rpm以及它们依赖的 iptables、iptables-libs、libnfnetlink、libcgroup 等。依赖列表不是靠猜的需要在打包机上用dnf download --alldeps一次性收齐。很多人只看顶层包名不看架构结果把 x86_64 的 rpm 拷进 arm64 服务器装的时候提示架构不匹配装完界面还显示成功但 docker 一运行就崩。所以包内所有 rpm 必须保证是 aarch64 或 noarch。2.2 arm64 平台的 rpm 依赖清单与目录规划离线安装包不是把 rpm 打个 tar 就完事。目录结构要让现场的人一眼看懂也要让脚本能按固定路径去读取。我一般会这样组织arm64-docker-offline/ ├── install.sh ├── uninstall.sh ├── verify.sh ├── README.md ├── sha256sums.txt ├── rpms/ │ ├── docker-ce-*.rpm │ ├── docker-ce-cli-*.rpm │ ├── containerd.io-*.rpm │ ├── docker-compose-plugin-*.rpm │ └── docker-buildx-plugin-*.rpm ├── compose/ │ └── docker-compose # arm64 独立二进制 └── images/ ├── hello-world.tar └── busybox.tar说明一下每个目录的用途rpms 是安装时直接rpm -ivh的输入compose 里放的是独立二进制不是插件images 是可选的但如果现场完全无外网带一个 hello-world 和 busybox 能让你装完后立刻验证容器能跑。版本号不建议写进文件名里夸口因为现场系统的版本不同依赖要求也不一样。更稳妥的做法是在 README 里写清“本次验证的 openEuler 大版本、内核版本、docker 版本范围”而不是承诺所有版本通用。那么这些 rpm 是从哪来的常见做法是准备一台和目标现场同架构、同大版本的联网 openEuler 机器配置好 docker-ce 的安装源后用 dnf 把包和依赖一并下载下来。命令大致是这样dnf install -y yum-utils yum-config-manager --add-repo docker-ce安装源地址 dnf download docker-ce docker-ce-cli containerd.io \ --alldeps --destdir./rpms --resolve这里我不把具体仓库地址写死因为你用的 openEuler 大版本、你的内网镜像源都不一样。关键在参数--alldeps会把所有依赖一起下载--resolve解决依赖关系--destdir指定输出目录。下载完之后不要急着打包先看一遍 rpms 目录里的文件确认里面没有出现 x86_64。如果发现混了直接删掉重下。也可以用一条命令核对for f in rpms/*.rpm; do echo $f rpm -qp --qf %{NAME} %{ARCH}\n $f done这条命令通过rpm -qp读取 rpm 包头里的架构字段不需要安装就能看到每个包的架构。输出里如果出现 x86_64 或 i686说明打包机或下载命令选错了架构。把这个检查放在打包流程里能为你省掉现场一整晚的折腾。2.3 docker-compose 的独立二进制方案为什么不在离线环境用 docker-compose-plugin关于 docker-compose 的形态问题我在不同系统上踩过好几回。docker-compose 有两种存在形式一种是docker-compose独立二进制历史上一直这么分发另一种是docker compose插件安装在 docker 的 cli-plugins 目录下由 docker 命令调用。离线环境里我推荐前者也就是直接把官方 release 里的docker-compose-linux-aarch64二进制放到 compose 目录安装时复制到/usr/local/bin/docker-compose。原因是插件模式对版本匹配更敏感。docker-ce 插件机制要求 cli-plugins 目录下可执行文件能被 docker CLI 识别如果 docker 版本和插件版本差太多会出现docker compose报错但docker-compose正常的情况。离线现场没有网去拉新插件与其折腾版本不如用独立二进制。独立二进制自带运行所需依赖不依赖系统中的 python-docker 组件在最小化安装的 openEuler 上更稳。需要提醒的是现在很多新教程都倾向于docker compose但在我们的离线脚本里我统一用docker-compose调用。这不代表docker compose不能用而是为了避免现场人员因命令差异产生困惑。如果你后续要在一个脚本里同时兼容可以在 install.sh 里做一个符号链接把独立二进制放到/usr/local/lib/docker/cli-plugins/docker-compose但这条路径存在目录不存在和权限问题反而增加出错面。所以离线包默认不做这个映射。2.4 用文件清单和校验和把包做“可核对”离线包交付不是面子工程客户机房的运维会要求核对文件完整性和来源。给每个文件算 sha256 是最低成本的交付动作。我通常会在打包机上执行cd arm64-docker-offline find . -type f -not -name sha256sums.txt -print0 | xargs -0 sha256sum sha256sums.txt这条命令的逻辑find列出包内所有常规文件xargs -0处理带空格的文件名重定向到sha256sums.txt。注意这里把 sha256sums.txt 本身排除了避免自引用。到现场后执行sha256sum -c sha256sums.txt如果输出都是 OK再继续装如果有 FAIL那说明包在拷贝过程中损坏了别硬装先解决传输问题。这里再补充一个容易被漏掉的细节U 盘拷贝本身也可能损坏。尤其是一次拷几十个 rpm 的包不要只看文件大小对不对一定要依赖 sha256 校验。我见过现场安装失败的最后定位是 U 盘坏块导致 containerd.io 的 rpm 数据错误rpm 安装时偶尔成功偶尔失败这就是典型的黑匣子问题。把校验做成脚本的第一步能让你省掉大量排查时间。校验和文件还应该附在交付邮件里让客户在解压前先做一次核对这一步对后期扯皮非常有帮助。3. 一键安装脚本的编写思路从环境检测到 systemd 接管3.1 install.sh 的分段设计解析参数、锁定路径一键安装脚本不能是“把命令一行行贴上去”的堆砌。我一般把它拆成四段环境预检、rpm 安装、compose 放置、服务启动。先看重合执行的问题客户现场可能会出现误操作重复执行安装脚本如果脚本不幂等第二遍就会把系统弄坏。因此脚本开头就要设置健壮性标志并记录日志。#!/usr/bin/env bash set -Eeuo pipefail BASE_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) LOG_FILE/var/log/docker-offline-install-$(date %Y%m%d_%H%M%S).log exec (tee -a $LOG_FILE) 21 log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* } trap log 安装中断请检查 $LOG_FILE; exit 1 ERR usage() { cat EOF 用法: $0 [--compose] [--no-images] --compose 额外安装 docker-compose 独立二进制 --no-images 跳过导入 images 目录下的 tar 镜像 EOF exit 1 } COMPOSE0 LOAD_IMAGES1 ARCH$(uname -m) OS_ID$(. /etc/os-release echo $ID)这里的关键参数说明set -Eeuo pipefail让脚本在遇到任何未处理错误时立刻退出避免“前面失败后面继续跑”的假成功BASE_DIR从脚本自身路径解析这样脚本不依赖你从哪个目录调用ARCH和OS_ID直接在脚本头部读取供后续预检使用。日志用exec (tee -a ...)同时输出到终端和文件现场出问题时能追日志。trap ... ERR配合set -e可以在任意一条命令失败时打印中断位置这个习惯建议长期保留。参数解析部分不要做得太花哨。最简单的 while 循环就够但要把参数默认值写出来让脚本行为可预期。比如--compose默认不装如果现场需要 docker-compose显式传参--no-images默认情况下会扫描 images 目录有 tar 就导入。这种设计让包在“最小安装”和“完整安装”之间可以切换也方便后面接上其他参数比如--skip-check跳过耗时较长的校验。3.2 环境预检os、arch、内核模块、端口占用预检是脚本的第一道防线。很多安装失败其实就是环境不对但报错信息被埋没在 rpm 输出里。所以我会写一个显式的检查函数check_env() { if [[ $(id -u) -ne 0 ]]; then log 请使用 root 用户或 sudo 执行否则无法写入 /usr/local/bin exit 1 fi if [[ $ARCH ! aarch64 $ARCH ! arm64 ]]; then log 当前架构是 $ARCH不是 arm64/aarch64安装包不适用 exit 1 fi if [[ $OS_ID ! openEuler ]]; then log 警告当前系统是 $OS_ID不是 openEuler脚本未在此系统验证过请自行确认兼容性 fi if ! systemctl is-system-running /dev/null 21; then log 警告systemd 状态异常可能影响后续 docker 服务启动 fi if command -v getenforce /dev/null 21 [[ $(getenforce) Enforcing ]]; then log 提示SELinux 处于 Enforcingdocker 需要容器运行时策略建议先做标记或临时切换测试 fi modprobe overlay 2/dev/null || true if ! lsmod | grep -q ^overlay; then log 警告overlay 内核模块未加载容器存储驱动可能只能退回到 vfs影响性能 fi }这里解释逻辑id -u检查管理员权限uname -m架构检查是必须的arm64 服务器有时uname -m输出 aarch64有时输出 arm64所以两个值都接受/etc/os-release里的 ID 判断是不是 openEuler如果不是就警告而不是退出因为 rpm 包在部分 CentOS/RHEL 系 aarch64 系统上也可能能装但既然标题强调 openEuler 验证过就要把“未验证”写清楚不能让用户误以为完全支持。预检还需要考虑端口和服务占用。docker 默认不监听远程端口只使用/run/docker.sock。如果系统里已经有 docker 在运行安装脚本继续执行就会和旧版冲突。常见做法是检查/run/docker.sock是否存在存在则提示先停服务或用uninstall.sh清理。如果后续你打算给 docker 开远程 API再额外检查 2375/2376 端口是否被占。这些检查写成一组函数每项失败都能在日志里看到具体原因而不是等到 rpm 报依赖冲突时再去猜。3.3 安装 rpm 包与 docker-compose 的放置逻辑rpm 安装这步网上很多脚本写的是rpm -Uvh *.rpm。但在离线重复安装场景下我建议用rpm -ivh --replacepkgs --replacefiles。区别在于-U是升级如果目标机上已经装了更高版本的 docker-ce-Uvh会拒绝降级或者产生依赖倒挂-i是安装配合--replacepkgs --replacefiles可以在包已存在、文件冲突的情况下强行保持一致状态。脚本片段如下install_rpms() { local rpm_dir$BASE_DIR/rpms if [[ ! -d $rpm_dir ]]; then log 没有找到 rpms 目录请确认离线包解压完整 exit 1 fi local rpm_files( $rpm_dir/*.rpm ) if [[ ${#rpm_files[]} -eq 0 ]]; then log rpms 目录下没有 rpm 文件 exit 1 fi log 开始安装 rpms 目录下的 rpm 包共 ${#rpm_files[]} 个文件 rpm -ivh --replacepkgs --replacefiles $rpm_dir/*.rpm systemd-sysusers 2/dev/null || true systemctl daemon-reload }注意$rpm_dir/*.rpm在数组里展开后每个文件都被当作独立参数这样即使目录中包含空格文件名也不会断裂。systemctl daemon-reload必须在所有 rpm 写入后执行否则 systemd 里可能还没有 docker.service 的 unit 文件。有些精简版 openEuler 没有 docker 用户和 docker 组rpm 安装脚本会自己创建但如果你发现安装后/etc/docker权限异常需要检查groups docker或查看/etc/group中是否存在 docker 组。这也是 rpm 安装后经常被忽略的地方。docker-compose 的放置不能和 rpm 混在一起。如果使用独立二进制安装逻辑是install_compose() { local src$BASE_DIR/compose/docker-compose if [[ ! -x $src ]]; then log compose 目录下没有可执行的 docker-compose 二进制跳过 return 0 fi install -m 0755 -o root -g root $src /usr/local/bin/docker-compose ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose 2/dev/null || true log docker-compose 已安装到 /usr/local/bin }install命令比cp好的地方在于它可以同时指定权限、属主属组。-m 0755保证可执行-o root -g root避免二进制被普通用户篡改。符号链接ln -sf是给那些 PATH 里没有/usr/local/bin的环境准备的不一定每个系统都需要但加上无害。有一点要注意不要同时又去 rpm 包里装docker-compose-plugin两个如果有冲突会出现docker-compose version拿到的版本不是你预期的。离线包里如果放了该插件 rpm可以从安装列表里排除或者干脆不放到 rpms 目录。3.4 启动 docker 与配置 daemon.json启动服务的写法看似简单但顺序很重要。很多人直接systemctl enable docker systemctl start docker如果包刚装上这样做没问题但重复执行时如果 docker 已经在运行start 会直接成功不会重新加载你刚才写入的 daemon.json。所以正确的做法是先写 daemon.json再systemctl daemon-reload再 restart。脚本片段configure_and_start_docker() { mkdir -p /etc/docker if [[ ! -f /etc/docker/daemon.json ]]; then cat /etc/docker/daemon.json EOF { log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 }, group: docker } EOF log 已写入 /etc/docker/daemon.json else log /etc/docker/daemon.json 已存在不覆盖请人工确认配置 fi systemctl daemon-reload systemctl enable docker systemctl restart docker systemctl --no-pager status docker --full || true }daemon.json里的三个字段值得说明log-driver: json-file是默认值显式写出来是为了限制日志max-size和max-file是防止容器日志无限增长把磁盘占满内网没人帮你自动清理这两个参数能救命group: docker意思是把 docker socket 的组设置成 docker 组这样加入 docker 组的普通用户就不用每次 sudo 执行 docker 命令。脚本里我刻意不覆盖已存在的 daemon.json因为现场可能存在预先配置好的镜像仓库认证信息、私有 CA 证书路径等盲目覆盖等于把别人的配置清掉这是交付大忌。如果你希望离线包统一日志配置可以把这段逻辑改成“合并配置”但合并 JSON 需要引入 jq 或 python会增加离线包依赖所以默认不覆盖更稳妥。整体上这个脚本把安装过程拆成小函数每一段只做一件事出问题时日志定位到具体函数。你不需要把它压缩成几行“炫技”命令离线的现场更看重可读性和可排查性。这也是“一键安装”背后的真正含义不是把复杂藏起来而是把复杂拆成可解释的步骤。4. 在 openEuler 下跑通验证最小命令与验证矩阵4.1 docker 服务状态与版本信息核对安装完成不等于验证完成。很多交付场景里安装脚本返回 0 就结束结果客户第二天用自己的镜像跑任务时发现 docker 起不来。所以我们需要一套可以在现场快速执行的验证命令序列。首先是最基本的服务状态和版本信息systemctl is-active docker docker --version docker version --format Server: {{.Server.Version}} / {{.Server.Os}} / {{.Server.Arch}} docker info --format Storage Driver: {{.Driver}}逻辑说明第一行检查服务是否 active第二行确认客户端可用第三行特别重要它会显示服务端架构比如linux/arm64这就直接证明了当前 docker 服务是真的 arm64 版本而不是在 x86 兼容层里跑第四行看存储驱动是 overlay2 还是 vfs。输出中如果 Driver 是 overlay2说明内核 overlay 模块和 Docker 的配置是对的如果显示 vfs说明你的内核模块加载有问题容器能跑但性能会差很多属于可以用的“勉强状态”。docker-compose 的验证要区分命令形式docker-compose --version command -v docker-compose file /usr/local/bin/docker-composecommand -v确认它在你当前 shell 的 PATH 里file确认二进制格式是 aarch64。如果file输出里有 “x86-64” 字样那就说明拷错文件了即使docker-compose --version能打出版本也只是在 aarch64 系统上以兼容模式跑极不稳定。这个问题需要在打包机上就拦截掉而不是等到现场。4.2 用 hello-world 与 busybox 验证容器全流程光看版本不能证明容器能创建、网络和存储都正常。最直接的做法是跑一次 hello-world 镜像。如果离线包中的 images 目录里已经存了 tar顺序应该是先 load 再 rundocker load -i $BASE_DIR/images/hello-world.tar docker run --rm hello-world这里有个容易忽略的点docker run --rm hello-world输出的是镜像内二进制向标准输出打印的一段文字它本身不依赖外部网络。如果在无网环境下跑半天没反应多半是 load 没有成功或者 tar 是 x86 架构的。跑 hello-world 之前可以用docker image inspect查看架构docker image inspect hello-world --format {{.Os}}/{{.Architecture}}预期输出是linux/arm64。如果输出是linux/amd64说明你在打包机上导错了镜像。很多人在打包阶段不做这一步现场 run 起来直接报 “exec format error”再回头查镜像架构等于白跑一趟。这也是我为什么坚持在验证流程里加上docker image inspect。接着用 busybox 做更完整的验证busybox 是一个静态编译的极简工具集镜像可以测容器内部的命令执行、网络命名空间和基本回环通信。最小命令docker load -i $BASE_DIR/images/busybox.tar docker run --rm busybox uname -m docker run --rm busybox ping -c 1 127.0.0.1uname -m在容器内输出aarch64说明容器运行时能正确执行 arm64 架构的静态二进制ping 127.0.0.1是为了验证容器网络命名空间的基本回环通信。如果这台机器以后要跑 docker-compose 部署真实业务再加一条写一个最小 compose 文件启动一个 busybox 容器并固定重启策略确认 compose 和 docker daemon 配合正常。这一部分可以并入验证矩阵。4.3 把验证过程固化成 verify.sh并给出可执行的验证矩阵验证不能被“等客户自己跑”这样草率地对待我通常会把上面的命令整理成一个 verify.sh随安装包交付。脚本片段如下#!/usr/bin/env bash set -Eeuo pipefail BASE_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) PASS0 FAIL0 check() { local desc$1 shift if $ /tmp/verify_out.log 21; then echo [PASS] $desc PASS$((PASS1)) else echo [FAIL] $desc tail -n 5 /tmp/verify_out.log FAIL$((FAIL1)) fi } check_arch() { local arch arch$(docker version --format {{.Server.Arch}}) case $arch in arm64|aarch64) return 0 ;; *) return 1 ;; esac } docker load -i $BASE_DIR/images/hello-world.tar /dev/null docker load -i $BASE_DIR/images/busybox.tar /dev/null check docker 服务 active systemctl is-active docker check docker server 可用 docker version --format {{.Server.Version}} check docker 架构 arm64 check_arch check hello-world 可运行 docker run --rm hello-world check busybox uname docker run --rm busybox uname -m echo PASS$PASS FAIL$FAIL exit $FAIL注意这里的check函数用法第一个参数是描述后面的$是实际执行的命令序列最后一行把输出重定向到日志失败时只显示最后 5 行避免刷屏。check_arch单独定义一个函数而不是在check参数里写管道因为$在执行时不会把参数中的|当作管道符传给 shell直接塞进去会让 docker 收到错误参数。验证脚本退出码等于 FAIL 的个数这样 CI 或现场脚本能根据退出码判断整体是否通过。验证矩阵可以用表格列出来交付时贴到 README 里让不在现场的人也能判断结果验证项最小命令预期结果docker 服务状态systemctl is-active dockeractive服务端/客户端版本docker version --format {{.Server.Version}}与安装包记录版本一致服务端架构docker version --format {{.Server.Arch}}arm64 或 aarch64hello-world 运行docker run --rm hello-world输出说明文字且退出码为 0busybox 运行docker run --rm busybox uname -maarch64docker-compose 可用docker-compose --version能输出版本号且 file 为 aarch64这套矩阵的执行不需要外网不需要额外装工具都是最基础的命令。现场跑完把输出保存成日志文件作为交付记录的一部分。这就是“已在 openEuler 操作系统下验证”这句话真正能落到纸面的证据而不是口头承诺。5. arm64 openEuler 离线安装避坑指南5 个最常见的翻车现场5.1 现象docker 启动失败日志里全是 iptables 相关报错装完 rpm 后systemctl start docker屏幕不报错但systemctl status docker显示 failedjournalctl -u docker里出现iptables failed: iptables --wait -t nat -A DOCKER ...之类。原因通常是两个第一openEuler 22.03 之后默认使用 nftables而 docker 的经典网络模型要调用 iptables 来维护 NAT 规则第二系统里根本没有 iptables 命令或者 iptables 被 nft 兼容层替代后行为不一样。解决办法不是去 daemon.json 里关掉 iptables那是把容器网络一起关掉的“后悔药”代价太大。正确的处理是让系统同时具备 iptables 工具集并确保内核模块nf_tables、iptable_nat已加载。排查命令command -v iptables journalctl -u docker --no-pager | grep iptables lsmod | grep br_netfilter如果command -v iptables没有输出说明离线包里漏带了 iptables 相关 rpm需要回到打包机补上 iptables 和 iptables-libs。还有一个细节/proc/sys/net/bridge/bridge-nf-call-iptables这个文件只有在br_netfilter模块加载时才会出现如果容器跨主机通信有问题先执行modprobe br_netfilter再确认。5.2 现象docker compose 命令找不到但 docker-compose 二进制又能用在离线安装完成后你输入docker compose versionbash 提示 command not found而输入docker-compose version却有输出。这不是 docker-compose 没装而是两个命令形态的区别。docker compose是 docker CLI 插件需要把二进制放到 docker 的插件目录里比如/usr/libexec/docker/cli-plugins/docker-compose如果你只往/usr/local/bin/docker-compose放独立二进制那docker compose自然找不到。解决方法是统一命令形态。我们的离线包明确使用docker-compose命令所以文档里所有示例都写成docker-compose up -d而不是docker compose up -d。如果客户团队坚持用新命令可以在安装脚本里补一段把独立二进制再复制一份到cli-plugins目录并加可执行权限但我通常不这么干因为这会掩盖“独立二进制 vs 插件”的版本差异让后面排查变复杂。现场操作时先用command -v docker-compose确认路径再用docker-compose version验证版本确保交付文档和实际执行一致。5.3 现象在 qemu 模拟 arm64 环境里安装成功到真机却失败很多人在制作离线包时手里没有真机就在 x86 上用 qemu 模拟 arm64 虚拟机先验证。验证时一切正常到了目标硬件上却出现 tar 解压失败、rpm 安装报错、docker 服务起不来的问题。原因在于 qemu 用户态模拟是在 x86 内核上翻译 ARM 指令很多内核级行为、CPU 特性指令、浮点特性都和真机不完全一致。它适合跑业务软件的冒烟测试不适合验证内核模块、systemd 服务和网络栈行为。解决这个问题没有捷径离线包验证必须在至少一台真机 arm64 硬件上完成。如果公司没有真机租一台或借一台同型号的服务器也行。验证时记录下硬件型号、CPU、内核版本、openEuler 版本并把结果写进 README让接手的人知道“这个包在哪个范围内验证过”。qemu 可以用于开发脚本逻辑但不能作为“已在 openEuler 操作系统下验证”的依据这是血泪经验。5.4 现象docker run 报 exec format error镜像 load 成功docker images能看到docker run却抛出exec format error。这是离线安装最典型的“架构不匹配”问题。原因可能是镜像本来就是 x86 的 tar也可能是docker pull时在多架构仓库里拉到了默认的 amd64 版本。在打包机不是 arm64 的时候更容易发生比如你在 x86 机器上docker pull busybox默认拉 x86 镜像再docker save导入到 arm64 就犯了。解决方法是先确认镜像架构再决定是否使用。命令docker image inspect 镜像名 --format {{.Os}}/{{.Architecture}}输出是linux/arm64才能用。如果镜像显示linux/amd64可以尝试拉取指定平台版本docker pull --platform linux/arm64 busybox。离线包交付前最好把所有 images 目录下的 tar 都用这条命令过一遍把结果写进文件清单这样到了现场就不会出现 “load 成功但 run 失败” 的尴尬。另外注意不要在 openEuler 上装qemu-user-static来“兼容” x86 镜像离线环境多一层二进制翻译问题只会更隐蔽。5.5 现象rpm 安装时提示依赖缺失或版本冲突离线包在 openEuler 20.03 上验证通过在 openEuler 22.03 上执行rpm -ivh却报libcgroup或iptables版本冲突。这背后的本质是docker-ce 的 rpm 依赖不同版本的 libcgroup/libseccomp/iptables而 openEuler 大版本之间这些库的版本有差异。把在 CentOS 或别的发行版上收集的 rpm 直接搬到 openEuler往往就会出现“高版本的包依赖低版本的库”这类矛盾。解决套路是每个 openEuler 大版本都用一台独立打包机重新收集一次依赖并把rpm -qpR输出的依赖清单交给现场核对。脚本层面安装顺序也很重要。我建议把所有 rpm 放在一个目录用一条rpm -ivh命令安装让 rpm 自己处理依赖排序而不是手工按顺序安装单个文件。如果确实需要分步安装顺序一般是 containerd.io、docker-ce-cli、docker-ce。如果现场提示冲突可以先执行rpm -qa | grep docker查看是否有系统自带的 docker 包残留用rpm -e清理后再装。这里不做强制覆盖因为系统自带包可能与网络配置绑定强行替换会引入新的不稳定因素。6. 把离线安装包做成可交付级产物版本固化、自检与回滚离线安装包的下一级要求不是“能装”而是“出了事能退回去”。我在交付时会把三个东西固化进去安装前状态记录、安装日志、回滚脚本。安装前脚本先执行docker --version、systemctl status docker快照到/var/log/docker-pre-install.txt如果原系统已经存在 docker就把/etc/docker/daemon.json和 socket 权限一起备份到安装包外的备份目录。回滚不是卸载这么简单而是要把这些文件恢复到执行前状态。一个最小的回滚逻辑是这样rollback() { if [[ -f /var/log/docker-config-backup/daemon.json ]]; then cp /var/log/docker-config-backup/daemon.json /etc/docker/daemon.json fi systemctl stop docker || true rpm -e docker-ce docker-ce-cli containerd.io 2/dev/null || true systemctl daemon-reload }这段代码用|| true防止“包没装上就执行卸载”时脚本因为报错退出。rpm -e的包名列表要和安装包里的 rpm 名字保持同步如果用插件 rpm也需要一并列入。回滚脚本通常不作为默认执行路径但放到 uninstall.sh 里并在 README 中写明“执行前会停掉所有运行中容器”避免客户误操作。把日志、配置备份、校验和、版本记录都放在/var/log/docker-offline-*下这样出问题后提供给二线团队的就是一套完整的现场时间线而不是零散的复制粘贴。这个方向值不值得做我的判断很明确只要你的交付环境还会遇到第二台没外网的 arm64 机器就值得做。制作离线包的过程本身并不复杂难的是把架构差异和版本差异管理起来。我在新环境验证前一定会先跑./verify.sh再看安装日志而不是直接跑业务镜像这个习惯救过我不少次。希望帮到你。本文还有配套的精品资源点击获取