
公司新上的服务器全在内网客户一句话“需要用Docker部署”我就知道麻烦来了。内网环境没有外网权限yum install直接打水漂更别提什么在线拉镜像。那段时间我连续折腾了两天才把离线安装 Docker 的整套流程跑通。这篇就是记录我在 CentOS 7 上做 Docker 离线安装的完整过程包括下载、搬运、安装、验证以及那些只有真正踩过坑才会注意到的细节。如果你也在做私有化部署、内网交付或者公司网络严禁连接外网这篇可以直接当操作手册用。1. 什么场景非逼你走离线安装这条路1.1 一次现场交付的教训我接过一个项目客户的服务器放在隔离机房物理上就不通公网。业务方要求在这台机器上跑容器化应用第一反应是先把 Docker 环境拉起来。正常情况下CentOS 7 上一条命令就完事yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y docker-ce docker-ce-cli containerd.io但在内网机器上这几条命令走到第二步就挂了——仓库地址解析不了yum一直在报网络超时。当时我还没太当回事以为换个国内的镜像源就行结果发现内网连所有公网域名都做了白名单限制不是换源能解决的。最后只能绕回最原始的思路在外网找一台同架构的机器把所有安装包下载好再搬到内网手动安装。这个经历让我意识到一件事离线安装不是“备用方案”而是很多内网交付场景里的唯一方案。你没法指望客户给你开临时外网权限也没法在等审批上耗时间。1.2 离线安装的底层逻辑把“安装过程”拆成“搬运过程”很多人一听“离线安装”就觉得难其实拆开看很简单。在线安装之所以省心是因为yum会自动处理依赖下载、版本冲突这些问题。离线安装的本质是提前把依赖和主包全部备齐再在目标机器上做一次“无网络参与的安装”。可以这样理解在线安装是在新家下单买齐家具等着送货上门离线安装是你提前把所有家具打包好一趟车拉过去到了新家只负责拆包摆放。所以整个流程就三个环节准备环节找一台和目标机器系统、架构一致的联网机器。下载环节下载 Docker 主程序包以及所有依赖包。安装环节把 RPM 包全部拷贝到内网机器本地安装。听起来简单但实际操作里最容易翻车的恰恰是第一步“准备环节”。有人图省事在一台 CentOS 8 的机器上下载 RPM 包结果带到 CentOS 7 的目标机上装依赖库版本对不上一堆报错。系统版本和 CPU 架构必须匹配这是离线安装的第一条铁律。2. 动手前先确认三件事系统版本、依赖关系、下载工具2.1 目标机的系统版本和内核要摸清楚在下载任何安装包之前我建议你先到目标机上跑三条命令cat /etc/redhat-release uname -r uname -mcat /etc/redhat-release看发行版版本比如 CentOS Linux release 7.9.2009。uname -r看内核版本CentOS 7 一般是3.10.0-xxx.el7.x86_64之类的。uname -m看系统架构主流是x86_64但也有不少 ARM 架构的机器aarch64。为什么非得先看这些因为 RPM 包是区分发行版和架构的。el7的包不能装到el8上x86_64的包不能装到aarch64上。这个错一旦犯安装阶段会直接报wrong ELF class或者依赖解析失败。另外要注意内核版本。Docker 官方要求 CentOS 7 内核不低于 3.10绝大多数 CentOS 7 即装即用但如果你遇到的是定制过内核的老系统就要多留个心眼。后面我会专门讲内核和存储驱动的坑。2.2 docker-ce 的依赖包没有你想象中那么少很多人以为只要下载一个docker-ce.rpm就够了实际执行rpm -ivh docker-ce.rpm的时候会弹出一堆Failed dependencies这才发现 Docker 依赖了那么多包。以我在 CentOS 7 上安装 Docker 20.10.x 为例核心安装包和依赖通常包括包名作用docker-ceDocker 主程序包包含 daemon 和 clientdocker-ce-cliDocker 命令行工具单独拆分出来的containerd.io容器运行时Docker 依赖它来管理容器生命周期docker-compose-pluginCompose V2 插件建议一起装后面编排会用container-selinuxSELinux 策略扩展包不装可能起不了服务pigz并行压缩工具部分镜像解压会用到device-mapper-libs早期存储驱动相关依赖老版本会需要audit-libs-python / libsemanage-python / policycoreutils-python安装 container-selinux 时的依赖链如果缺少这些rpm -Uvh会给出很明确的报错但问题是你在内网没法执行yum install去补依赖只能重新去外网下载再传进来一来一回非常浪费时间。所以下载阶段就要把所有依赖全部拉齐。2.3 用 yumdownloader 自动解析依赖别手动一个个找面对这么多依赖手动去 rpmfind 网站一个个下载不是不行但效率极低还可能下错版本。我的做法是用yumdownloader配合--resolve参数让 yum 自动把依赖关系解析好一次性下载到位。yumdownloader需要先安装yum-utilsyum install -y yum-utils然后在联网机器上把 Docker 官方仓库加进来yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo如果你要指定版本而不是装最新的可以先看仓库里有哪些可用版本yum list docker-ce --showduplicates | sort -r确定版本号之后创建下载目录mkdir -p /root/docker-offline cd /root/docker-offline最后执行下载命令关键是--resolveyumdownloader --resolve docker-ce-20.10.24 docker-ce-cli-20.10.24 containerd.io docker-compose-plugin --destdir/root/docker-offline这条命令会把所有依赖的 RPM 包全部下载到/root/docker-offline目录。下载完成后用ls -lh看一下包的数量和大小我那次大概拉了十几个 RPM总大小在 80MB 到 100MB 左右。这个阶段有个小技巧把container-selinux单独确认一下。有时候yumdownloader --resolve会因为系统里已经装过 SELinux 相关策略包而跳过某些依赖到了离线机器上反而缺。保险起见可以手动补一条yumdownloader --resolve container-selinux pigz --destdir/root/docker-offline多花几秒钟省得后面来回折腾。3. 完整离线安装流程从下载到上线的五步操作3.1 在联网机器上拉取全部 RPM 包接上一节当所有 RPM 包都躺在/root/docker-offline目录里之后先别急着打包整理一下目录内容明确装了什么。我当时整理出来的文件类似这样docker-ce-20.10.24-3.el7.x86_64.rpm docker-ce-cli-20.10.24-3.el7.x86_64.rpm containerd.io-1.6.22-3.1.el7.x86_64.rpm docker-compose-plugin-2.24.1-1.el7.x86_64.rpm container-selinux-2.119.2-1.911c772.el7_8.noarch.rpm pigz-2.3.4-1.el7.x86_64.rpm ...这里想提醒一句如果目标机之前装过旧版本 Docker不建议直接用这些包做升级实验。离线环境下的版本升级比全新安装更麻烦容易出现配置残留、旧依赖清理不干净的问题。生产环境里我倾向于保持版本一致避免在离线场景引入额外变量。3.2 打包、传输与校验下载好的 RPM 包如果数量多我一般先打包再传输减少文件数量避免漏传cd /root tar czvf docker-offline-rpms.tar.gz docker-offline/*.rpm打包完成后用scp传到内网目标机scp /root/docker-offline-rpms.tar.gz root内网IP:/root/如果内网无法直连可以通过跳板机操作scp -oProxyJump跳板机用户跳板机IP /root/docker-offline-rpms.tar.gz root内网IP:/root/传输完成之后建议在目标机上做个校验。虽然scp本身有完整性校验但我在实际操作中遇到过传输过程中文件损坏的情况后来养成了习惯在联网机器上先算好 MD5到目标机再核对# 联网机器上 md5sum /root/docker-offline-rpms.tar.gz # 内网目标机上 md5sum /root/docker-offline-rpms.tar.gz两个值一致再继续解压否则重新传。这一步看着多余但能帮你屏蔽掉最诡异的一类问题——安装报错并不是因为你操作错而是因为包文件本身坏了。解压mkdir -p /root/docker-offline tar xzvf /root/docker-offline-rpms.tar.gz -C /root/docker-offline3.3 在目标机上执行安装进入 RPM 包目录后我建议先用rpm -qa确认目标机上没有装过老的 Docker 包rpm -qa | grep docker如果输出为空说明是干净环境直接安装cd /root/docker-offline rpm -Uvh *.rpmrpm -Uvh的U是升级安装如果是全新环境和-ivh效果一样它会按照 RPM 包自身的依赖元数据自动处理。但是要注意rpm -Uvh *.rpm并不保证安装顺序正确只是把所有包一次性交给 rpm 处理rpm 会根据依赖关系自行判断顺序。如果安装过程中出现Failed dependencies大概率是某个依赖包没下载全。此时把报错信息里的包名记下来回联网机器上补下yumdownloader --resolve 缺失的包名 --destdir/root/docker-offline补完再传输、再安装。如果遇到的是已经存在的包版本冲突比如系统自带的podman或runc与 Docker 冲突可以用rpm -e把冲突包先移除但操作前要小心最好确认一下这个包没有其他关键依赖。3.4 配置开机自启并启动 Docker 服务安装完成后先配置开机自启再启动服务systemctl enable docker systemctl start docker执行systemctl start docker后不要急着跑容器先用systemctl status docker看一眼状态是否active (running)。如果启动失败用journalctl -u docker查日志重点看有没有failed to start daemon、Error starting daemon之类的字眼。在我印象里新装 Docker 启动失败最常见的原因有三个containerd.io没装上或者版本不匹配。SELinux 相关策略没有正确安装导致container-selinux相关的报错。内核模块缺失比如overlay、br_netfilter没有加载。这三个问题都会在后面专门讲。3.5 用 docker version 和 docker info 确认安装状态Docker 启动之后验证命令非常关键。很多人只敲一句docker --version看能输出版本号就以为大功告成。实际上docker --version只能证明 client 端命令存在真正要确认服务端可用必须看docker version和docker infodocker version正常输出应该分两段Client和Server。如果只有Client而Server段报错说明 Docker daemon 没起来问题还大着呢。接着看docker infodocker info重点看这几个字段Storage Driver正常应该是overlay2。如果你看到devicemapper说明系统不支持 overlay2存在性能隐患。Cgroup Driver默认是cgroupfs如果以后要接 Kubernetes这里最好是systemd不然后面会踩坑。Server Version确认服务端版本。到这一步Docker 离线安装基本算成功了。但你别急着高兴因为还有一个绕不开的问题等着你内网环境里没有镜像Docker 装好了却拉不了镜像相当于买了车却没有油。4. 离线装完 Docker 后镜像怎么带进内网4.1 不要在内网执行 docker pull先把镜像导出装好 Docker 之后很多人习惯性地敲一句docker pull nginx:1.25然后发现卡在connect: network is unreachable。这是离线环境的常态不用慌。正确的思路是在联网机器上先把镜像拉下来导出成文件再把文件搬到内网导入。整个链路和 RPM 包离线安装如出一辙# 联网机器上 docker pull nginx:1.25 docker save -o nginx-1.25.tar nginx:1.25docker save会把镜像连同它的历史层、元数据全部打包进一个 tar 文件。这里有个细节docker save默认不会压缩镜像越大tar 文件越大。如果镜像超过几百 MB建议打包后用 gzip 压缩docker save nginx:1.25 | gzip nginx-1.25.tar.gz传到内网之后解压再导入或者直接在导入时解压docker load -i nginx-1.25.tar.gzdocker load支持 gzip 压缩的 tar 文件会先解压再导入所以我倾向于直接用压缩包传输。4.2 单机导入的 docker save / docker load 操作实录实际操作中内网机器往内网机器传镜像文件和 RPM 包传输一样使用scpscp nginx-1.25.tar.gz root内网IP:/root/到内网机器上执行docker load -i nginx-1.25.tar.gz输出会显示每一层镜像加载的过程最后出现Loaded image: nginx:1.25就算成功。再用docker images确认镜像已经在本地列表里。有一点特别提醒docker save保存的是当前机器平台架构对应的镜像。如果你在 x86_64 的机器上docker pull nginx:1.25导出的镜像也是 x86_64 架构的拿到 aarch64 的目标机上 load 也能加载但运行时爆炸的概率极高。所以下载镜像前先确认目标机架构再在对应架构的联网机器上拉取或者用docker pull --platform指定平台拉取。4.3 想长久解决镜像分发内网镜像仓库的搭建思路如果内网机器多一台台scp镜像文件再docker load不是不行但效率低。上线一个容器要传一个 tar十几个应用就是十几个 tar而且镜像文件版本混乱后特别难管理。更好的做法是在内网搭建一个私有镜像仓库比如跑一个 Registry 容器然后让所有离线机器从内网仓库拉镜像。但这里有一个先有鸡还是先有蛋的问题内网还没有 Docker 环境怎么跑 Registry 容器我的做法是先按前面步骤在一台机器上装好 Docker用docker save/load把registry:2镜像导入然后启动 Registry 容器docker run -d -p 5000:5000 --name registry --restartalways \ -v /data/registry:/var/lib/registry registry:2之后其他机器只需要在/etc/docker/daemon.json里配置insecure-registries内网 HTTP 或无信任证书时{ insecure-registries: [192.168.1.100:5000] }然后就能直接从内网仓库 push/pull 镜像了。这个方案的收益很直观以后任何镜像变更只需要在一台机器上docker pull后docker tag到私仓地址再docker push其他机器从私仓拉取不用再传 tar 包。做私有化交付时这套组合拳非常省事。5. 离线安装高频踩坑记录与排查方法5.1 依赖包版本不对导致安装中途报错我早期踩得最多的坑就是版本错位。比如docker-ce是 20.10.24而containerd.io是另一个较老的版本安装时 rpm 会报Error: Package: docker-ce-20.10.24-3.el7.x86_64 Requires: containerd.io 1.6.0这个报错已经算仁慈了因为它直接告诉你缺什么。最烦的是某些依赖版本满足报错条件但实际运行时行为异常。所以我后来固定动作是用yumdownloader --resolve时指定相同版本号尽量让主包、CLI、containerd 版本处于一个发布周期内。5.2 内核过老导致存储驱动回退CentOS 7 默认内核 3.10用 Docker 20.10 一般没问题。但如果遇到一些被裁剪过的定制内核overlay2驱动不可用Docker 会自动回退到devicemapper性能下降一截之外容器挂载行为也怪。可以先检查内核模块lsmod | grep overlay lsmod | grep br_netfilter如果没加载手动加载一下modprobe overlay modprobe br_netfilter确认模块存在后最好设置开机自动加载。新建/etc/modules-load.d/docker.confoverlay br_netfilter然后把桥接流量转发到 iptables 的配置写入/etc/sysctl.d/docker.confnet.bridge.bridge-nf-call-ip6tables 1 net.bridge.bridge-nf-call-iptables 1执行sysctl -p /etc/sysctl.d/docker.conf生效。这些配置在做 Kubernetes 节点时尤其重要离线环境里没外网出了问题排查成本更高前置做好能省心很多。5.3 cgroup 驱动不一致前期没感觉后期带出大问题docker info里有一项Cgroup Driver默认值是cgroupfs。如果这台机器以后要加入 Kubernetes 集群而 kubelet 的 cgroup 驱动是systemd两者不一致会直接导致 kubelet 报错容器生命周期管理混乱。解决方法是提前配置/etc/docker/daemon.json{ exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 }, storage-driver: overlay2 }改完重启 Dockersystemctl daemon-reload systemctl restart docker在这里多说一句daemon.json 最好在 Docker 刚装好、还没有跑任何容器的时候配置因为 cgroup 驱动切换需要重启 Docker这一重启所有运行中的容器都会受影响。生产环境里没人愿意为了改一个配置去挪一堆容器。5.4 传输损坏和磁盘空间不足的隐蔽坑RPM 包传输损坏的问题前面提过镜像 tar 包同样有这个风险。比如内网网络质量差scp一个 2GB 的镜像包传到 90% 的时候断了硬着头皮继续用结果docker load提示archive/tar: invalid tar header。这时候别怀疑操作步骤先重新传一遍或者校验一下 md5。另一个隐蔽的坑是磁盘空间。离线安装本身占不了多少空间但容器运行起来之后镜像、容器层、日志全往/var/lib/docker里塞如果这个目录所在分区空间不足Docker daemon 会非常奇怪地报错比如no space left on device或者更隐晦的failed to mount local volume。所以装好 Docker 之后第一件事就是检查/var/lib/docker所在分区的剩余空间df -h /var/lib/docker如果空间不够建议在daemon.json里修改>{ data-root: /data/docker }这个配置同样要在跑容器之前改好否则迁移数据目录的复杂度会直线上升。5.5 时间不同步导致的奇怪问题内网机器时间飘了是离线环境里最容易被忽略的“隐形杀手”。Docker 在拉取镜像、获取镜像元数据时经常要验证时间戳如果本地时间和实际时间相差太大会报证书过期、签名验证失败这类莫名其妙的错误。我之前有一次在内网搭 Registry镜像 push 过去一切正常pull 的时候却报x509: certificate has expired or is not yet valid查了半天才发现是机器时间慢了三个小时。离线环境访问不了公网 NTP 服务解决办法是配置内网 NTP 服务器yum install -y ntpdate ntpdate 内网NTP服务器IP如果内网没有 NTP 服务器也可以在计划任务里定期从某一台相对可靠的机器同步但这种方式精度有限尽量还是让运维把内网时钟源搭起来。安装了 Docker 之后我的习惯是把系统时钟同步也纳入交付清单里。你会发现很多看似无关的问题最后都能回溯到时间或网络这类基础环境上。最后说一个我自己这些年固定的操作习惯每做完一次离线安装我都会把用到的 RPM 包、镜像 tar、daemon.json 模板、安装命令脚本全部归档到一个文件夹存到内部共享存储里。下次再做类似交付直接复制这份“离线工具箱”版本一致、流程一致踩过的坑不会第二次踩。这比任何“在线速成”方案都更可靠——因为你的工具箱是用一次次的现场问题喂出来的。