ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

arm64 内网 Docker 与 docker-compose 离线安装实战

arm64 内网 Docker 与 docker-compose 离线安装实战

简介:面向 arm64 架构服务器与 openEuler 环境的离线部署需求,这套 Docker 与 Docker Compose 安装包针对内网、无外网或网络受限场景整理,适合运维人员及需要快速落地容器环境的开发者使用。压缩包共 4 个文件,约 54.41MB,涵盖 Docker 18.09.9 的 tgz 离线镜像包、systemd 服务配置文件、一键安装脚本以及 docker-compose 可执行文件,覆盖安装、服务注册与编排工具配置等关键环节,减少逐项搜集与版本匹配成本。已有 646 人学习下载,经过 openEuler 系统验证,对 arm64 平台兼容性有明确参考价值。使用者可据此完成 Docker 引擎与 Compose 的离线部署,并借助 service 配置与脚本化流程降低手工操作遗漏,适合需要可复用离线安装方案、快速搭建容器运行环境并保留排错与二次调整空间的读者。

1. 内网 arm64 机器上装 Docker:先想清楚离线包到底替你省了哪几步

一台 arm64 服务器被推进内网机柜,交付窗口只剩 SSH,外网不通,yum 源也不可达。此时要装 Docker 和 docker-compose,靠的不是运气,而是一套能在 openEuler 上落地的离线安装包:里面是 aarch64 的 docker 二进制、compose 二进制、systemd 单元、daemon.json 和一键安装脚本。它解决的是“无外网、无包管理器依赖、架构必须匹配”的三重约束,适合内网交付、边缘节点、实验室 arm64 机器和需要批量复装的场景。拿到包之后别急着执行脚本,先确认架构、systemd 状态和存储驱动,后面按拆包、安装、compose 落地、排错逐层推进。

2. 拆开离线包:aarch64 二进制、systemd 单元与一键脚本怎么配合

2.1 目录结构与文件职责

我拿到这类离线包,第一件事不是运行 install.sh,而是先看目录。常见结构会按“二进制、配置、服务单元、脚本、校验文件”分开,目录名可能不同,但职责基本一致。下面这张表按常见路径拆分,实际以你手里的包为准,文件名里的<版本>不用纠结,重点看架构和可执行权限。

路径作用检查点
bin/dockerdDocker 守护进程file结果应为 ARM aarch64
bin/docker客户端命令与 dockerd 同版本、同架构
bin/containerd容器运行时必须存在,否则 dockerd 起不来
bin/runc底层运行时权限 0755,路径在 PATH 内
bin/docker-init、bin/docker-proxy容器初始化与端口转发不要漏拷,否则网络异常
compose/docker-composecompose v2 二进制架构同样是 aarch64
systemd/docker.servicesystemd 单元ExecStart指向/usr/bin/dockerd
config/daemon.json守护进程配置JSON 不能有注释和尾逗号
install.sh一键安装脚本看是否有备份、幂等和参数解析
SHA256SUMS校验清单解压后先跑校验,别省这一步

先用find把文件清单和大小列出来,确认没有 0 字节文件,也确认没有把 x86_64 的二进制混进来。命令不复杂,但能提前暴露很多“装完才翻车”的问题。

find docker-offline-arm64 -maxdepth 3 -type f -printf '%p %s\n' | sort

这段命令只做一件事:按路径排序列出文件大小。-maxdepth 3防止目录太深刷屏,-printf比ls -l更适合看字节数。若某个二进制只有几百字节,大概率是占位文件或软链,不要直接 install 到/usr/bin。

2.2 为什么静态二进制比离线 rpm 更省事

在 openEuler 上,离线 rpm 不是不能用,而是依赖闭包要先解决:docker 相关包、containerd、runc、iptables、libcgroup、systemd 依赖,任何一个版本对不上,安装器就会在目标机上停住。静态二进制把依赖压进文件本身,脚本只负责拷贝、赋权、写 systemd 单元、启动服务。对交付窗口很窄的内网机器,这条路更直接,也更容易回滚:保留旧二进制和旧daemon.json,出问题能退回去。

但静态二进制不是万能后悔药。它不替你加载内核模块,不替你改sysctl,不替你处理 firewalld 与 nftables 的边界,也不包含业务镜像。常见做法是脚本只做“安装 Docker 运行时”,内核参数、防火墙策略、镜像导入分别用独立步骤验收。这样出问题时能定位到是哪一层:二进制没跑起来、systemd 没拉起来、网络没通、还是镜像没导入。docker-compose 也是同样逻辑,v2 是 Go 静态二进制,离线包里直接放一个可执行文件即可;v1 依赖 Python 包,在 arm64 离线环境里反而容易卡在依赖上。选型时优先docker compose插件方式,命令统一,脚本里少写一套兼容分支。

2.3 执行前的四项检查

一键脚本最怕在错误前提上执行。架构、权限、systemd、磁盘,这四项先过一遍,能省掉大量排查时间。下面这段检查可以直接抄到终端里跑,也可以作为 install.sh 的前置逻辑。

#!/usr/bin/env bash set -euo pipefail ARCH="$(uname -m)" case "$ARCH" in aarch64|arm64) ;; *) echo "需要 arm64/aarch64,当前为 $ARCH" >&2; exit 1 ;; esac [ "$(id -u)" -eq 0 ] || { echo "请用 root 或 sudo 执行" >&2; exit 1; } command -v systemctl >/dev/null || { echo "systemd 不可用" >&2; exit 1; } DF_AVAIL="$(df -Pm /var/lib | awk 'NR==2 {print $4}')" [ "$DF_AVAIL" -ge 2048 ] || { echo "/var/lib 可用空间不足 2GB" >&2; exit 1; } echo "架构=$ARCH 空间=${DF_AVAIL}MB 检查通过"

set -euo pipefail让脚本在变量未定义、管道失败时立即退出,避免“前面报错后面继续跑”。uname -m在 arm64 机器上可能是aarch64或arm64,两个都放行。id -u判断 root,是因为后面要写/usr/bin、/etc/systemd/system。df -Pm用 MB 为单位,阈值 2GB 只是起步线,实际还要看镜像层大小。command -v systemctl确认不是 chroot 或裁剪系统。

注意:如果目标机是容器内的“伪系统”,systemd 可能不可用,这套脚本不要硬跑。先确认systemctl is-system-running有返回,再继续。

2.4 一键脚本的幂等设计

一键脚本要能重复执行,不能第一次成功、第二次把配置覆盖坏。关键动作是:备份旧配置、停止旧服务、拷贝二进制、重载 systemd、启动并检查状态。下面是一段常见骨架,重点看顺序和容错,不要照抄文件名。

#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" BACKUP_DIR="/etc/docker/backup-$(date +%Y%m%d%H%M%S)" # 旧服务存在就先停,不存在也不让脚本退出 if systemctl list-unit-files | grep -q '^docker.service'; then systemctl stop docker || true fi # 备份旧配置,避免覆盖后无法回退 mkdir -p "$BACKUP_DIR" [ -f /etc/docker/daemon.json ] && cp -a /etc/docker/daemon.json "$BACKUP_DIR/" || true # 拷贝二进制并赋权 install -d -m 0755 /usr/bin for f in dockerd docker containerd containerd-shim-runc-v2 ctr runc docker-init docker-proxy; do [ -f "$ROOT_DIR/bin/$f" ] && install -m 0755 "$ROOT_DIR/bin/$f" "/usr/bin/$f" done # 写配置和 systemd 单元 install -d -m 0755 /etc/docker /etc/systemd/system install -m 0644 "$ROOT_DIR/config/daemon.json" /etc/docker/daemon.json install -m 0644 "$ROOT_DIR/systemd/docker.service" /etc/systemd/system/docker.service systemctl daemon-reload systemctl enable --now docker systemctl --no-pager --full status docker | sed -n '1,20p'

ROOT_DIR用脚本所在目录,避免依赖当前工作目录。systemctl stop docker || true处理“旧服务不存在”或“停止失败但可继续”的情况。cp -a保留权限和时间戳,回滚时直接拷回。install -m 0755比cp多一步权限控制,适合二进制。循环里用[ -f ... ]判断文件存在,避免某个可选组件缺失导致整个脚本退出。最后systemctl enable --now docker同时设置开机自启和立即启动,status只打印前 20 行,方便看主状态。

提示:脚本里不要写systemctl start docker && sleep 2 && docker ps这种“睡两秒就当成功”的判断。改成systemctl is-active --quiet docker或直接看docker info返回值,失败时把journalctl -u docker -n 80 --no-pager打出来。

3. 在 openEuler 上跑一键安装:从解压校验到 docker info 通过

3.1 解压、校验与放置路径

离线包先放到/opt或/usr/local/src,不要放在/tmp里跑完就丢。解压后先校验,再执行脚本。校验文件可能是SHA256SUMS,也可能按文件分开;没有校验文件时,至少用file看架构,用tar -tzf看包内路径有没有多一层目录。

sudo mkdir -p /opt/docker-offline sudo tar -xzf docker-offline-arm64.tar.gz -C /opt/docker-offline cd /opt/docker-offline/docker-offline-arm64 # 有校验文件就跑,没有就跳过这一行 sha256sum -c SHA256SUMS # 看关键二进制架构,避免 exec format error file bin/dockerd bin/docker compose/docker-compose

tar -xzf的-C指定解压目录,避免污染当前目录。sha256sum -c会逐行比对清单,输出OK或FAILED,失败就别继续。file的输出里应出现ARM aarch64或ARM64,如果看到x86-64,说明包拿错了,后面所有步骤都不用做。解压后目录多一层是常见事,cd到真正包含install.sh的那一层再执行。

3.2 脚本参数与执行顺序

不同包的一键脚本参数不一样,常见会支持--with-compose、--data-root、--user、--no-start这类开关。先看--help,没有 help 就直接打开脚本看顶部变量。不要在不清楚参数的情况下直接sudo ./install.sh,尤其是它会覆盖/etc/docker/daemon.json的时候。

cd /opt/docker-offline/docker-offline-arm64 # 先看脚本接受哪些参数 sudo ./install.sh --help || sed -n '1,120p' install.sh # 常见执行方式:安装 compose 插件,数据目录保持默认 sudo ./install.sh --with-compose --data-root /var/lib/docker # 如果脚本没有参数解析,按脚本内变量改完再执行 sudo bash -x ./install.sh

--with-compose表示把 compose 二进制安装到 CLI 插件目录;--data-root改 Docker 数据目录,默认/var/lib/docker够用就不要乱改。bash -x会打印每条命令,适合第一次在测试机排错,生产交付时不要长期开着,输出太多反而干扰。执行顺序上,先停旧 docker,再拷二进制,再写配置,再daemon-reload,最后enable --now;顺序颠倒会出现“新二进制配旧 unit”的怪状态。

如果脚本执行到systemctl start docker报错,不要重复跑脚本。先看systemctl status docker和journalctl -u docker -n 100 --no-pager,常见是daemon.json语法错、dockerd路径不对、containerd没拷进去。修正后重新systemctl daemon-reload再启动,比反复执行 install.sh 更干净。

3.3 安装后验证:版本、架构、cgroup 与存储驱动

安装完成不等于可用。至少验证四件事:客户端与服务端版本、架构、cgroup driver、存储驱动。openEuler 上常见期望是Cgroup Driver: systemd、Storage Driver: overlay2、Architecture: aarch64。如果 cgroup driver 是cgroupfs,后面接容器编排或系统服务时容易出兼容问题。

docker version docker info --format 'Arch={{.Architecture}} Driver={{.Driver}} Cgroup={{.CgroupDriver}} Root={{.DockerRootDir}}' systemctl is-active docker systemctl is-enabled docker journalctl -u docker -n 50 --no-pager

docker version同时看 Client 和 Server,Server 段没出来说明守护进程没起。docker info --format用 Go 模板直接取字段,比翻长输出快。systemctl is-active返回active才算服务在跑,is-enabled返回enabled才算开机自启。journalctl看最近 50 行,重点看failed to start daemon、failed to load listeners、permission denied这类关键词。

注意:离线环境不要用docker run hello-world做首次验证,它会尝试拉取镜像,网络不通时看起来像 Docker 坏了。用包内自带的测试镜像,或者只跑docker info和docker images。

3.4 非 root 使用与开机自启的边界

默认只有 root 能访问/var/run/docker.sock。日常用非 root 账号,常见做法是加docker组,然后重新登录或newgrp docker。这一步是便利性配置,不是必须项;生产机上谁进 docker 组,等于给了很高的宿主权限,别随手把所有人都加进去。

# 创建 docker 组(安装脚本通常会做) sudo groupadd -f docker # 把当前用户加入组 sudo usermod -aG docker "$USER" # 当前 shell 立即生效,或者退出重新登录 newgrp docker id docker ps

groupadd -f表示组已存在也不报错。usermod -aG的-a很重要,漏掉会把用户从其他附加组里踢出去。newgrp docker只对当前终端生效,新开窗口不一定继承,所以交付时要说明“重新登录”。id看输出里有没有docker组,docker ps能列出空表也算通。不要用chmod 777 /var/run/docker.sock解决权限,这等于把 root 能力开放给所有本地用户。

开机自启用systemctl enable docker,但要注意数据目录挂载顺序。如果/var/lib/docker在独立磁盘上,而 docker.service 在网络和本地文件系统之前启动,可能出现目录未挂载就启动的情况。常见做法是在 unit 里加After=local-fs.target,或者把数据目录放到根分区。这个边界不处理,重启后容器全丢的案例并不少见。

4. docker-compose 离线落地:插件路径、镜像导入与 pull_policy 配置

4.1 compose v2 插件与独立二进制的选型

docker-compose 在离线环境里有两个常见形态:v2 的 CLI 插件docker compose,以及独立二进制docker-compose。v2 是 Go 静态二进制,不依赖 Python 包,放在 arm64 离线包里最省事;v1 的 Python 依赖链在无外网机器上很容易卡住。选插件方式的好处是命令统一,脚本里写docker compose;选独立二进制的好处是旧脚本不用改,直接调docker-compose。

我的建议是优先插件方式,同时保留独立二进制做兼容,但不要让两个版本同时抢 PATH。插件路径通常在/usr/local/lib/docker/cli-plugins/docker-compose或~/.docker/cli-plugins/docker-compose,文件名必须是docker-compose,Docker CLI 靠这个名字发现插件。独立二进制放/usr/local/bin/docker-compose,权限 0755。两条路选一条作为主路径,另一条只在迁移期存在,验收时明确用哪条命令。

4.2 安装到 cli-plugins 并验证

插件方式的安装动作很轻:建目录、拷文件、赋权、跑版本。注意目录权限,/usr/local/lib/docker/cli-plugins一般 root 可写,普通用户只读;用户级插件目录则是~/.docker/cli-plugins,适合单用户场景。

# 系统级插件目录 sudo install -d -m 0755 /usr/local/lib/docker/cli-plugins sudo install -m 0755 compose/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose # 验证插件是否被 docker CLI 发现 docker compose version # 如果还要兼容旧脚本,再装独立二进制 sudo install -m 0755 compose/docker-compose /usr/local/bin/docker-compose docker-compose version

install -d -m 0755建目录并设权限,install -m 0755拷文件并设可执行。docker compose version输出里应看到 v2 版本信息;如果报docker: 'compose' is not a docker command,说明插件目录不对或文件名不对。独立二进制的docker-compose version用于旧脚本兼容,但不要在同一个脚本里混用docker compose和docker-compose,否则排错时很难判断到底调了哪个二进制。

提示:用command -v docker-compose和docker compose version分别确认路径与插件状态。两个都装时,脚本里固定写死一种调用方式,不要把选择权留给运行环境。

4.3 离线镜像导入与 compose 启动

compose 项目能不能起来,不取决于 compose 二进制本身,而取决于镜像有没有提前导入。离线环境先docker load,再确认镜像架构是 arm64,最后用pull_policy: never或--pull never阻止 compose 尝试联网。

# 导入离线镜像包,可能是一个或多个 tar docker load -i images/app-arm64.tar docker load -i images/redis-arm64.tar # 看镜像列表和架构 docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}' docker image inspect demo/app:arm64 --format '{{.Architecture}}'

docker load -i从 tar 包恢复镜像层和元数据。docker images用表格格式看仓库、标签、ID、大小。docker image inspect取Architecture字段,必须是arm64,如果显示amd64,说明镜像包拿错了,compose 起来也会报exec format error。镜像名和标签要和 compose 文件里的image完全一致,差一个 tag 就会触发拉取。

services: app: image: demo/app:arm64 pull_policy: never ports: - "8080:8080" environment: APP_ENV: prod

pull_policy: never告诉 compose 不要尝试拉取,直接用本地镜像。这个字段在较新的 compose 规范里可用;如果现场版本不认,就在命令上加--pull never。ports把容器端口映射到宿主,离线环境也要检查防火墙是否放行。environment按项目实际改,不要照抄。

docker compose -f compose.yaml up -d --pull never docker compose -f compose.yaml ps docker compose -f compose.yaml logs --tail=50

up -d后台启动,--pull never是命令级兜底,和 yaml 里的pull_policy形成双保险。ps看容器状态和端口,logs --tail=50看启动日志。若容器反复重启,先看日志里的镜像架构、入口命令、配置文件路径,不要一上来就怀疑 Docker 本身。

4.4 版本共存与回滚

插件和独立二进制共存时,最容易出现“终端 A 能用、终端 B 不能用”。原因是 PATH 不同、用户级插件目录不同、或者旧docker-compose还在/usr/bin。验收时用绝对路径查一遍,交付脚本里固定调用方式,能减少这类玄学问题。

场景推荐调用检查命令
新项目、脚本统一docker composedocker compose version
旧脚本兼容docker-composecommand -v docker-compose
单用户安装用户级插件目录ls ~/.docker/cli-plugins
系统级安装/usr/local/lib/docker/cli-pluginsls -l看权限

回滚时保留旧二进制和旧 compose 文件,不要直接覆盖。升级前把/usr/local/bin/docker-compose或插件文件改名备份,出问题改回来再hash -r清 shell 缓存。Docker 本身的回滚同理:保留/usr/bin/dockerd旧文件、/etc/docker/daemon.json备份、/etc/systemd/system/docker.service备份。离线交付的底气不是“不会坏”,而是坏了能在十分钟内退回去。

5. 避坑与排查:五个让脚本装完却跑不起来的细节

5.1 现象:执行 dockerd 报 exec format error

现象很直接:systemctl start docker失败,journalctl里出现exec format error,或者手动执行/usr/bin/dockerd --version直接报格式错误。原因通常不是权限,而是二进制架构不匹配:包是 x86_64 的,机器是 aarch64;或者反过来,脚本把bin/下某个占位文件拷进去了。

先别重装,用file和readelf看机器码。file看整体类型,readelf -h看Machine字段。aarch64机器上应看到AArch64,不是Advanced Micro Devices X86-64。

uname -m file /usr/bin/dockerd readelf -h /usr/bin/dockerd | grep -E 'Class|Machine'

uname -m确认宿主架构,file确认文件架构,readelf确认 ELF 头。解决方式是从离线包里重新拷正确的 aarch64 二进制,别用chmod +x硬修,架构不对加执行权限也没用。脚本里加一道file bin/dockerd | grep -q aarch64的前置检查,能提前拦住这类问题。

5.2 现象:systemctl start docker 失败,日志指向 daemon.json

现象是服务起不来,systemctl status docker显示failed,journalctl -u docker里提示unable to configure the Docker daemon with file /etc/docker/daemon.json。原因多是 JSON 语法错:尾逗号、单引号、注释、字段拼写错误,或者把log-opts写成了字符串而不是对象。

先用jq empty做语法校验,再用dockerd --validate或前台调试看具体字段。jq不在离线环境里时,用python3 -m json.tool也能看。前台运行会占用终端,适合测试机,不要在生产窗口长期挂着。

jq empty /etc/docker/daemon.json sudo dockerd --config-file /etc/docker/daemon.json --debug

jq empty只解析不输出,返回 0 说明 JSON 结构合法。dockerd --debug前台启动,能看到配置加载到哪一步失败。修完配置后systemctl daemon-reload不是必须的,但systemctl restart docker必须做。若配置里改了>sysctl net.ipv4.ip_forward sysctl net.bridge.bridge-nf-call-iptables lsmod | grep -E 'br_netfilter|overlay' firewall-cmd --list-all

ip_forward应为 1,bridge-nf-call-iptables应为 1,br_netfilter和overlay模块应加载。firewall-cmd --list-all看当前 zone 放行了哪些端口和服务。解决方式是按现场安全策略放行端口,或把docker0放进信任区域;不要直接systemctl stop firewalld了事,交付后会被安全审计追着跑。修改sysctl后要写进/etc/sysctl.d/,否则重启失效。

注意:br_netfilter没加载时,sysctl net.bridge.bridge-nf-call-iptables可能直接报错。先modprobe br_netfilter,再写/etc/modules-load.d/docker.conf,重启后才会自动加载。

5.4 现象:compose up 卡在拉取镜像

现象是docker compose up停在Pulling,最后超时。原因不是 compose 坏了,而是本地没有对应镜像,或者镜像 tag 与 yaml 不一致,compose 按默认策略尝试联网。离线环境必须显式禁止拉取。

docker images --format '{{.Repository}}:{{.Tag}}' docker compose config | grep -n 'image:' docker compose up -d --pull never

先看本地镜像列表,再核对 compose 文件里的image行。docker compose config会渲染最终配置,能看出变量替换后到底是什么镜像名。--pull never是命令级兜底;yaml 里加pull_policy: never是配置级兜底。两个都加,现场不会因为版本差异退化成联网拉取。若镜像名对但架构不对,up可能不报拉取错误,而是启动后exec format error,这时回到 5.1 检查镜像架构。

5.5 现象:非 root 用户 permission denied

现象是普通用户执行docker ps报permission denied while trying to connect to the Docker daemon socket。原因是用户不在docker组,或者加了组但当前 shell 没重新加载组信息。还有一种情况是/var/run/docker.sock权限被改坏。

id getent group docker ls -l /var/run/docker.sock sudo usermod -aG docker "$USER" newgrp docker

id看当前用户附加组,getent group docker看组成员,ls -l看 socket 属主和权限。usermod -aG后必须重新登录或newgrp,否则当前进程的组 ID 不变。不要用chmod 777修 socket,这会把 Docker 守护进程的控制面开放给所有本地用户。生产环境更稳妥的做法是只给交付账号加组,或者用 sudo 白名单限制具体命令。

6. 验收与复装:把离线安装压成三条可重复的命令

6.1 三项验收

离线安装做完,不要只看“脚本没报错”。我通常压成三条命令:架构、Docker 服务信息、compose 版本。三条都过,才把机器交出去。

检查项命令通过标准
架构uname -maarch64或arm64
Docker 服务docker info --format '{{.Architecture}} {{.Driver}} {{.CgroupDriver}}'aarch64 overlay2 systemd
composedocker compose version有 v2 版本输出
uname -m docker info --format '{{.Architecture}} {{.Driver}} {{.CgroupDriver}}' docker compose version systemctl is-active docker

docker info这条同时看架构、存储驱动、cgroup driver,比docker version更直接。systemctl is-active补一刀服务状态。任何一项不符合,先回到对应章节查,不要靠重启碰运气。

6.2 一个交付习惯

血泪经验是:离线机器上最容易出问题的不是安装脚本,而是“装完没留证据”。后来我每次交付都强制走一遍uname -m && docker info && docker compose version,再把journalctl -u docker -n 200 --no-pager存到交付记录里。下次同一批机器复装,直接对比上次输出,架构、驱动、cgroup 哪一项变了,一眼就能看出来。这套 arm64 离线包的价值不在脚本多花哨,而在你把检查顺序固定下来,换一台 openEuler 机器也能按同样节奏复现。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进