上个月帮客户部署一套容器化的内网交付环境,机器清一色是Kylin Server V10(Halberd),机房跟外部网络物理隔离。业务要求在这些机器上装好Docker,并且装完得能稳稳当当跑起来。听起来就六个字“离线安装Docker”,实际做的时候才发现,问题全藏在细节里:rpm依赖缺一串、x86和ARM的包不能混用、iptables版本对不上、SELinux插一脚、firewalld再搅和一把。这篇文章就把我这次完整走通过的路程记录下来,从环境确认到包准备,从安装配置到排障,再到离线镜像搬运,每一步的理由和替代方案都尽量讲清楚。适合正在做内网交付、准备在Kylin Server上跑容器化应用的运维和交付同学参考。
1. 离线安装Docker真正的坎:不只是拷rpm包
很多人第一反应是:找台能联网的机器,把Docker的rpm下载下来,传到目标机器上rpm -ivh一把梭。这个流程在CentOS上可能一次就过,但放到Kylin Server上,很少这么顺利。原因很简单,Docker的rpm包依赖链比想象中长,至少涉及containerd、runc、container-selinux、iptables、libseccomp这些组件。如果离线包里没带全,安装器会直接甩一句Requires: xxx is needed by docker-ce,然后戛然而止。
1.1 为什么不能直接拷二进制文件
Docker说到底是dockerd这个daemon加上一堆客户端工具,理论上下载官方发布的二进制tarball,解压放到/usr/bin就能跑。我在CentOS上也这么干过,确实能起来。但在Kylin Server上,二进制方式会带来三个麻烦:
- systemd的service文件得自己写,开机自启、日志处理全要手动维护,交付时不规范。
- runc、containerd、docker-ce三者之间的版本匹配关系容易搞乱,网上很多教程还是旧版组合,跑新镜像时接口不兼容。
- 项目交付时审计要装机记录,拷二进制这种方式显得太“野”,rpm安装则能通过
rpm -qa清晰追溯到每个包的版本和来源。
所以离线安装Docker,走rpm路线是更规范也更稳的选择。既然决定用rpm,就必须正视依赖链。这也是为什么很多人拿着一个docker-ce的rpm跑到内网装,卡在container-selinux、iptables这些依赖上动弹不得。
1.2 Kylin Server与CentOS源的兼容性判断
Kylin对外宣称兼容RHEL生态,但它不是CentOS,yum源里的包版本、repo文件内容都有自己的一套。判断能不能用某个源的包,最靠谱的指标是glibc版本。
rpm -q glibc看到版本是2.28及以上的,就优先考虑el8系列(RHEL 8 / CentOS 8)的Docker包。如果Kylin版本比较老,glibc低于2.28,那就退回el7的包。我这次碰到的Kylin Server V10版本,glibc是2.28,所以直接选el8的Docker rpm包,一次装上没问题。
还有个细节得注意:Kylin V10的repo文件里,$releasever可能被解析成7或者10,而Docker官方仓库的路径是/linux/centos/$releasever/,里面根本没有10这个目录。配置yum源的时候如果不管这个变量,执行yum install docker-ce会报404。处理方式就是在repo文件里手动把$releasever替换成8。
1.3 出发前必须确认的三条命令
在联网机器上开始下载包之前,先在目标机器上把这三条命令的输出记下来,它们决定了你在联网机器上要下什么包。
cat /etc/kylin-release arch rpm -q glibccat /etc/kylin-release拿到系统具体版本,比如V10 SP1还是SP2;arch决定下载x86_64还是aarch64的包;rpm -q glibc决定该选el7还是el8的包。三者的组合判断如下表:
| 系统版本 | 机器架构 | glibc版本 | 推荐的rpm目录 |
|---|---|---|---|
| V10 SP1 | x86_64 | 2.28 | centos/8/x86_64 |
| V10 SP1 | aarch64 | 2.28 | centos/8/aarch64 |
| 老版本V10 | x86_64 | 2.17 | centos/7/x86_64(备选) |
这个判断表看起来简单,但能帮你少传几十个没用的包。架构选错是最直接的坑——x86_64的rpm包传到aarch64机器上,安装直接报wrong ELF class,根本装不进去。
2. 备料环节:需要下载哪些rpm,以及怎么一次拉齐
确认完环境和架构,下一步是去联网机器上把包备齐。这一步的关键不是“下载docker-ce这一个包”,而是把整条依赖链一次性拉下来。
2.1 Docker rpm家族有哪些成员
以docker-ce 24.0.x版本为例,离线包里至少要包含下面几个成员:
docker-ce:主程序,提供dockerd daemon。docker-ce-cli:docker命令行工具,没有它你没法敲任何docker命令。containerd.io:容器运行时,新版docker-ce默认用它来管理容器生命周期。docker-buildx-plugin:构建镜像用的Buildx插件,现在官方默认带。docker-compose-plugin:docker compose插件,有了它才支持docker compose命令。container-selinux:SELinux策略包,Kylin如果开了SELinux,没有这个包,docker容器启动会报权限错误。
除了上面几个,还有一批系统级依赖,比如iptables、libnfnetlink、libnetfilter_conntrack、libseccomp、policycoreutils-python-utils等。这些通常来自系统基础源,而不是Docker源,离线打包时很容易漏掉。
2.2 用yumdownloader一次拉全依赖
如果联网机器本身是CentOS 8或者同架构的机器,配置好docker-ce的yum源之后,可以用yumdownloader这个工具把依赖递归解析出来。
# 安装工具 yum install -y yum-utils # 配置docker-ce源,注意将$releasever替换成8 curl -o /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sed -i 's/\$releasever/8/g' /etc/yum.repos.d/docker-ce.repo # 指定目录一次性下载 mkdir -p /opt/docker-rpm yumdownloader --resolve --destdir=/opt/docker-rpm \ docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin container-selinux--resolve选项会递归解析依赖,把缺的包一并下载到指定目录。这个命令跑完,/opt/docker-rpm里通常会多出十几个包,其中就有iptables相关组件。
2.3 没有yum-utils时的手工下载方案
有些内网交付的联网跳板机很干净,连yum-utils都不愿意装。这时候就得手工下载。以阿里云Docker镜像源为例,路径是:
https://mirrors.aliyun.com/docker-ce/linux/centos/8/x86_64/stable/Packages/浏览器打开这个目录,能看到所有稳定版包列表。把需要的包下载到本地,再按文件名挑出对应架构的版本即可。
mkdir -p /opt/docker-rpm cd /opt/docker-rpm wget https://mirrors.aliyun.com/docker-ce/linux/centos/8/x86_64/stable/Packages/docker-ce-24.0.9-1.el8.x86_64.rpm wget https://mirrors.aliyun.com/docker-ce/linux/centos/8/x86_64/stable/Packages/docker-ce-cli-24.0.9-1.el8.x86_64.rpm注意,手工下载容易漏的是依赖包。比如container-selinux需要policycoreutils-python-utils,如果跳板机能联网,建议还是先配好源,让yumdownloader帮你把这些边角料包一并收集。
2.4 一份可复用的离线包清单
下面是这次实际用过的一份包目录,版本号以仓库实时列表为准,但包名结构是通用的:
| 包名 | 说明 |
|---|---|
| docker-ce-24.0.9-1.el8.x86_64.rpm | Docker主程序 |
| docker-ce-cli-24.0.9-1.el8.x86_64.rpm | Docker命令行 |
| containerd.io-1.6.28-3.1.el8.x86_64.rpm | 容器运行时 |
| docker-buildx-plugin-0.14.1-1.el8.x86_64.rpm | Buildx插件 |
| docker-compose-plugin-2.28.1-1.el8.x86_64.rpm | Compose插件 |
| container-selinux-2.229.0-1.el8.noarch.rpm | SELinux策略 |
| libnetfilter_conntrack-*.rpm | 依赖包 |
| libnfnetlink-*.rpm | 依赖包 |
| iptables-*.rpm | Docker网络依赖,版本要满足要求 |
下载时永远记住一句话:宁可多带,不要少带。离线环境里缺一个依赖包,就意味着要重新找机器下载、重新刻盘或者走审批流程传递文件,时间成本高得吓人。
3. 目标机器的完整安装流程:从上传rpm到开机自启
包备齐了,接下来就是上目标机器操作。这一步如果按部就班来,非常快,全程十分钟内能搞定。
3.1 上传包后的第一件事:先体检
先把整个rpm目录用U盘、scp或者内部文件传输通道拷贝到目标机器,统一放到/opt/docker-rpm。上传后不要急着安装,先做两件事:
cd /opt/docker-rpm ls -lh rpm -qip docker-ce-24.0.9-1.el8.x86_64.rpmls -lh看文件大小,防止传输过程中产生半截文件。如果一个包的大小和源端差太多,别用,重新传。rpm -qip查看包的元信息,确认架构是x86_64还是aarch64,确认Source是el8还是el7。这一步能拦截掉百分之九十的架构错误和下载错误。
3.2 安装顺序:先把依赖喂饱
不建议直接rpm -ivh *.rpm一股脑装,虽然rpm会自动跳过已安装的包,但遇到依赖缺口时它会停下来,而且报错信息不直观。更稳的做法是分两步走:
# 第一步:解决selinux和系统依赖 yum localinstall -y container-selinux-*.noarch.rpm policycoreutils-python-utils-*.rpm 2>/dev/null rpm -Uvh libseccomp-*.rpm libnfnetlink-*.rpm libnetfilter_conntrack-*.rpm iptables-*.rpm 2>/dev/null # 第二步:安装docker本体和组件 rpm -Uvh containerd.io-*.rpm docker-ce-cli-*.rpm docker-ce-*.rpm rpm -Uvh docker-buildx-plugin-*.rpm docker-compose-plugin-*.rpm为什么要先装container-selinux?因为docker-ce的rpm包在metalink里明确写了Requires: container-selinux >= 2:2.95,不满足这个依赖,docker-ce是装不进去的。而container-selinux又依赖policycoreutils-python-utils,所以这两个包得排在最前面。
如果报runc版本不满足containerd.io的要求,别慌。目标机器上通常自带runc,但版本可能偏老,比如1.0.0-rc95这种,而新版containerd.io要求runc >= 1.1.0。解决办法是去Kylin基础源或者EPEL源里找对应架构的runc 1.1.x包,一起带进来升级。
3.3 配置daemon.json:比默认配置多走一步
装完rpm包后,/etc/docker/目录还不存在,需要手动创建并写入daemon.json。这份配置我建议一开始就写好,不要用默认配置跑,否则后面迁移数据目录、限制日志都要重启服务,麻烦。
mkdir -p /etc/docker /data/docker cat > /etc/docker/daemon.json <<'EOF' { "data-root": "/data/docker", "exec-opts": ["native.cgroupdriver=systemd"], "storage-driver": "overlay2", "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "iptables": true } EOF这里解释几个关键项。>systemctl daemon-reload systemctl enable --now docker systemctl status docker
systemctl status看到Active: active (running)之后,再用两条命令验证:
docker version docker info内网环境没有hello-world镜像,所以不能靠docker run hello-world来验证。只要docker version里Client和Server两端都有版本信息,就说明daemon正常启动了。docker info可以再看一眼存储驱动是否是overlay2、Cgroup驱动是不是systemd。
4. 装完必翻车的四个细节:权限、防火墙、存储和日志
Docker装完并不等于万事大吉,下面这几个问题我几乎每次都能碰到一两个,提前处理能省下大量排障时间。
4.1 docker组:为什么普通用户执行docker会失败
rpm安装完成后,Docker会自动创建docker组,但当前登录的普通用户并不在这个组里。普通用户执行docker ps会看到:
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock原因是/var/run/docker.sock的权限属于root:docker,普通用户没权限访问。解决办法是把用户加进docker组,然后重新登录:
usermod -aG docker kylinuser注意,重新登录的意思是退出当前SSH会话再重新连接,单纯newgrp docker开一个新shell也可以,但更彻底的做法是退出重连。不生效的话,检查id kylinuser看是否已经包含docker组。
4.2 firewalld和iptables的隐形冲突
这是离线安装后最闹心的问题。表现现象是:容器起来了,docker port能看到映射,但外部访问不通。原因通常是Docker会直接操作iptables规则来配置端口映射,而Kylin自带的firewalld在nftables框架下管理网络过滤,两边互不认识,产生规则冲突。
处理办法分情况。如果是生产环境且防火墙必须开着,那就不要停firewalld,而是把需要对外提供服务的端口在firewalld里放行:
firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --reload如果是测试验证环境,可以直接停掉firewalld,再重启一次Docker:
systemctl stop firewalld systemctl restart docker重启Docker之后,用iptables -t nat -L -n | grep DOCKER检查NAT表里有没有Docker的端口映射规则。有,说明端口转发链路正常。
4.3 overlay2存储驱动检查
Docker默认会优先用VFS、overlayfs等驱动,但在Kylin Server上如果内核模块没加载,存储驱动会自动降级为VFS。VFS的性能非常差,跑几个容器后磁盘占用会暴涨数倍。
检查当前驱动:
docker info | grep -A2 "Storage Driver"如果输出是overlay2,那就没问题。如果显示vfs,多半是br_netfilter模块没加载,或者/etc/modules-load.d/里没配开机加载。需要手动加载:
modprobe br_netfilter echo "modprobe br_netfilter" >> /etc/rc.local chmod +x /etc/rc.local sysctl -w net.bridge.bridge-nf-call-iptables=1这个操作同时也能解决一部分容器间通信、端口映射不通的问题,因为br_netfilter是Docker网桥流量经过iptables过滤的前提。
4.4 日志分区被占满
数据目录改到/data/docker之后,如果忘了限制日志,容器产生的json日志文件会直接灌到/data/docker/containers目录下。一个调试阶段打日志频繁的容器,一天写几个G跟玩一样。
这里有个容易忽略的点:daemon.json里的max-size和max-file只对之后创建的新容器生效,已经跑起来的容器不会自动回收日志。所以部署容器时要先确认日志参数生效,再启动容器。如果已经在跑容器,日志文件又很大,只能重建这个容器来让限制生效,或者手动清空/var/lib/docker/containers/<id>/*.log。
5. 离线的镜像怎样进来:save/load、内网Registry和compose
Docker装好了,接下来最大的问题是:镜像从哪里来?内网没有外网访问能力,镜像搬运就成了一件绕不开的事。
5.1 单机场景:docker save / docker load
如果只有一两台机器,docker save加docker load是最直接的方案。在联网机器上把镜像导出成tar包,传到目标机器再导入。
# 联网机器上 docker pull nginx:1.24 docker save nginx:1.24 | gzip > nginx-1.24.tar.gz # 目标机器上 docker load -i nginx-1.24.tar.gz这里有几个实践经验。第一,导出和导入的机器架构必须一致。x86_64的机器上拉下来的镜像是amd64架构的,传到aarch64机器上跑不起来。所以备料阶段就要确定目标机器是ARM还是x86,然后在同架构的联网机器上拉镜像。第二,多个镜像不要打成一个tar。一次导入一个大tar,如果中间某个镜像失效,整个导入可能失败,而且排查定位都费劲。单镜像单文件,传一个导一个,虽然文件多一点,但稳。第三,大镜像建议先gzip再传,压缩率通常能到30%到50%,能省不少传输时间。
5.2 多机场景:内网Registry才是正解
机器一多,比如十台以上,save/load的工作量就会变得非常大。这时候更合理的方案是内网搭一个镜像仓库。
在内网一台存储足够的机器上部署Registry或者Harbor,然后把目标机器的daemon.json里加上insecure-registries字段:
{ "insecure-registries": ["registry.internal:5000"] }之后在联网机器上docker pull镜像,docker save导出,传到Registry所在机器上docker load,再改tag并docker push到内网Registry。所有内网机器再从Registry拉取镜像。
docker tag nginx:1.24 registry.internal:5000/nginx:1.24 docker push registry.internal:5000/nginx:1.24这个方案的优点是镜像统一管控,版本升级时只push一次,内网机器pull就行,不用再逐台搬运tar包。
5.3 compose插件和后续升级
离线环境下装docker-compose,现在不用再去GitHub上单独下载那个二进制了。只要在备料阶段把docker-compose-plugin这个rpm包一起带上,安装后就能直接用:
docker compose version能输出版本号,说明compose插件已经生效。后续升级Docker也一样,把新版docker-ce及相关rpm包带进内网,rpm -Uvh覆盖安装即可,正在运行的容器不受影响,只要在窗口期内重启一下dockerd就行。
5.4 一个小经验
每次做离线交付,我都会在目标机器上把docker version的输出和一份安装记录存档,同时在联网机器上把整个rpm目录和验证过的daemon.json模板打包,存成一套“交付套件”。下次再遇到Kylin Server V10的机器,一小时内就能装完。装完第一时间docker info截图存档,这个动作能让后期的故障排查省很多事。