上个月处理一台内网服务器,戴尔 PowerEdge R740,双路至强,CentOS 7.9,放在隔离网段里,只能通过跳板机访问。业务方把容器化后的服务包丢过来,说得很轻松:“帮我把 Docker 环境整起来。”我当时也天真,以为拷几个 rpm 上去rpm -ivh就完事,结果一路撞见依赖缺失、iptables 版本不匹配、SELinux 拦截各种问题,最后花了大半天才把环境稳定起来。所以这篇东西,本质上是给准备在 x86 平台做离线 Docker 部署的朋友留一份实操记录,能少踩一个坑算一个。
标题里的“x86 版本环境”其实是个很宽泛的说法。现在大部分服务器,不管是 Intel 还是 AMD,跑的系统都是 x86_64 位,离线安装 Docker 的难点反而不在 CPU 架构,而在你怎么在没有外网的情况下把安装包、依赖、镜像这三样东西都凑齐。
1. 为什么离线安装 Docker 是常态而不是个例
1.1 真实的内网部署场景
离线装 Docker 的场景,比大部分人想象得多。生产环境里,很多业务系统和数据库跑在隔离网段,不能直接上外网;金融、政务、企事业单位的自建机房,网络策略一条条卡得很死;还有一部分是灾备环境,平时根本没有出网权限。这时候要在服务器上跑容器,唯一的办法就是把安装介质准备好,再手工搬到目标机器上。
另外有一类场景容易被忽略:目标机器能上网,但源站网络不稳定,yum install或apt install拉到一半就断开,反复重试浪费时间。干脆在有外网的另一台机器上把包全部拉下来,一次性拷贝过去,反而更可控。
1.2 离线安装 Docker 不是“拷个包”那么简单
很多人把离线装 Docker 理解成“下载一个 rpm 或 deb,拷过去装上就行”。实际动起手来你会发现,一个完整的 Docker 运行环境至少包含三层内容:
- 安装包本体:服务端
dockerd、客户端docker、容器运行时containerd、runc等核心二进制。 - 依赖项:libltdl、iptables 相关工具、device-mapper、SELinux 策略包等。这些依赖在联网机器上会被包管理器自动拉取,但在离线环境下不会凭空出现。
- 镜像数据:装好 Docker 之后你还需要业务镜像,镜像得通过
docker save/load或内网仓库搬进去,这一步很多人会漏掉。
搞清楚这三层,后面的思路就顺了。
2. 动手前的系统勘察:架构、发行版和磁盘空间
2.1 先确认到底是不是 x86_64
离线安装最关键的一件事,就是别拿错架构的包。虽然现在绝大多数服务器都是 x86_64,但偶尔还能遇到 i686 或 arm64 的机器,确认方法很简单:
uname -m输出x86_64,那就放心去下载 amd64 架构的安装包;如果输出aarch64,那就是 ARM 平台,后续整个部署方案完全不同,下面的内容就不适用了。还可以再用lscpu看一遍 CPU 型号和指令集,确认无误。
注意:Docker 官方二进制包的文件名里一般会带上架构标记,比如
linux-amd64。下载时一定要认准这个标记,别因为机器能跑就随手拿了一个 arm64 的包,等到exec format error报出来再回头折腾就浪费时间了。
2.2 判断发行版和包管理体系
同样标题里的“x86 环境”,实际装上什么系统千差万别。最常见的是 CentOS / RHEL 7.9、Ubuntu 20.04 / 22.04,还有一批国产 Linux 发行版逐渐在政企内网里普及。判断方法很直接:
cat /etc/os-release核心看两点:ID字段和ID_LIKE字段。如果是centos、rhel、fedora,走 rpm 包或二进制包路线;如果是ubuntu、debian,走 deb 包或二进制包路线。国产发行版大多数兼容其中一种,比如 openEuler 系接近 rpm,统信 UOS 的服务器版能兼容 deb,麒麟系一般也基于 rpm 体系。拿不准的时候,先在系统里执行rpm -q rpm或dpkg --version,看哪个命令存在,就能判断包管理体系。
还需要看两个关键内核参数:
uname -r cat /sys/module/overlay/parameters/version 2>/dev/nullDocker 默认存储驱动是overlay2,内核至少要 3.18 以上才支持得比较好。CentOS 7.9 的内核是 3.10 系列,官方在特定版本以后做了兼容处理,但新版本 Docker 在旧内核上有时会遇到 overlay 相关的稳健性问题。如果确认内核太老或者 overlay 模块加载不了,可以在/etc/docker/daemon.json里把存储驱动改成vfs,但代价是镜像层会占用大量磁盘空间,我一般不推荐,除非实在没有别的办法。
2.3 磁盘空间和目录规划
Docker 的默认数据目录是/var/lib/docker,镜像、容器、数据卷都落在这里。离线部署前先看磁盘:
df -h /var/lib/docker /opt给/var/lib/docker留出的空间,按镜像总量的 1.5 到 2 倍来预估比较安全,因为镜像层、容器读写层、日志文件都会继续膨胀。如果你把/和/var放在同一个分区,那么直接看根分区剩余空间即可;如果/var/lib/docker是独立挂载点,就要单独评估。
部署包建议放在单独目录:
mkdir -p /opt/dockeroffline/pkg mkdir -p /opt/dockeroffline/images目录建好不是为了好看,离线部署过程中你会反复来回拷贝包、校验、解压,固定一个路径能让你少犯“明明刚才放了这个文件,怎么找不到了”的糊涂错。
3. 获取安装包与依赖:离线部署的“搬货”思路
3.1 在有网机器上批量拉取 rpm / deb 包
离线部署的本质是在另一台能联网的机器上“囤货”。以 CentOS 7 为例,先在联网机器上安装yum-utils,然后进入一个专门目录,用yumdownloader把 Docker 相关的包和依赖一次性拉下来:
mkdir -p /tmp/docker-offline yum install -y yum-utils yumdownloader --resolve --destdir=/tmp/docker-offline/ docker-ce docker-ce-cli containerd.io--resolve参数的意思是,把所有依赖包也一并下载。这一步非常关键,否则你只拿几个主包,到无网机器一装就知道什么叫“依赖地狱”。
在 Debian / Ubuntu 联网机器上,对应操作是:
mkdir -p /tmp/docker-offline cd /tmp/docker-offline apt-get download docker.io不过docker.io在 Debian/Ubuntu 仓库里是系统维护的老版本。如果需要最新 Docker CE,需要先联网把 Docker 仓库的.deb包下载好,同样放到一个目录里。我个人的经验是,如果你在离线 Ubuntu 上不想折腾 Docker 官方源依赖,直接用系统自带的docker.io包最省事,很多场景下基本功能已经够了。
3.2 直接拿二进制包:最省心的搬运方式
除了 rpm / deb,还有第三种路线:直接下载 Docker 官方发布的二进制压缩包。这个做法在离线部署里我非常推荐,因为它几乎没有包管理器的顺序问题。
从 Docker 官方 Release 页面找到对应 x86_64 平台的docker-版本号.tgz(文件名一般带有linux-amd64标识),下载下来以后,里面已经打包好了dockerd、docker、containerd、runc、docker-proxy、docker-init等全部核心二进制。整个过程不需要 yum,不需要 apt,也不需要解析一堆依赖关系。
二进制包的缺点是:没有安装记录,卸载和升级都要自己维护;systemd 服务文件需要手工创建。但这些缺点在离线环境下完全可以接受,后面第 4 节会给出完整的服务和启动配置。
3.3 下载后的完整性校验
离线环境里包很容易被 U 盘、移动硬盘、网盘中转过程中弄坏。每次转移前我都建议先做一次校验:
sha256sum docker-*.tgz md5sum docker-*.rpm把校验值记录到自己的部署文档里,传到目标机器后再算一遍。一句话,校验值和版本号别嫌麻烦,直接写进交接文档,后面排障能省很多事情。
4. 进场安装:rpm、deb、二进制三种路径实操
4.1 CentOS / RHEL 系列:本地 rpm 包批量安装
如果你走 rpm 路线,把之前收集的 rpm 包全部放到目标机器的/opt/dockeroffline/pkg目录,然后执行:
cd /opt/dockeroffline/pkg yum localinstall -y *.rpmyum localinstall会在本地目录里解析 rpm 之间的依赖顺序,自动逐个安装。相比裸用rpm -ivh *.rpm --nodeps,它更安全、不容易装完以后出现运行时缺库。
装完以后用docker version验证,能看到 client 和 server 两段信息就说明 daemon 已经在跑。
提示:离线部署时很容易看到“依赖被已安装的包冲突”这种报错,多数是因为之前机器上装过旧版本 Docker。处理办法是把旧包先用
yum remove docker docker-client docker-common清掉,再重新执行 localinstall,不要盲目加--nodeps硬装。
我在 CentOS 7.9 上实测过,顺利的话整个过程 10 分钟以内能完成,慢主要慢在 rpm 包数量多、依赖解析时间较长。
4.2 Ubuntu / Debian 系列:deb 包的依赖顺序问题
deb 路线比 rpm 更容易踩坑,因为dpkg不像 yum 那样会自动解决依赖。网上很多教程让你直接:
dpkg -i *.deb但如果软件包之间有依赖顺序,dpkg -i会在第一个缺依赖的位置报错退出。更稳妥的方式是让 apt 从本地目录安装:
cd /opt/dockeroffline/pkg apt-get install ./*.debapt 会优先在当前目录里寻找依赖,目录里找得到就一直装下去;如果报出“依赖:xxx 但是尚未安装”,说明你缺的包根本没在这个目录里,只能回到联网机器补下载。这也是为什么我一直建议,离线环境里能用二进制包就别用 dpkg,少受这种折磨。
4.3 二进制包部署:创建 systemd 服务并启动
二进制包是我在离线部署里最常用的一条路,下面给出完整流程。
先把压缩包放到/opt/docker目录并解压:
mkdir -p /opt/docker tar -xzf docker-*.tgz -C /opt/docker --strip-components=1解压后把核心二进制复制到/usr/bin,保证 PATH 里能直接访问:
cp /opt/docker/docker /opt/docker/dockerd /usr/bin/ cp /opt/docker/containerd /opt/docker/containerd-shim-runc-v2 /usr/bin/ cp /opt/docker/ctr /opt/docker/runc /usr/bin/ cp /opt/docker/docker-init /opt/docker/docker-proxy /usr/bin/ chmod +x /usr/bin/docker* /usr/bin/containerd* /usr/bin/runc /usr/bin/ctr然后创建docker.socket文件,路径在/etc/systemd/system/docker.socket:
[Unit] Description=Docker Socket for the API [Socket] ListenStream=/var/run/docker.sock SocketMode=0660 SocketUser=root SocketGroup=docker [Install] WantedBy=sockets.target再创建/etc/systemd/system/docker.service:
[Unit] Description=Docker Application Container Engine After=network-online.target firewalld.service containerd.service Wants=network-online.target Requires=docker.socket [Service] Type=notify ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock ExecReload=/bin/kill -s HUP $MAINPID TimeoutSec=0 RestartSec=2 Restart=always LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity [Install] WantedBy=multi-user.target解释一下两个关键点。Type=notify表示 systemd 会等待 dockerd 通过 sd_notify 机制发出“已就绪”通知后才认为服务启动成功,比普通Type=simple更可靠,能避免 Docker 还没准备好,后续脚本就急着执行容器命令。-H fd://表示 dockerd 从 systemd 传入的 socket 文件描述符接收请求,这就是上面Requires=docker.socket存在的原因。
接着加载并启动:
groupadd docker systemctl daemon-reload systemctl enable --now docker.service docker.socket systemctl status docker看到 active (running) 就说明核心环境已经起来了。
5. 启动阶段的三个常见拦路虎:依赖库、iptables 与 SELinux
5.1 dockerd 启动不了:先查动态库
离线环境里最容易犯的错,是主包装了但依赖库没带全。安装完启动 Docker 时,systemctl status docker显示 failed,用journalctl -u docker -n 30 --no-pager看日志,如果里面出现“error while loading shared libraries”,说明某个动态库缺失。
最直接的排查命令是:
ldd /usr/bin/dockerd | grep "not found"这个命令会把 dockerd 依赖的共享库全部列出来,凡是标着not found的,就是缺失的库文件。我遇到最多的两个:
libltdl.so.7缺失,对应 rpm 包是libtool-ltdl;libseccomp.so缺失,对应软件包是libseccomp。
回到联网机器把这些底层依赖包用yumdownloader --resolve拉下来,补装完再重启 Docker,基本能解决。
5.2 iptables 版本与内核模块不匹配
CentOS 7 环境下跑新版 Docker,另一个高频问题是在启动网络时直接失败,日志常出现类似“Failed to create NAT chain”或者“iptables v1.8.2 (nf_tables)”的报错。原因是 CentOS 7 默认启用了 nftables 体系,但旧内核的 nf_tables 支持和 Docker 预期不一致。
处理思路有两个。最常见的是把相关内核模块先加载起来,确保br_netfilter模块存在,调整内核参数:
modprobe br_netfilter echo "br_netfilter" > /etc/modules-load.d/docker.conf sysctl -w net.bridge.bridge-nf-call-iptables=1 sysctl -w net.ipv4.ip_forward=1另一个思路是强制让 iptables 走 legacy 工具集。在 CentOS/SUSE 系机器上可以检查iptables --version,如果输出带nf_tables字样,就安装iptables-legacy相关包,并把系统默认的 update-alternatives 切到 legacy 模式。这一步依赖发行版细节,不同系统命令有差异,但核心目标一致:让 Docker 在做 NAT 和端口映射时能调用到它熟悉的 iptables 接口。
5.3 SELinux 拦截容器创建
在开启了 SELinux 的 CentOS/RHEL 内网机器上,Docker 主进程能起来,但docker run拉起的容器很可能被拦截,报错内容往往是“Operation not permitted”或者“failed to create container mount”。先用getenforce确认是不是 Enforcing 状态。
如果你能联网,正确做法是安装container-selinux这个策略包,让 SELinux 认识容器标签;离线环境下,部署包清单里一定要提前把container-selinux和它的依赖带上。如果内网安全基线允许,也可以临时把 SELinux 切到 permissive 模式,但这件事需要走内部审批流程,不能盲目操作,也不能简单在/etc/selinux/config里改完就完事,还要确认不会影响其他安全组件。
6. 镜像怎么进内网:docker save/load 与本地 Registry
6.1 单机场景:save 与 load 的组合
Docker 环境装完只是第一步,真正的核心是业务镜像能不能进去。内网单机上最直接的做法是用docker save在镜像来源机器上导出,再在目标机器上导入。
来源机器(一般是能联网的开发机或镜像构建机)上执行:
docker save -o myimages.tar nginx:1.25 redis:7.2 mysql:8.0生成一个myimages.tar文件,大小取决于镜像本身。把这个 tar 传到目标机器,执行:
docker load -i myimages.tarload 完成以后docker images就能看到镜像了。
这里有个容易误操作的点:如果你想迁移的镜像非常多,建议按业务域分几个 tar 分别导出,不要一口气全塞进一个大 tar。原因是大 tar 在中断恢复时很难定位位置,而且离线拷贝文件体积超过 U 盘或移动硬盘限制会很尴尬。
还有一个来自架构的坑:docker save出来的镜像本身带平台信息。如果你在 ARM 开发机上导出了一个 arm64 版本的镜像,拿到 x86_64 目标机 load 之后再运行,会直接报exec format error。所以导出前一定要用docker images或docker inspect确认镜像架构是amd64或者linux/amd64。
6.2 多机场景:搭一个内网镜像仓库
如果离线环境里有三五台机器都要拉镜像,一台台docker load就很低效。更聪明的做法是找一台机器当内网镜像分发点,跑一个最小化的 Registry。
先在有镜像的机器上导出registry:2:
docker save -o registry.tar registry:2把registry.tar传到内网选定的仓库机上 load 并启动:
docker load -i registry.tar docker run -d --name registry --restart=always \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ registry:2其他机器要拉镜像,先在/etc/docker/daemon.json中写入:
{ "insecure-registries": ["192.168.10.20:5000"] }这里192.168.10.20:5000需要替换成你的仓库机内网地址。然后重启 Docker:
systemctl restart docker接着就可以在任意一台机器上:
docker pull 192.168.10.20:5000/myimage:v1别忘了用curl http://192.168.10.20:5000/v2/_catalog检查仓库是否正常,返回一个 JSON 列表说明 Registry 没问题。内网没有 DNS 的时候,推荐直接用 IP 访问,少去配置 hosts 的麻烦。
6.3 大镜像分层的传输优化
对于特别大的镜像,我更习惯用另外两个小技巧。一个是用docker save导出后先压缩再拷贝:
docker save myimage:latest | gzip > myimage.tar.gz目标机上:
gunzip -c myimage.tar.gz | docker load压缩率在 30% 到 60% 之间,传输时间能明显缩短。另一个技巧是,目标机器上如果已经有相同的基础镜像层,可以直接在镜像来源机器上docker export容器,而不是导出镜像,但这种方式会丢掉镜像历史,比较适合只想搬一个最终文件的场景,不是常规推荐路线。
7. 上线前后的维护心得与避坑清单
7.1 版本固定:离线环境经不起临时升级
离线环境最大的特点就是“动了手难得回滚”。我在实际部署里强烈建议把 Docker 版本完全固定下来,不要动不动追最新版。一旦选定一个版本,就把版本号、校验值、依赖包清单、部署日期全部记录到一个文本文档里,跟离线安装包一起归档。以后万一环境出问题要重装,或者需要给另一批机器做同样部署,直接照方抓药。
镜像也一样,尽量别用latest标签。离线环境里latest的含义非常模糊,一旦你在开发机上重新 pull 了一次,镜像内容可能就悄悄变了,到了内网才发现行为对不上。直接把版本标签写死,比如nginx:1.25、mysql:8.0.36,内网环境不会撒谎,写死什么就跑什么。
7.2 数据备份与重启策略
离线环境里通常没有云厂商的自动快照,所以数据备份要自己规划好两件事。第一件,镜像本身用docker save定期备份到独立磁盘目录,备份的时候排除临时容器和数据卷;第二件,容器尽量设置自动重启策略,尤其是一台机器重启以后希望容器跟着拉起来:
docker run -d --restart unless-stopped ...这个策略表示:容器如果被手动停止,下次开机不会自动启动;机器重启引起的容器退出则会自动拉起。运维上比--restart always更可控,不会出现“想停掉保安全,结果开机又自己起来了”的窘境。
7.3 时间同步往往是最容易被忽略的细节
说到离线环境,时钟问题经常被遗忘。容器日志、认证、调度的很多时间戳依赖宿主机时钟,如果内网机器不能通过 NTP 对时,时间漂移会越来越大,排查问题时很容易被误导。离线环境再不方便,也建议在部署清单里加上至少一台上游时间源,或者部署一个内网 NTP 服务。我在这上面吃过亏:容器里日志时间和宿主机差了好几个小时,一开始完全想不到是时钟问题,白白排查了很久。
最后再说一个实操体会:整个流程走下来,离线装 Docker 最难熬的不是某个命令,而是节奏。很多人习惯拿到包就装,装不上就直接从网上搜报错,结果越折腾越乱。我的经验是严格按“宿主系统确认架构 → 依赖包补齐 → 装核心二进制 → 启动验证 → 镜像 load 进内网仓库 → 业务容器跑起来”这个顺序来,每一步验证通过再进下一步,基本不会走到不可收拾的地步。祝各位在内网部署时少点折腾,一次过。