简介:在内网隔离环境中,Linux服务器无法直接访问互联网,软件安装与更新往往依赖人工上传RPM包,效率低且难以保证版本一致。这份基于Rocky Linux 9.2的实操文档面向运维工程师与系统管理员,完整讲述了借助HTTP服务搭建局域网YUM源的具体过程。部署场景采用YUM服务器(192.168.15.100)与客户机(192.168.15.101)的双节点结构,详细覆盖ISO镜像挂载至/var/www/html/opt、创建local.repo仓库文件、安装并启动httpd服务、关闭防火墙及SELinux,以及客户机修改baseurl并执行yum clean all、yum makecache和yum list验证等环节,每一步都配有明确命令,便于直接落地。资源包共1个文件,为120KB的Word文档,结构清晰,按服务器端和客户端分步组织,适合内网运维团队参考复用。目前已有1692人学习使用,该方案不止适用于Rocky Linux,稍加调整也能迁移至其他RPM系发行版,有效解决内网大批量软件部署与版本一致性问题。
1. 为什么在 Rocky 9.2 上还要自建局域网 yum 源:一个 http 目录就能解决的事
一台离线机器手动 rpm 装 nginx,装到怀疑人生。几十台 Rocky 9.2 组成的实验室内网,外网访问不通,每台机器都要装同样的软件栈,靠 U 盘拷包一个个解决依赖,效率低到不想说话。Rocky 9.2 基于 http 方式搭建局域网 yum 源,本质就是把一台有外网的机器变成内网仓库服务器,其他机器用 dnf 直接从内网安装和更新软件,全程不碰外网。核心其实很轻:一个 http 目录加上正确的 repodata 元数据,客户端就能把它当官方源用。区别在于 Rocky 9 的仓库规范比 CentOS 7 时代复杂,AppStream 模块、元数据机制都有新坑。这篇文章按原理、最小方案、完整镜像、排错到验证的顺序写,适合机房管理员、离线交付项目的人,看完能直接照着搭一套能长期维护的源。
2. 先搞清 Rocky 9 的仓库结构和 repodata:为什么不能照搬 CentOS 7 的教程
2.1 Rocky 9 的四个仓库:BaseOS、AppStream、extras、CRB 各装什么
Rocky 9 的包不再像 CentOS 7 那样堆在一个 os/Packages 目录里让 dnf 自己翻。官方仓库按内容拆成几个逻辑仓库,每个仓库都有独立的 repodata 目录。BaseOS 装的是内核、固件、网络工具、shell 这类基础组件,系统能跑起来靠它。AppStream 装的是各种应用软件和运行时,nginx、python、redis、mysql 都在这里,这是和 CentOS 7 最大的区别。extras 是官方不维护但随发行版发布的额外包,数量不多。CRB 是 RHEL 9 时代由 PowerTools 改名的仓库,主要放构建依赖和开发库,一般客户端不用开启,但做镜像时建议同步下来留作后手。
这对搭建 yum 源的影响非常直接。如果内网只同步了 BaseOS,客户端执行 dnf install nginx 大概率报“没有匹配的包”——不是包不存在,而是 nginx 的 rpm 在 AppStream 仓库里,客户端根本没启用这个仓库,dnf 在解析包时只看已启用仓库的元数据。搭建前先明确需求:只是给基础系统打补丁,BaseOS 一个就够;凡是涉及应用软件,必须把 BaseOS 和 AppStream 一起做进去。实际部署中我见过不少把 CentOS 7 老教程直接搬过来的人,只同步一个 Packages 目录,结果客户端装什么都缺包,这就是对新仓库结构不熟悉导致的翻车。
2.2 为什么用 http 发布而不是 nfs、ftp 或 file
file:// 方式适合单机自用,把 repo 文件里的 baseurl 指向本地目录就行,但它解决不了局域网内其他机器访问的问题。有人会把目录 NFS 共享出去再写 file:// 路径,这在跨网段、跨防火墙环境里容易遇到端口被拦、服务挂掉后 dnf 长时间卡死,不推荐作为主方案。FTP 在 dnf 下也能工作,但被动模式碰上内网防火墙时经常出现连接建立失败,排错成本比 http 高一个量级。
http 是最省心的选择:nginx 监听一个 80 端口就能服务整个网段,dnf 对 http 仓库的元数据缓存行为经过大量生产环境验证,客户端用 curl 就能直接调试 URL,404 还是 403 一眼看清。还有 http 连接复用这个点容易被忽略:局域网里几十台机器同时执行 dnf makecache 时,nginx 的 keepalive 可以让客户端复用已有的 TCP 连接,而不是每请求一次就重新握手一次。机器少于 20 台时感知不明显,超过 50 台后,仓库服务器的 CPU 占用和连接数差异会很直观。
2.3 repodata 是什么:createrepo_c 生成的文件与 dnf 的消费方式
一个合格的 yum 源目录里,除了 rpm 文件本身,还必须有一个 repodata 子目录。repodata 里最关键的是 repomd.xml,它是整个仓库的索引签名文件;primary.xml.gz 记录每个 rpm 的包名、版本、架构和依赖关系;filelists.xml.gz 记录每个包的文件列表,供 dnf 按文件路径反查归属;other.xml.gz 是 changelog 等杂项。dnf 客户端首次访问仓库时,先下载 repomd.xml,用其中的校验和去校验后续元数据文件,再按需拉取 primary 和 filelists。
所以“把 rpm 拷到目录里不生成 repodata”等于没做源,客户端只会看到仓库目录存在但无法解析任何包。RHEL 9 和 Rocky 9 上生成元数据的工具是 createrepo_c,它是 C 语言实现,处理几千个 rpm 时比老的 Python 版 createrepo 快得多,命令名也变成了 createrepo_c,老教程里直接敲 createrepo 会提示找不到命令,这个细节坑了一大批从 CentOS 7 迁移过来的人。createrepo_c 生成的元数据默认带 sha256 校验和,被 dnf 直接识别,不需要额外配置。理解到这一层就够用了:客户端要的是包名、版本、依赖、校验和四类信息,createrepo_c 负责把这四类信息从 rpm 头部抽出来建立索引。
3. 最小可用方案:dnf download + createrepo_c + nginx 三步把源跑起来
3.1 目录规划与基础工具安装
最小方案面向的场景是:内网机器数量不多,需要安装的软件可以提前列出来,不需要完整镜像官方仓库。在一台有外网的机器(后面叫源机)上规划一个目录存放 rpm,比如 /data/repo。我习惯把目录按用途分开,基础包和业务包各放一个子目录,后续生成元数据时互不干扰。先安装工具:
dnf install -y nginx createrepo_c dnf-plugins-corenginx 用来发布 http 服务,createrepo_c 用来生成仓库元数据,dnf-plugins-core 提供 dnf download 子命令。这三个包在 Rocky 9 的默认仓库里都有,源机能连外网时装起来不会缺依赖。如果源机本身完全离线,先用另一台有外网的机器下载这三个 rpm 打成压缩包带进来,后续流程不变,只是更新 rpm 时多一步人工拷贝。
提示:Rocky 9 最小安装默认很可能没有 nginx。如果 dnf 提示找不到 nginx,先确认是不是把 AppStream 仓库禁用掉了。
3.2 用 dnf download 把包和依赖一起拉下来
源机要发布哪些软件,就提前把它们下载到仓库目录里。dnf download 和 dnf install 的区别是只下载不安装,专门用来攒离线包:
dnf download --destdir=/data/repo/base --resolve --alldeps nginx net-tools bash-completion--destdir 指定 rpm 保存目录;--resolve 让 dnf 把目标包的所有依赖一起下载;--alldeps 在 --resolve 基础上更激进,即使某个依赖已经在源机上安装了也会一并下载。这样做的原因是源机和客户端的环境不一定相同,源机装过的包客户端可能没有,加上 --alldeps 生成的源更完整,代价是 rpm 数量多一些,但局域网场景完全能接受。
如果想做一个通用的基础源,把源机系统里所有已安装的包导出,可以这样:
dnf download $(rpm -qa) --destdir=/data/repo/base --resolve --alldepsrpm -qa 列出当前系统所有包名,dnf download 批量解析下载,相当于把一台机器的运行环境打包成仓库。注意这里的前提是仓库里有与当前系统完全一致的 name-version-release,如果源机做过升级,用 rpm -qa --qf '%{NAME}\n' 只取包名再下载,会拿到仓库里的最新版。
3.3 用 createrepo_c 生成元数据
rpm 文件放进目录后,下一步生成 repodata:
createrepo_c --workers 4 --checksum sha256 /data/repo/base--workers 指定并行线程数,源机四核以上就填 4,核心少就填 2,这个参数直接影响生成速度。--checksum 显式指定元数据校验算法,sha256 是 Rocky 9 的默认值,写出来是表明意图,不写也能正常工作。命令执行完成后,目录下出现 repodata 子目录,里面有 repomd.xml、primary.xml.gz 等文件。如果按业务分了多个子目录,比如 /data/repo/base 和 /data/repo/app,要分别对每个目录执行一次 createrepo_c,不能把两个目录的 rpm 混在一起生成一份元数据,否则出现同名不同版本时 dnf 会把包混为一谈,安装时可能拉到错误版本。
后续往仓库里追加新 rpm,不需要重新跑全量生成,用增量模式:
createrepo_c --update --workers 4 /data/repo/base--update 会对比目录里已有的 repomd.xml,只重新解析新增和变更的 rpm。几百个包里加一个包时,时间从几十秒缩短到几秒,这是最小方案长期维护的关键命令。
3.4 nginx 发布目录与 autoindex
目录建好了,用 nginx 把它变成 http 可访问的资源。在 /etc/nginx/conf.d/repo.conf 里写入:
server { listen 80; server_name _; root /data/repo; autoindex on; autoindex_exact_size off; autoindex_localtime on; keepalive_timeout 10; }root 指向仓库根目录,客户端访问 http://源机IP/base/ 时对应 /data/repo/base/。autoindex 必须打开,否则 nginx 找不到 index 文件时直接返回 403,而 yum 源目录恰好没有 index.html,这是新手最常踩的坑。autoindex_exact_size 和 autoindex_localtime 只是让目录列表显示更友好,不设也能用。keepalive_timeout 对应前面说的 http 连接复用,调成 10 秒可以让空闲连接更快释放,避免大并发时连接数堆积。
写好配置执行 nginx -t 检查语法,然后:
systemctl enable --now nginx firewall-cmd --permanent --add-service=http firewall-cmd --reloadfirewalld 这步漏掉,其他机器访问 80 端口会连接超时,而源机自己 curl 却是通的,容易让人误以为 nginx 配置有问题。
3.5 客户端 repo 文件与冒烟测试
在任意一台内网机器上创建 repo 文件指向源机:
cat > /etc/yum.repos.d/local.repo <<'EOF' [local-base] name=Local Rocky Base baseurl=http://192.168.1.10/base/ enabled=1 gpgcheck=0 [local-app] name=Local Rocky App baseurl=http://192.168.1.10/app/ enabled=1 gpgcheck=0 EOF这里把 gpgcheck 临时设为 0,是因为最小方案里这些 rpm 是 dnf download 出来的,没有官方签名链验证,强行开启会报签名错误。客户端执行 dnf clean all 和 dnf repolist,能看到 local-base 和 local-app 两个仓库就说明源已经通了。验证安装用 dnf install nginx --downloadonly,这条命令只解析依赖并下载不实际安装,不会把测试机器搞乱。
注意:gpgcheck=0 适合内部临时源。生产环境建议把 gpgkey 配回去,客户端系统自带的 /etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 文件可以直接复用,或者直接用下一章的官方仓库全量镜像方案。
4. 完整方案:rsync 同步官方仓库,做成自动更新的局域网镜像
4.1 最小方案的维护瓶颈在哪里
最小方案在包数量变多之后,维护成本明显上升。每引入一个新软件包,都要在源机上手动 dnf download,再跑一次 createrepo_c --update。管理员记不住哪些包下载过、哪些没下载,仓库会慢慢出现漏包,客户端装到一半报找不到依赖。完整方案是一劳永逸的做法:用 rsync 把 Rocky 官方仓库或国内镜像站仓库同步到本地,客户端直接按官方仓库路径使用,不需要自己维护 rpm 集合。
4.2 选镜像站与仓库目录结构
同步源建议选网络延迟低的国内镜像站,比如中科大镜像的 rocky 目录。保持镜像站原有的目录结构,不要自己重新整理:
/data/rocky/9/BaseOS/x86_64/os/ /data/rocky/9/AppStream/x86_64/os/ /data/rocky/9/extras/x86_64/os/ /data/rocky/9/CRB/x86_64/os/保持结构和镜像站一致有两个好处:一是 repodata 不用重新生成,同步过来的元数据直接可用;二是客户端 repo 文件可以照搬官方文件的写法,只把 baseurl 里的主机名换成内网 IP,不需要额外适配。如果内网同时存在 x86_64 和 aarch64 机器,把路径中的 x86_64 替换成 aarch64 再同步一份即可。
4.3 同步命令与参数说明
同步单个仓库的命令:
rsync -avrt --delete \ --exclude='debug/' \ --exclude='source/' \ --timeout=120 \ rsync://rsync.mirrors.ustc.edu.cn/rocky/9/BaseOS/x86_64/os/ \ /data/rocky/9/BaseOS/x86_64/os/-a 保留文件属性并递归;-v 输出同步过程,首次同步方便观察进度;-t 保留时间戳,rsync 靠文件大小和 mtime 判断变更,时间戳必须保留,否则每次都会全量重传。--exclude 排除 debug 和 source 子目录,这两类占体积大且内网几乎用不到,排除能省 30% 左右同步流量。--timeout 120 防止网络抖动时 rsync 卡死。--delete 让本地删除镜像站已经下线的文件,避免陈旧 rpm 残留,但这个参数要慎用,细节在避坑章节展开。
仓库数量多时写一个脚本循环同步,顺便记录日志:
#!/bin/bash REPO_BASE="/data/rocky/9" MIRROR="rsync://rsync.mirrors.ustc.edu.cn/rocky/9" LOG="/var/log/rocky-sync.log" for repo in BaseOS AppStream extras CRB; do echo "[$(date '+%F %T')] start sync $repo" >> "$LOG" rsync -avrt --delete \ --exclude='debug/' --exclude='source/' \ --timeout=120 \ "$MIRROR/$repo/x86_64/os/" \ "$REPO_BASE/$repo/x86_64/os/" >> "$LOG" 2>&1 echo "[$(date '+%F %T')] end sync $repo" >> "$LOG" done日志里记录时间戳,是为了后面排查“客户端报陈旧元数据”时能确认同步到底有没有跑成功。首次同步建议在带宽空闲时段执行,四个仓库初始流量约 15 到 20GB,具体看镜像站速度和网络条件,可能几十分钟到几小时。
4.4 nginx 发布与客户端官方风格 repo 文件
发布配置和最小方案类似,root 指向 /data/rocky 即可。客户端 repo 文件写成官方风格:
cat > /etc/yum.repos.d/lan.repo <<'EOF' [lan-BaseOS] name=LAN Rocky BaseOS baseurl=http://192.168.1.10/rocky/9/BaseOS/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 [lan-AppStream] name=LAN Rocky AppStream baseurl=http://192.168.1.10/rocky/9/AppStream/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 EOF$basearch 由 dnf 自动替换成 x86_64 或 aarch64。gpgcheck=1 可以放心开,因为同步过来的是官方仓库原始 rpm 和官方 repodata,签名链完整。客户端系统在安装 rocky-repos 包时已经把官方 GPG key 放到了 /etc/pki/rpm-gpg/ 下,所以 gpgkey 用 file:// 路径即可,不需要从源机额外下载验证文件。
4.5 定时同步与验证
用 cron 做每日同步,放在凌晨流量低峰:
chmod +x /opt/sync-rocky.sh crontab -e在 crontab 里加一行:
0 3 * * * /opt/sync-rocky.sh > /dev/null 2>&1验证同步是否成功,关键是看 repomd.xml 能否从客户端访问:
curl -I http://192.168.1.10/rocky/9/BaseOS/x86_64/os/repodata/repomd.xml返回 200 且 Content-Length 正常即可。不建议用 dnf makecache 替代 curl 验证,因为 dnf 有缓存,无法区分“同步正常”和“dnf 还在用旧缓存”这两种状态。
5. 局域网 yum 源避坑:从 403 到 repodata 损坏的五条记录
5.1 客户端访问仓库目录直接 403 Forbidden
现象:curl 访问 http://源机IP/base/ 返回 403,但访问具体 rpm 文件却能下载;浏览器打开源目录也显示 Forbidden。
原因:nginx 对没有 index 文件的目录默认拒绝列出,而 yum 源目录恰好没有 index.html。另一个常见原因是 SELinux 没放行自定义目录的访问。
解决:在 nginx 配置里加 autoindex on 并重启;如果还 403,检查 SELinux。SELinux 开启状态下,nginx 只能读取标记为 httpd_sys_content_t 的目录,放 /usr/share/nginx/html 下的目录默认没问题,自定义的 /data/repo 需要手动标记:
semanage fcontext -a -t httpd_sys_content_t "/data/repo(/.*)?" restorecon -RF /data/reposemanage 在 policycoreutils-python-utils 包里,没有就先安装。临时验证可以用 chcon -R -t httpd_sys_content_t /data/repo,但 chcon 重启后可能丢失,semanage 才是持久方案。
5.2 dnf repolist 能看到仓库,install 却提示找不到包
现象:客户端 dnf repolist 正常列出仓库,但 dnf install nginx 报“没有匹配的包”,而源服务器上明明有 nginx 的 rpm。
原因:只有 BaseOS 仓库被同步和启用,AppStream 没同步或者 repo 文件里没写。nginx、python、redis 这类应用基本都在 AppStream,BaseOS 里只有系统组件。
解决:检查源机目录下有没有 AppStream 对应的 repodata,检查客户端 repo 文件里是否配置 AppStream 条目。这个坑的隐蔽之处在于 repolist 不报错,BaseOS 仓库本身是正常的,dnf 只是按已启用仓库列表搜索不到目标包而已。
5.3 rsync --delete 把仓库同步成了半截毁状态
现象:同步过程中网络中断,客户端 dnf 报“repodata 与磁盘上的不一致”,或者 repomd.xml 解析失败。
原因:rsync 默认逐文件覆盖,--delete 删除过期文件时如果中断,本地会残留旧 rpm 和新 repodata 混搭的状态,repomd.xml 里记录的校验和与实际文件对不上。
解决:同步到临时目录再切换,这是最稳妥的姿势:
rsync -a --delete rsync://.../BaseOS/x86_64/os/ /data/rocky-tmp/BaseOS/x86_64/os/ mv /data/rocky-tmp /data/rocky先同步到 /data/rocky-tmp,完成后用 mv 原子替换,客户端在任何时刻看到的都是完整目录。牺牲一点磁盘空间,换来的是源永远不出现半截状态。
5.4 装 AppStream 里的包报模块定义错误
现象:AppStream 同步完成,dnf install nginx 时报“无法检测 nginx 的模块定义”,即使包就在仓库里。
原因:AppStream 仓库带了模块化流的元数据 modules.yaml.gz,如果同步时误排除了模块文件,或者用 wget 从 AppStream 手动拉了几个 rpm 自己 createrepo_c,模块元数据就丢了。
解决:AppStream 必须整体同步,不要自定义裁剪。如果确认同步完整仍然报错,在客户端执行 dnf clean all && dnf makecache 清掉旧缓存再试。
5.5 客户端 dnf 操作卡顿迟缓
现象:客户端执行 dnf repolist 或 makecache 要等几十秒甚至更久,源服务器负载并不高。
原因:repo 文件里残留了官方 mirrorlist 路径,dnf 会先请求 mirrorlist.rockylinux.org 拿镜像列表再访问 baseurl,内网访问外网地址超时后才回退;另外系统里如果启用了 fastestmirror 插件,局域网下它会反复尝试探测各镜像,白白浪费时间。
解决:repo 文件里只留 baseurl,注释或删掉 mirrorlist;把 /etc/yum.repos.d/ 下原来的 rocky-*.repo 全部移到备份目录,只留内网 repo 文件。然后在 /etc/dnf/dnf.conf 的 [main] 段追加:
echo "fastestmirror=0" >> /etc/dnf/dnf.conf禁用 fastestmirror 后,配合 nginx 的 keepalive_timeout 参数,客户端并发执行 makecache 时的整体延迟会明显下降。
6. 客户端 repo 配置、验证命令与增量更新小技巧
完整方案里的客户端 repo 文件还有一种更健壮的写法,把四个官方仓库全部定义进去,避免后续临时用 CRB 时再去改文件:
cat > /etc/yum.repos.d/lan.repo <<'EOF' [lan-BaseOS] name=LAN Rocky BaseOS baseurl=http://192.168.1.10/rocky/9/BaseOS/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 [lan-AppStream] name=LAN Rocky AppStream baseurl=http://192.168.1.10/rocky/9/AppStream/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 [lan-extras] name=LAN Rocky extras baseurl=http://192.168.1.10/rocky/9/extras/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 [lan-CRB] name=LAN Rocky CRB baseurl=http://192.168.1.10/rocky/9/CRB/$basearch/os/ enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 EOF写好后按顺序执行三条验证命令:
dnf clean all dnf repolist dnf repoquery --whatprovides 'nginx'dnf repoquery --whatprovides 比 dnf install 试装更轻量,能直接确认目标包在源里是否可解析,还能看到模块化版本的候选。想进一步确认客户端和源服务器元数据一致,用 dnf repolist -v 查看仓库元数据时间戳,如果和源服务器 repomd.xml 里的时间一致,说明客户端读到的就是最新状态。
最小方案的增量更新,核心是前面提过的 createrepo_c --update。放进 cron 时注意,它只能解决本地目录新增 rpm 后的元数据同步,不能替代 rsync 全量同步。两条路线的边界很清晰:包集合可控、机器数少用最小方案;机器多或包范围不固定,直接上 rsync 脚本。我维护的几套内网环境里,最小方案翻车一般不是命令写错,而是懒——新包没往目录里放,客户端报缺依赖时才想起补。后来我给自己定了个习惯:拿到新需求先判断是一次性交付还是长期滚动,前者用最小方案,后者直接写 rsync 脚本,别让仓库变成只有自己能看懂的半成品。希望帮到你。
本文还有配套的精品资源,点击获取