在外面跑运维的人应该都遇到过这种场景:机器在隔离网段,物理上就跟公网隔开了,系统装完第一件事想执行yum install,结果一串Could not resolve host把心态直接搞崩。上周我帮客户处理一台 CentOS 7 的离线服务器,花了大半天把整个离线 yum 源从零搭完,整个过程踩了不少坑,也总结出一套可以复现的流程。
这篇内容适合谁?两类人。一类是刚接手内网环境的新手运维,不熟悉 yum 仓库结构,遇到断网就只会拿 U 盘来回拷 rpm 还老缺依赖。另一类是做等保、内网穿透部署的老手,想把自己手里那套“半自动化”离线源方案整理得更规范。离线 yum 源能解决的核心问题,就是让无外网机器依然可以用 yum 去装软件、自动解析依赖,而不是依赖rpm -ivh人肉排错。
我先把结论放在最前面:离线源的完整链路只有四步——在联网机器上把需要的 rpm 包连依赖一起下载;用createrepo给包目录生成 repodata 元数据;通过 U 盘或内网传输把仓库搬到目标机器;写.repo文件让 yum 指向本地目录或局域网 HTTP 地址。下面我按实操顺序把每一步的细节和坑都拆开讲。
1. 离线源这件事,难的不是命令,是思路
很多新手第一次接触离线源,下意识的想法是“我直接把 rpm 包拷过去装上不就行了”。单机装一两个包确实可以,但一旦遇到几十个依赖的软件,比如装个图形库、数据库客户端,rpm 手动依赖解析能做到怀疑人生。yum 之所以好用,核心在于它读取仓库根目录下的 repodata 元数据,里面记录了每个包的文件清单、依赖关系、校验信息,装包时自动在仓库里找依赖。
所以要实现离线 yum,本质上不是“拷包”,而是要把一个完整的、带元数据的软件仓库搬运到目标环境。这是一个思路转换问题。
1.1 为什么内网环境是刚需
我见过不少客户的真实情况:机房建在独立网段,出口有严格的访问控制,公网 DNS 都不通。部署的机器可能是等保二级、三级环境,也可能是生产区、测试区、办公区之间的隔离区。在这些环境里,运维装软件只有三条路:找厂商要安装包、自己人工拷包、搭一个内部软件源。前两条路短期应急可以,长期维护一定会出问题,因为补丁、安全更新完全跟不上。
自己搭一个离线源,一次准备,后面所有机器都能用。这也是这块工作最大的价值:它不是一锤子买卖,而是一套可以持续使用的内部基础设施。
1.2 离线源的两种形态:目录源和局域网源
根据目标环境的大小,离线源有两种典型形态。
第一种是单机目录源,适合一两台机器。把仓库文件夹放到服务器本地磁盘,配置baseurl=file:///路径,yum 直接读取本地文件。优点是零额外组件,缺点是如果机器多了,每台都要拷一份。
第二种是局域网 HTTP 源,适合几十台甚至上百台机器。找一台能通内网的机器做仓库服务器,用 Nginx 或 Apache 把仓库目录通过 HTTP 暴露出来,其他机器配置baseurl=http://仓库服务器IP/路径。优点是一份仓库全网共享,管理方便,缺点是要维护一台仓库服务器。
具体选哪种,取决于机器数量和你的维护周期。我个人的经验是:机器超过三台,就直接上 HTTP 源,不要各拷各的,否则补丁更新时你会疯掉。
1.3 动手前先想清楚的三个问题
第一个问题,目标系统的版本和架构。CentOS 7 的 x86_64、CentOS 7 的 aarch64,它们对应的仓库内容是不同的,下载包时必须用相同版本和架构的机器去下载,否则装的时候一堆冲突。
第二个问题,需要哪些软件集合。是只要基础工具,还是要覆盖某个业务依赖的完整堆栈?建议按业务需求列一个软件清单,不要贪多。仓库里包越多,createrepo 生成的元数据越大,后期同步时间越长。
第三个问题,多久更新一次。离线环境不是永远不更新,安全补丁还是要打的。想清楚更新频率,决定了你要不要提前规划增量同步的策略。这三个问题想清楚,后面所有操作都是水到渠成的事。
2. 准备物料:在联网机器上把 RPM 包连依赖一起拉下来
离线源的第一步,是在一台可以联网、系统和目标机器版本一致的机器上,把需要的 rpm 包全部下载到本地。这一步的关键不是下载软件包本身,而是把它的依赖树完整地拉下来。
2.1 三个下载工具怎么选
下载 rpm 包最常用的工具有三个:yumdownloader、repotrack和dnf download。CentOS 7 上主要用前两个,CentOS 8 及以上的机器用dnf download更多。
先说yumdownloader,它属于yum-utils包,基本用法是:
yumdownloader --resolve --destdir=/opt/localrepo nginx--resolve参数的意思是同时下载依赖,--destdir指定保存目录。这个工具适合下载单个或少量软件包,但它有局限性——它只会下载指定软件包的依赖,如果依赖本身还有依赖,某些版本可能不会全量递归。
再说repotrack,同样属于yum-utils,用法更简单:
repotrack -p /opt/localrepo nginx-p指定保存目录。repotrack 会完整记录依赖链,把该软件包以及所有依赖全部下载,包括依赖的依赖。实测下来,处理复杂依赖时 repotrack 比 yumdownloader 更稳,我习惯把它作为主力工具。
CentOS 8 上,优先用 dnf 自带的能力:
dnf download --resolve --alldeps --destdir=/opt/localrepo nginx--alldeps会拉取所有平台的依赖,某些场景下包会多一点,但对离线环境来说宁多勿缺。
提示:下载时建议在干净系统上操作,也就是只挂官方基础源,不要混入其他第三方源。混用第三方源会导致依赖包版本混乱,拷到目标机器上装的时候容易出现无法解决的冲突。
2.2 官方源和 EPEL 源怎么处理
CentOS 只有 base、extras、updates 这几个默认源,它们里面的包相对克制。但是很多常用软件,比如htop、python3-pip之类,在默认源里没有,需要 EPEL 源。如果你确定目标离线环境需要 EPEL 的包,必须在下载机上提前装好 EPEL 源,然后把epel-release这个包本身也放进离线仓库。
epel-release这个包比较特殊,它本身来自 EPEL 源,但它又是开启 EPEL 的钥匙。我的习惯是,在建离线源的仓库根目录下单独放一个epel-release的 rpm 包,当目标机器需要装 EPEL 软件时,先手动rpm -ivh epel-release-latest-7.noarch.rpm导入 EPEL 源配置,再用离线 yum 源里的 EPEL 目录装软件。
如果目标机器上已经配置的源指向公网,记得在离线配置完成后禁用或注释掉原来的公网源,否则 yum 会先去尝试连外网,超时后才会走本地源,每次操作会卡很久。
2.3 直接把缓存目录搬过去可行吗
有人会问:联网机器上执行yum install后,rpm 包缓存都在/var/cache/yum,把这个目录整个拷到离线机器不就行了吗?理论上可以,但实际上不推荐,原因有三。
第一,缓存目录的目录结构是按仓库、架构、包名组织的,直接搬过去 yum 不会自动识别为可用仓库,要手动拼 repodata,非常麻烦。
第二,缓存里的包是“装过的”,不代表“完整的”,很多装系统时已经存在的依赖不会出现在缓存里,别的机器装同一个软件时会缺依赖。
第三,缓存目录里往往同时混着多个源、多个版本的包,完整性无法保证。
如果你一定要用缓存目录,正确做法是从缓存目录把 rpm 包文件全部提取出来,放到同一个平铺目录,然后用 createrepo 重新生成元数据,不要直接把缓存目录当成仓库。平铺方式虽然简单粗暴,但配合 createrepo 之后是可靠的。
2.4 打包搬运的体积优化
下载完的 rpm 包积累多了之后,仓库体积会很大,一套基础工具加 EPEL 常用软件,动辄几个 GB。拷贝的时候可以考虑先打成 tar 包再搬运,注意 CentOS 7 自带的 tar 版本比较老,大文件用tar -czf可能效率不高,我用的是:
tar -czf localrepo.tar.gz /opt/localrepo如果仓库超过 4GB,建议保持文件系统格式支持大文件,U 盘用 exFAT 或 NTFS,拷贝前先检查目标机器挂载的分区剩余空间。还有一个细节:FAT32 的 U 盘单文件不能超过 4GB,包文件超过这个限制会拷不进去,这是我在现场被坑过一次的问题。
3. 生成仓库元数据:createrepo 是真正的核心
所有 rpm 包放到一个目录后,这个目录还不能直接被 yum 使用。yum 真正读取的是目录下repodata子目录里的元数据文件。生成这个元数据的工具就是createrepo,这一步是整个离线源搭建的灵魂。
3.1 repodata 里到底装了什么
如果你把一个在线源的 repodata 目录列出来,会看到类似这样的文件:
repodata/repomd.xml repodata/xxxxx-primary.xml.gz repodata/xxxxx-filelists.xml.gz repodata/xxxxx-other.xml.gzrepomd.xml是入口文件,记录了其他元数据文件的位置和校验值。primary.xml.gz里是每个 rpm 包的名字、版本、依赖关系、提供能力等核心信息,yum 解决依赖时主要靠它。filelists.xml.gz记录每个包包含哪些文件,安装时检查文件冲突会用到。other.xml.gz存储一些 changelog 之类信息,相对没那么关键。
所以当你手动createrepo一个本地目录时,本质上就是在生成一份和在线源结构一致的元数据。对 yum 客户端来说,它不关心仓库是来自公网还是本机目录,只关心repomd.xml是否存在、内容是否合法。
3.2 生成元数据的标准操作
先在下载机上安装 createrepo:
yum install -y createrepo接着对包目录执行:
createrepo /opt/localrepo执行完成后,/opt/localrepo/repodata目录就出现了。这个过程会扫描目录下所有 rpm 包,逐个解析包头信息,生成 XML 元数据并压缩。包的数量越多,耗时越长,几百个包一般几十秒到几分钟,上万个包可能要等一阵。
执行完以后检查一下:
ls -lh /opt/localrepo/repodata/ cat /opt/localrepo/repodata/repomd.xml | head -20repomd.xml里能看到<revision>标签,记录的是元数据生成时间戳。不要小看这个时间戳,后面做增量更新时,它就是你判断仓库是否更新的依据之一。
注意:createrepo 生成的元数据只覆盖“执行时目录里已有的包”。如果你后面往目录里新增了 rpm 包但没有重新执行 createrepo,目标机器上
yum install就找不到新包。这不是 yum 出故障,而是元数据没刷新。
3.3 增量更新与多架构并存
仓库首次创建后,后续添加新包不需要全量重新生成。用--update参数可以增量更新元数据:
createrepo --update /opt/localrepo--update的原理是读取已有的 repodata,只对新增或变更的包做分析,然后重写元数据文件。对大型仓库来说,这个参数能把刷新时间从几分钟缩短到几秒。
如果仓库里同时要放 x86_64 和 aarch64 两种架构的包,建议不要混在一个目录里。最稳妥的做法是分目录管理:
/opt/localrepo/x86_64/ /opt/localrepo/aarch64/每个子目录独立执行 createrepo,客户端配置时按架构指向对应子目录。混在一个目录里,createrepo 也能处理,但 yum 安装时可能出现架构不匹配的重复包,自找麻烦。
3.4 校验与完整性检查
元数据生成完,搬运到目标机器之前,强烈建议做一次完整性校验。一个常见问题是:rpm 包在拷贝过程中损坏,导致目标机器报Metadata file does not match checksum。
我自己常用的校验方法是,在下载机上先算好整个目录的 MD5 校验文件:
cd /opt && md5sum localrepo.tar.gz > localrepo.md5拷到目标机器后执行:
md5sum -c localrepo.md5如果校验不通过,不要解压直接装,老老实实重新拷贝。现场赶时间硬着头皮解压安装,后面报错排查的耗时一定远超重新拷贝的时间,这个亏我吃过。
4. 客户端配置:让 yum 认你的本地仓库
仓库到了目标机器上,剩下的工作就是让 yum 知道从哪里读取。这一步看起来简单,其实坑都在细节里,我把目录源和 HTTP 源分开讲。
4.1 .repo 文件的正确写法
yum 的仓库配置放在/etc/yum.repos.d/目录,每个仓库一个.repo文件,文件名任意但建议语义清晰。仓库文件的核心结构是:
[localrepo] name=Local Repository baseurl=file:///opt/localrepo enabled=1 gpgcheck=0[localrepo]是仓库 ID,不能和已有仓库 ID 重复,否则后加载的会覆盖前面的。baseurl是仓库根目录的地址,这里注意:本地路径必须写成file:///加绝对路径,不能省略协议头。很多新手写baseurl=/opt/localrepo然后执行yum clean all,发现仓库没生效,就是因为少了file://。
如果仓库目录下还有架构子目录,可以把 baseurl 写成:
baseurl=file:///opt/localrepo/$basearch$basearch是 yum 自带的变量,会自动替换为x86_64或aarch64。同理还有$releasever,会自动替换成大版本号。这些变量在配置多版本仓库时特别有用。
4.2 配置完成后必须执行的两个命令
写完.repo文件,不要急着直接yum install。先清缓存再建缓存:
yum clean all yum makecacheyum clean all是清掉旧缓存,避免 yum 读取到以前在线源留下的过期数据。yum makecache是让 yum 重新读取所有可用仓库并生成缓存。执行完makecache后,可以用:
yum repolist查看当前启用的仓库列表,确认本地源的名字已经出现。这一步能看到仓库是否被识别,比直接装软件更快速定位问题。
4.3 本地缓存模式与离线源的关系
CentOS 7 的 yum 有--cacheonly选项,意思是仅使用本地缓存,不尝试连接远程仓库。离线源配置好之后,如果要确认 yum 完全没有走网络,可以执行:
yum --cacheonly install nginx如果本地仓库里有这个包,它能正常安装。这个选项不能替代离线仓库,但可以作为验证手段,确认你的仓库是真离线还是假离线。
4.4 GPG 签名问题怎么处理
rpm 包默认都带 GPG 签名,仓库源也带了公钥。离线源最常见的一类报错就是这样:
warning: /var/cache/yum/.../package.rpm: Header V4 RSA/SHA256 Signature, key ID xxxx: NOKEY出现这个报错,说明目标机器上没有导入对应仓库的 GPG 公钥。注意,在线源可以自动从公网拉取公钥,离线环境没网,公钥必须提前手动导入。解决方式有两种。
第一种,把官方 GPG 公钥手动导入。CentOS 7 的公钥文件路径通常是/etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7,如果你已经把整个系统装好了,这个文件一般已经在机器上,直接执行:
rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7如果是 EPEL 的包,导入对应公钥:
rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7第二种,图省事的话,在.repo文件里把gpgcheck=0,关掉验签。内网环境如果安全要求不是特别高,这种方式能少很多麻烦。但正式生产环境,尤其是做等保测评的机器,建议保持验签开启,不要关。我个人的习惯是验签开着,提前把公钥导入,一劳永逸。
注意:即使仓库配置了
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7,如果公钥文件不存在,导入一样会失败。离线环境里,公钥文件本身也是需要保障的前置条件。
5. 从单机到局域网:离线源进阶玩法
单机离线源能用,但管理效率低。机器一多,最佳实践是搭一个小型局域网仓库,一台仓库服务器,全网共享一份离线源。
5.1 用 HTTP 服务把仓库目录共享出去
仓库服务器上,我习惯用 Nginx 来做静态文件服务,配置极简。CentOS 7 上先安装:
yum install -y nginx然后在 Nginx 配置里加上一个 server 块:
server { listen 80; server_name repo.local; root /opt/localrepo; autoindex on; }autoindex on这句很关键。yum 客户端访问仓库时,需要能列出目录内容,否则某些情况下会报 403 或找不到 repodata。启动服务后,先在浏览器里访问http://仓库服务器IP/repodata/repomd.xml,如果能正常显示 XML 内容,说明 HTTP 服务没问题。
客户端配置改为:
[localrepo] name=Local Repository baseurl=http://192.168.1.10/localrepo enabled=1 gpgcheck=0我的实践中,不建议在 baseurl 里写域名而写 IP,离线环境 DNS 解析不一定可靠,写 IP 最稳妥。如果网络环境里有严格的 ACL 限制,记得在防火墙上放行仓库服务器的 80 端口,客户端到服务器方向。
5.2 局域网源的防火墙与权限细节
Nginx 默认监听 80 端口,CentOS 7 自带的 firewalld 默认会挡掉外部访问。仓库服务器上要执行:
firewall-cmd --permanent --add-service=http firewall-cmd --reload如果用 Nginx 做访问控制,比如只允许内网某网段访问,可以加:
allow 192.168.1.0/24; deny all;这个在安全要求高的内网里很实用。另外一个细节:如果客户端通过 IP 访问,Nginx 的server_name匹配不上时,会走默认 server,建议把默认 server 直接指向仓库目录,避免客户端访问时被转发到其他站点。
5.3 离线源的定期更新机制
离线源不是搭完就完事,补丁还是要定期打。我总结了一套简单可行的更新流程,周期建议一个月一次。
下载机上,先把需要更新的包重新拉取:
repotrack -p /opt/localrepo/update nginx然后把新包合并进仓库目录:
cp /opt/localrepo/update/*.rpm /opt/localrepo/ rm -rf /opt/localrepo/update增量刷新元数据:
createrepo --update /opt/localrepo打包传输到仓库服务器,替换目录,最后在仓库服务器上重新执行一次 createrepo(如果打包传输的是整个仓库的话)。整个过程半小时内能完成,比每台机器手动拷包省太多时间。
5.4 同一套源同时支持不同版本系统
实际环境里,很可能同时有 CentOS 7 和 CentOS 8 的机器。这种情况下,我的建议是仓库服务器上按大版本分目录:
/opt/repo/7/x86_64/ /opt/repo/7/aarch64/ /opt/repo/8/x86_64/ /opt/repo/8/aarch64/每个目录独立执行 createrepo。客户端配置时,用$releasever和$basearch变量,就能一套配置兼容多个版本:
[localrepo] name=Local Repository baseurl=http://192.168.1.10/repo/$releasever/$basearch enabled=1 gpgcheck=0第一次配置时注意验证目标机器的$releasever实际值,CentOS 7 一般是7。调试时可以在命令行执行:
yum --showduplicates list 2>/dev/null | head如果 baseurl 路径不对,yum 会提示找不到 repodata,看yum repolist -v的Repo-baseurl字段能最快定位。
6. 常见问题与排查技巧实录
离线源搭建过程中,几乎所有问题都集中在元数据、路径和签名这三类。我把自己和一个同行朋友实际上踩过的坑整理成速查表,方便你遇到问题时对照排查。
6.1 报错速查表
| 报错/现象 | 直接原因 | 解决办法 |
|---|---|---|
| Could not resolve host | 系统尝试解析公网源域名 | 禁用公网 repo,只保留本地 repo |
| Cannot find a valid baseurl for repo | .repo中路径写错或目录不存在 | 检查baseurl路径与repodata是否存在 |
| repomd.xml does not match metadata | 元数据与仓库内容不一致 | 重新执行createrepo或createrepo --update |
| Public key for xxx is not installed | 缺少 GPG 公钥 | 导入公钥或关闭gpgcheck |
| Metadata file does not match checksum | 包文件损坏 | 重新校验并拷贝完整文件 |
| Nothing to do / No package available | 元数据中无该包 | 确认包是否下载成功、是否执行过 createrepo |
| delta rpm 相关报错 | 客户端尝试应用增量包失败 | 清理缓存后重试,必要时关闭 deltarpm |
6.2 一个典型的排查过程
举个真实例子。某次我在目标机器上执行yum install htop,报错内容是:
http://192.168.1.10/repo/7/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - "Failed to connect to 192.168.1.10 port 80: Connection refused"这个报错一眼看是连不上 HTTP 服务,但仓库服务器上 Nginx 明明在运行。后来排查发现是 firewalld 没放行 80 端口,curl 127.0.0.1通、curl 服务器IP不通,典型的本地服务正常但外部访问被防火墙拦截。执行firewall-cmd --add-service=http后解决。
另一个例子是客户端报repomd.xml does not match metadata。当时我反复执行 createrepo 都没用,最后发现是仓库服务器上有两个不同的元数据缓存,Nginx 开着 autoindex,但客户端访问时走了一个旧的软链路径。删除软链、用真实路径后解决。这类问题提醒我:元数据刷新以后,一定要检查客户端实际访问的 URL 路径是否指向了同一个目录。
6.3 我强烈建议养成的四个习惯
第一,所有.repo文件先备份再改动。改之前cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak,出问题能立刻回滚。
第二,仓库目录用只读方式挂载或设置权限。离线源内容原则上不应该被客户端改,仓库服务器上把这个目录设为chmod -R a-w或挂载为只读,能避免误操作污染仓库。
第三,每次增补包后必须重新 createrepo。哪怕只多了一个包,不刷新元数据这个包在客户端就是不可见的。这条我已经被坑过多次,现在写进了自己的操作清单。
第四,离线环境一定要记录仓库的构建时间戳和包清单。我习惯每次更新后在仓库根目录生成一个README或timestamp.txt,写上“本次更新日期、新增包、构建人”,后面排查问题时能少走很多弯路。
最后再分享两个小技巧
离线源搭完以后,我个人还有一个使用习惯:把仓库构建机上常用的 yum 操作提前在离线环境下验证一遍,比如安装gcc、python3、openssl-devel这类高依赖组件。这样真正业务部署时不会临时发现缺包,又得回头补拉依赖。
另一个技巧是,如果目标机器型号比较杂,建议在下载机上直接用yumdownloader --resolve多下载几个常用开发库的包,主动把“可能用到”的依赖提前囤进仓库。虽然会多占一些空间,但在现场缺包时的紧张感,远大于那点空间成本。
离线 yum 源这件事,工具链并不复杂,难点全在细节上。路径写错、公钥没导入、元数据忘了刷新,任何一个环节出错都够你排查一阵子。但只要把这套流程跑顺,后续所有内网机器的软件安装都会变得像有网一样自然。