简介:在内网环境中,服务器无法访问互联网时,如何高效安装和更新软件是一大难题。这份基于Rocky Linux 9.2的实战文档,专门面向Linux运维工程师及内网服务器管理人员,演示了通过HTTP服务构建局域网YUM源的完整流程。资源为单个docx文档,大小仅120KB,内容涵盖服务器端ISO镜像挂载、本地YUM源配置、httpd服务安装与启动,以及客户端baseurl修改、yum clean all和makecache缓存更新等核心操作,并附有BaseOS与AppStream仓库的配置示例。已有1691人学习下载。按照文档逐步操作,即可为多台同版本Linux服务器快速搭建统一的软件源,实现软件包集中管理和版本一致性,大幅提升内网运维效率;文档还特别说明了关闭firewalld、setenforce 0等安全策略调整方法,可帮助读者顺利部署。该方法亦可迁移至其他基于RPM的Linux发行版,对DevOps、系统管理员及运维新手均具有较高的实用价值。
1. 为什么三十台 Rocky 9.2 一起 dnf install 时,最先崩的是公网源
三十台 Rocky 9.2 同时 dnf install 时公网 yum 源开始报错,全组一起抓瞎——这就是我决定在局域网里用 http 方式搭一套自有 yum 源的那个下午。做法是:用一台能上外网的 Rocky 9.2 做同步机,reposync 把官方仓库拉到本地,经 httpd 用 http 协议发布到内网;其余机器把 yum 源 baseurl 指向它,装包速度直接变成局域网水平。它适合离线机房、内网测试环境和培训教室;新手照着做一天能跑通,熟手能拿走增量更新与排错的关键参数。
2. 先搞懂 dnf 在要什么:repomd.xml、http 协议与连接复用的关系
2.1 dnf 的读取链:从 repomd.xml 到 primary.xml.gz,再到具体 rpm
dnf 的仓库机制和“把目录里的 rpm 列出来”完全不是一回事。客户端执行 dnf makecache 或 dnf install 时,第一件事是请求仓库根目录下 repodata/repomd.xml 这个清单文件。repomd.xml 本身只有几十 KB,里面记录着 primary.xml.gz、filelists.xml.gz、modules.yaml 等元数据文件的路径和 SHA-256 校验值;dnf 把这些元数据下载下来并逐个校验,全部通过后才会在本地建立包索引。真正装包的时候,它再按 primary 里记录的相对路径去拉取对应的 rpm 包。
# 模拟 dnf 的第一跳:直接看 repomd.xml 内容 curl -s http://192.168.10.20/rocky9/baseos/repodata/repomd.xml | head -n 20这条 curl 把 dnf 的黑匣子打开了一层。如果看到的是 XML 里的 type 和 location 标签,说明元数据链路是通的;如果返回 403 或者 HTML 错误页,那客户端改再多配置也白搭。另外要注意 repodata 里几类文件的职责不同:primary 管包名和依赖,filelists 管文件级 provides,modules.yaml 管 AppStream 模块流,comps 管分组。局域网源只要漏了其中一类,就会有“某些依赖永远解析不出来”这类诡异问题,所以同步时必须把整份 repodata 完整带下来,不能只挑 rpm。
2.2 为什么选 http:和 file://、https 对比之后的选择
很多实验教材讲 linux 配置本地 yum 源,都是挂载 ISO 后 baseurl 写 file:///mnt/iso,那是单机教学场景;局域网源要服务几十台机器,这套就不成立了。我实际对比过三种方案的差别:
| 方案 | 客户端 baseurl 写法 | 跨机器共享 | 并发与续传 | 额外成本 |
|---|---|---|---|---|
| file:// | file:///mnt/iso | 要配 NFS,挂载一断全部报错 | 无 | NFS 服务端 |
| http | http://ip/rocky9/baseos | 天然支持 | dnf 下载支持断点续传 | 一个 httpd |
| https | https://ip/rocky9/baseos | 天然支持 | 同上 | 证书签发与 ca 信任维护 |
内网我直接选 http,三个理由。第一,防火墙成本最低,firewalld 里一条 add-service=http 就完事,https 则要额外放行 443 和维护证书信任;第二,http 连接复用是实打实的收益,dnf 在同步元数据阶段要连续请求几十个小文件,httpd 默认开 Keep-Alive,同一个 TCP 连接反复使用,省掉了大量的握手开销,局域网里这点差别在几十台机器同时 makecache 时会被明显放大;第三,包的安全已经有 gpgcheck 的 GPG 签名兜底,完整性和来源都有保障,内网再套 https 属于重复投入。http 和 https 的区别在公网场景才是刚需,局域网里 http 就是性价比最高的默认项。
2.3 单机同步还是双机分离:局域网搭建源的两种拓扑
仓库从哪里拉、由谁发布,决定了整套东西的可靠边界。常见做法是两种拓扑。
第一种是单机:同一台机器既当同步机又当发布机。它能上外网,配好官方源后 reposync 把仓库拉到本地磁盘,再由本机 httpd 发布出去。结构最简单,故障点最少,一台机器扛几十个客户端毫无压力,我的环境一直这么跑。要注意的是这台机器的流量:第一次全量同步会吃满不少带宽,尽量安排在业务低峰期。
第二种是双机:同步机和发布机物理分离。同步机拉完仓库后,用 rsync 推到完全离线的发布机 /var/www/html 下,发布机不配外网也不启用官方源。好处是发布机暴露面小,同步过程不抢占发布机的磁盘 IO,客户端上百台时更稳;代价是要维护 rsync 的密钥和推送策略,多一个环节就多一类故障。我的建议是客户端规模在百台以内都别上双机,先把磁盘预算给足,这比架构折腾更管用,运维成本低一个量级。
3. 服务端搭建:reposync 拉全量仓库、createrepo 重建元数据、httpd 发布
3.1 装机准备:dnf-utils 与 createrepo_c 缺一不可
服务端要做三件事:拉仓库、生成元数据、发布,对应三个包。
# Rocky 9.2 服务端一次性安装 dnf install -y dnf-utils createrepo_c httpd systemctl enable --now httpd hostnamectl set-hostname repo.landnf-utils 是 Rocky 9 上提供 reposync 命令的包,一些老教程让你装 yum-utils,在 Rocky 9 里它只是个指向 dnf-utils 的兼容包,直接装 dnf-utils 即可。createrepo_c 是 C 语言实现的元数据生成器,比老一代 Python 版的 createrepo 快一个量级,处理 AppStream 的模块元数据也更可靠;安装后系统会同时提供 createrepo_c 和 createrepo 两个命令,前者是本体,后者是兼容软链,后面脚本我统一用 createrepo_c,避免歧义。httpd 就是 Apache,默认监听 80,文档根目录 /var/www/html,发布静态仓库足够。
装完顺手看一眼磁盘:df -h。仓库我放在 /var/www/html/rocky9 下,这样 SELinux 的文件上下文直接就是 httpd_sys_content_t,后面不会撞上 403。容量按 BaseOS 十五到二十 GB、AppStream 四五十 GB 起步预算,再加三成冗余,独立分区给到 100GB 左右比较从容。磁盘写满是搭源最容易翻的车,预算给足后面少很多事。
3.2 禁用官方源后 reposync 同步 BaseOS 和 AppStream
同步之前,先做一个看起来有点反直觉的操作:把服务端自带的官方源临时关掉。不是因为它没用,而是防止 reposync 读到混乱的源配置,也为了让这台机器后续统一由自建源管理。
# 备份官方源配置,之后可随时恢复 cp -a /etc/yum.repos.d/rocky.repo /etc/yum.repos.d/rocky.repo.bak sed -i 's/enabled=1/enabled=0/' /etc/yum.repos.d/rocky.repo # 创建仓库根目录并同步 mkdir -p /var/www/html/rocky9 reposync --repoid=baseos -p /var/www/html/rocky9 --download-metadata -a x86_64 reposync --repoid=appstream -p /var/www/html/rocky9 --download-metadata -a x86_64reposync 的参数逐个说清楚。--repoid 后面跟仓库 id,必须和 rocky.repo 里的 [baseos]、[appstream] 段名严格一致,区分大小写,写错会直接报 “no repository to sync”。-p 指定仓库根目录,同步出来的结构是 /var/www/html/rocky9/baseos 这样的“根目录/仓库id”两层。--download-metadata 会把上游 repodata 完整拉下来,其中包括 AppStream 的 modules.yaml,这个参数不能省,第 5 章讲模块包会再提。-a x86_64 只同步当前架构的包,不写的话会把 aarch64、s390x 的包也拖回来,磁盘占用直接翻倍。
如果团队还需要编译类工具,可以把 crb(CodeReady Builder)也同步一份,命令完全一样。同步耗时看带宽,BaseOS 加 AppStream 全量跑一两个小时是常态;中途断了就重新跑同一条命令,reposync 会跳过已下载完成的文件直接续上,不会有副作用。
3.3 createrepo_c 重建元数据:为什么不能只信上游的 repomd.xml
同步完成后,目录里其实已经带了一份从上游抄来的 repodata,为什么我坚持再生成一遍?这是不少教程没讲透的地方。
# 首次全量生成元数据 createrepo_c /var/www/html/rocky9/baseos createrepo_c /var/www/html/rocky9/appstream原因有两条。第一,上游元数据里的路径记录是基于它自己的仓库布局计算的,本地目录必须和它完全相同才能命中;自己生成一遍,等于给“这个目录里实际有什么文件”拍了一张快照,彻底消除这种隐式依赖。第二,也是最关键的:这套源后面一定会被增量更新、被塞进本地自研 rpm,每一次操作都在改变目录内容,上游抄来的 repomd.xml 会越跑越失真;而 createrepo_c 的 --update 参数只重扫有变化的文件,几分钟的增量几秒就能刷完,这是第 6 章自动化脚本能跑起来的性能基础。
顺带一提,首次全量生成元数据可能要等几分钟,屏幕上滚动的 worker 进度条是正常的,不是卡死。生成的 repodata 目录里会看到 repomd.xml 和一堆 .xml.gz 文件,这才是一份自洽的元数据。生成完可以顺手统计一下包数量:find /var/www/html/rocky9/baseos -name "*.rpm" | wc -l,这个数字后面用来和客户端 repolist 对账。
3.4 发布与自检:启动 httpd 后先 curl 一通 repomd.xml
httpd 装完就监听 80,仓库又放在文档根目录里,理论上已经可以访问了。但发布动作必须配一次验证,否则客户端那边排错成本更高。
firewall-cmd --add-service=http --permanent firewall-cmd --reload curl -I http://127.0.0.1/rocky9/baseos/repodata/repomd.xml curl -I http://<本机内网IP>/rocky9/baseos/repodata/repomd.xml第一条 curl 验证本机 httpd 进程和文件权限,第二条验证走内网 IP 能通,判断标准只有一个:返回 HTTP/1.1 200 OK。403 大概率是 SELinux 或目录权限,500 基本是 httpd 配置错误,这些留到第 5 章细说。如果希望浏览器里能直接浏览目录树方便人肉排查,可以在 /etc/httpd/conf.d/repo.conf 里针对仓库目录加一句 Options Indexes;不加也不影响 dnf,它对目录列表没有需求。
注意:本机 curl 通了不代表局域网里通,一定要用内网 IP 再测一次,这一条最容易在刚架好的时候被漏掉。
4. 客户端配置 yum 源:baseurl、gpgcheck、module_hotfixes 一次配对
4.1 先备份并禁用 rocky.repo,别让官方源和局域网源打架
客户端默认的 /etc/yum.repos.d/rocky.repo 指向公网 mirrorlist,如果不处理,dnf 会把官方源和局域网源同时启用。后果是元数据更新时间翻倍,某些包从公网拉到一半断掉,报错还在两个源之间互相甩锅。我的习惯是先整体离线官方源,再放新的仓库文件。
# 每台客户端执行 cp -a /etc/yum.repos.d/rocky.repo /etc/yum.repos.d/rocky.repo.bak sed -i 's/^enabled=1/enabled=0/' /etc/yum.repos.d/rocky.repo备份保留在原目录里,将来要切回官方源,恢复备份并把局域网源 enabled=0 就行。有个细节:sed 匹配的是行首的 enabled=1,防止误伤其他配置项。如果不做这一步,后面 dnf repolist 的输出会非常嘈杂,排错时你很难分清某个包到底命中哪个源。文件以 .bak 结尾也不会被 dnf 读取,放心放着。
4.2 手写 rocky-lan.repo:三段完整配置与参数解释
新仓库文件放在 /etc/yum.repos.d/rocky-lan.repo,BaseOS、AppStream、Extras 各一段,每段只有几个关键参数:
[baseos] name=Rocky9-LAN-BaseOS baseurl=http://192.168.10.20/rocky9/baseos enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 [appstream] name=Rocky9-LAN-AppStream baseurl=http://192.168.10.20/rocky9/appstream enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 module_hotfixes=1 [extras] name=Rocky9-LAN-Extras baseurl=http://192.168.10.20/rocky9/extras enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9参数逐个讲。baseurl 用 http 协议加服务器内网 IP,这里绝不能沿用官方源里的 metalink 字段,那是公网源用来做镜像调度的,在局域网里既慢又不可靠;路径要和服务端目录严格一致,后面目录规划一旦变化,这里要同步改。gpgcheck=1 保持默认开启,内网源也要验签名,这是包可信度的底线,不值得为了省事改成 0。gpgkey 指向 Rocky 9 官方公钥,路径固定是 /etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9,每台客户端这个文件系统自带。module_hotfixes=1 只写在 appstream 段,它告诉 dnf:本仓库的包允许覆盖模块流里同名同版本号的包,否则同步来的 AppStream 在装 php、nginx 这类模块包时会莫名失败,这是模块仓库特有的坑。
4.3 dnf repolist 与 makecache:装包之前必须做的两次验证
配置文件就位后,先验证再装包,顺序不能乱。
dnf clean all dnf makecache dnf repolist dnf install -y treednf clean all 清掉旧缓存,这是改动仓库配置后的固定动作,否则 dnf 可能继续用内存里的旧元数据。makecache 从局域网源拉元数据建索引,这一步如果报 Failed to download metadata,报错信息里会带具体 URL,拿那个 URL 去 curl 就能快速定位是路径还是网络问题。repolist 输出里三行 repo id 前面带星号表示启用,后面的包数量对应服务端 createrepo 的统计,数量对得上说明元数据解析成功。最后装个 tree 这种小包试水,确认依赖解析、下载、签名校验、安装全链路都通,再放量大装。这一步做完,客户端的“配置 yum 源”才算真正闭合。
4.4 批量下发:一个循环脚本把源推到三十台客户端
几十台机器一台台手敲不现实。我把 rocky-lan.repo 放在服务器上,用循环批量推,前提是运维已经给服务器配了免密 ssh。
SRV_IP=192.168.10.20 for host in 192.168.10.21 192.168.10.22 192.168.10.23; do scp rocky-lan.repo root@$host:/etc/yum.repos.d/ ssh root@$host "cp -a /etc/yum.repos.d/rocky.repo /etc/yum.repos.d/rocky.repo.bak && \ sed -i 's/^enabled=1/enabled=0/' /etc/yum.repos.d/rocky.repo && \ dnf clean all && dnf makecache" done脚本逻辑很简单:scp 推文件,ssh 过去做禁用官方源、清缓存、建缓存三连。第一次 makecache 是串行的,客户端越多越慢,三十台机器同一秒打过来 httpd 的瞬时压力不小;更稳的做法是先推文件和禁用配置,makecache 让各机器错峰跑,或者用 cron 把时间分散到五分钟窗口内。如果团队用 Ansible,整个循环对应 copy 加 command 两个模块,思路完全一样,无非是把循环交给它去并行。
5. 避坑与排查:搭 Rocky 9 局域网源最容易翻车的 5 个地方
5.1 客户端报 unexpected status 502 bad gateway:先查反代再查并发
现象:客户端 dnf makecache 或 dnf install 时偶发 “unexpected status 502 bad gateway: unknown error”,有时伴随连接被重置,重试几次又恢复正常。
原因:这个报错最常见的是仓库前面套了 nginx 反代——有人贪图统一入口,用 nginx 把公网源反代到内网,上游源一旦慢速响应或带宽打满,nginx 就直接吐 502。另一种情况是并发太高,几十台客户端同一秒触发 makecache,默认配置的 httpd worker 被打满后开始拒绝新连接。
解决:首选方案是不套反代,局域网源用 httpd 直接发布静态目录,少一层代理少一类故障。如果必须用 nginx,在 location 里加 proxy_read_timeout 300s 和 proxy_next_upstream error timeout,容忍上游慢速响应。httpd 侧想调 MaxRequestWorkers 之前,先去看 /var/log/httpd/error_log 里有没有对应警告,别凭感觉调参,玄学调参会把问题掩盖得更深。
5.2 同步到一半磁盘写满,repomd.xml 校验失败
现象:reposync 跑着跑着报 “no space left on device”;更隐蔽的是同步虽然结束,但客户端 dnf 报 “Failed to download metadata”,错误指向 repomd.xml 校验失败。
原因:磁盘写满导致同步中断,目录里留下半截文件,元数据不完整;reposync 重跑会跳过已存在文件,半截的损坏文件不会被自动清理,于是每次都在同一个地方翻车。
解决:同步前 df -h 确认磁盘余量至少是仓库体积的一倍;一旦报磁盘错误,把对应仓库目录整个删了重跑,别指望增量修复。想省磁盘,第一轮同步就加 -n(--newest-only)只保留每个包的最新版本,局域网源几乎不需要历史版本,这一条能把 AppStream 的体积砍掉一半左右。源端重新生成过元数据后,客户端一定要 dnf clean all,旧缓存里的校验和会让客户端来回报错。
5.3 装包时报 Public key 未安装:gpgkey 路径写错或密钥混乱
现象:dnf install 走到中途,提示 “Public key for xxx.rpm is not installed”,事务直接失败,下载好的包缓存起来不进入安装。
原因:配置文件 gpgcheck=1 但 gpgkey 行缺失或路径写错;另一类是这台机器之前导入过别的密钥,本地缓存里的密钥指纹和 Rocky 9 官方公钥对不上,dnf 拒绝信任新包。
解决:先确认 repo 文件每段都有 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9,且客户端这个文件真实存在。如果密钥确实混乱,先 rpm -qa gpg-pubkey 列出已导入的公钥,记下可疑的完整名字,用 rpm -e gpg-pubkey-<完整名字> 删掉,再 rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 重新导入,最后 dnf clean all 重试。就算在封闭内网,我也建议保持 gpgcheck=1,局域网源一旦被误放进来源不明的 rpm,这把签名钥匙是最后一道防线。
5.4 目录返回 403 Forbidden:SELinux 文件上下文没配
现象:服务端自己 curl http://127.0.0.1/rocky9/baseos/repodata/repomd.xml 返回 403,但 ls 看文件权限完全正常,属主也对。
原因:Rocky 9 默认 SELinux 强制模式,httpd 只能读 scontext 为 httpd_sys_content_t 的文件。仓库放在 /var/www/html 下时默认就是这种类型;一旦把仓库放到独立挂载点或自定义目录,新目录会被标记成 default_t,httpd 没权限读,就统一表现为 403。
解决:最省事是把仓库规划在 /var/www/html 下;目录已经定死的情况下,用 semanage 声明类型再 restorecon 刷新:
dnf install -y policycoreutils-python-utils semanage fcontext -a -t httpd_sys_content_t '/data/rocky9(/.*)?' restorecon -Rv /data/rocky9执行完再 curl 一次,返回 200 就说明类型生效了。这条坑我踩了两次才记住:排错顺序应该是先确认 SELinux,再怀疑权限,顺序反了会白折腾半天。
5.5 AppStream 模块包永远装不上:module_hotfixes=1 是后悔药
现象:客户端 dnf install php 时提示包不可用或版本冲突,但服务端目录里明明摆着 php 的 rpm 文件;手动指定 rpm 路径又能装上。
原因:AppStream 仓库带着模块元数据 modules.yaml,同步下来的包按模块流组织。客户端 repo 文件里没有 module_hotfixes=1 时,dnf 的模块解析器认为这些包和已有模块存在冲突,直接把仓库里的包忽略了,表现就是“目录里有文件但 dnf 说没有”。
解决:在 appstream 段补上 module_hotfixes=1,dnf clean all 后重试;同时确认服务端同步时带了 --download-metadata,否则本地根本没有 modules.yaml,这个参数也无力回天。这条是我回给别人最多的一条经验,写 repo 模板时直接默认带上,比踩了再查省时间。
6. 增量更新与自动化:把局域网源养成长期服务
6.1 cron 里跑增量同步:reposync -n 配合 createrepo_c --update
源架好只是开始,跟上上游版本节奏才是常态。我把同步写成脚本 /usr/local/bin/rocky-repo-sync.sh,执行 chmod +x 后放入 /etc/cron.d/rocky-repo 按周跑:
#!/bin/bash reposync --repoid=baseos -p /var/www/html/rocky9 --download-metadata -a x86_64 -n reposync --repoid=appstream -p /var/www/html/rocky9 --download-metadata -a x86_64 -n createrepo_c --update /var/www/html/rocky9/baseos createrepo_c --update /var/www/html/rocky9/appstream-n 只拉新增版本,--update 只重扫变化文件,整套增量跑下来通常几分钟。定时任务用 flock 锁防止手动触发和定时任务重合,日志重定向到固定文件,出问题时有据可查。
6.2 每月的体检三连
我每个月在源上做三件事:本机和客户端分别 curl -I repomd.xml 确认 200;df -h 看磁盘余量,AppStream 增长很快,余量低于 20% 就该清理或扩容;找台客户端 dnf repolist 对照包数量,数量没涨说明定时同步大概率挂了,去翻日志里 cron 的执行记录。
6.3 一个源服务多个发行版
同一台 httpd 可以按目录分发行版,/var/www/html/rocky9 放 Rocky,旁边再放 CentOS 7 的 BaseOS 和 Updates,各自客户端配各自的 baseurl 就行。CentOS 7 配置本地 yum 源的老经验在这里大半还能复用,只是命令体系还是 yum,reposync 用它对应的仓库 id 同步。
这套东西带给我最大的习惯是:源服务器上每次改动防火墙、SELinux 或目录结构,顺手留下一条 curl -I 的自检命令记录。后来公司在公网源 502 的那个晚上,全靠这条命令让全组十分钟内切到内网源恢复部署,那一刻觉得搭源花的时间都值了。希望帮到你。
本文还有配套的精品资源,点击获取