在 Rocky Linux、RHEL、AlmaLinux 等 RPM 系发行版中,大家习惯把内部软件源称为“YUM 私服”。不过从 Rocky Linux 10 开始,实际承担软件包解析、下载和依赖管理的是新一代 DNF/DNF5,YUM 更多是兼容性名称。因此,本文所说的 YUM 私服,本质上是一个符合 RPM-MD Repository 规范、可供 DNF 客户端访问的软件仓库服务。
企业内部搭建 YUM 私服,通常不是为了“再造一个下载站”,而是为了获得以下能力:
将 Rocky Linux BaseOS、AppStream、Extras、CRB 等仓库镜像到内网;
降低大量服务器重复下载 RPM 带来的公网流量和构建时间;
在公网异常、跨境网络抖动或隔离网络中维持系统安装和升级能力;
托管企业自己构建的 RPM;
固定可安装的软件包集合,降低上游更新导致的环境漂移;
对软件来源、签名、版本、发布和下线进行统一治理;
为 CI/CD、Kubernetes 节点和批量装机提供稳定的软件源。
本文基于 Rocky Linux 10,采用以下技术栈:
1 2 3 4 5 6 reposync / dnf5 reposync:同步远程仓库 createrepo_c:为企业自有 RPM 生成仓库元数据 Nginx:通过 HTTP/HTTPS 发布仓库 systemd Timer:定时执行增量同步 GPG / rpmsign:验证或签名 RPM Docker Compose:容器化运行 Nginx 和同步任务
一、YUM 私服的整体架构 建议将“官方镜像仓库”和“企业自有仓库”分开管理。
flowchart LR
Rocky[Rocky Linux 官方镜像] -->|reposync| Sync[同步工作区]
EPEL[EPEL 等可信第三方仓库] -->|reposync| Sync
Build[企业 RPM 构建流水线] -->|上传已签名 RPM| Custom[企业自有 RPM 仓库]
Custom -->|createrepo_c| Metadata[生成 RPM-MD 元数据]
Sync --> Publish[发布目录]
Metadata --> Publish
Publish --> Nginx[Nginx HTTPS]
Nginx --> Client1[Rocky Linux 10 服务器]
Nginx --> Client2[CI/CD 节点]
Nginx --> Client3[离线或内网服务器]
建议的目录结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /srv/yum-sync/ # reposync 工作目录,不直接对客户端暴露 └── rocky/ └── 10/ └── x86_64/ ├── baseos/ ├── appstream/ ├── extras/ └── crb/ /srv/yum-repo/ # Nginx 发布目录 ├── rocky/ │ └── 10/ │ └── x86_64/ │ ├── baseos/ │ ├── appstream/ │ ├── extras/ │ └── crb/ ├── custom/ │ └── el10/ │ └── x86_64/ │ ├── Packages/ │ └── repodata/ └── keys/ ├── RPM-GPG-KEY-Rocky-10 └── RPM-GPG-KEY-COMPANY
二、先理解三类仓库 2.1 官方镜像仓库 例如:
1 2 3 4 5 Rocky Linux 10 BaseOS Rocky Linux 10 AppStream Rocky Linux 10 Extras Rocky Linux 10 CRB EPEL 10
这类仓库已经由上游生成了完整元数据,可能包含:
primary:RPM 基础信息;
filelists:RPM 文件列表;
other:变更日志等信息;
comps:软件包组;
updateinfo:安全更新和公告信息;
模块或其他扩展元数据;
SQLite/Zchunk/Zstandard 等压缩数据。
因此镜像官方仓库时,推荐使用:
1 reposync --download-metadata
不要在同步完成后无脑执行 createrepo_c 覆盖官方元数据。
否则可能丢失软件包组、安全公告、模块信息或其他附加元数据。
2.2 企业自有 RPM 仓库 企业自己上传 RPM 时,目录中通常只有:
1 2 example-app-1.0.0-1.el10.x86_64.rpm example-agent-2.1.0-1.el10.noarch.rpm
这类目录没有 repodata,需要使用 createrepo_c 生成仓库元数据。
2.3 版本快照仓库 如果需要严格复现某个时间点的软件环境,仅同步“当前最新版”还不够。可以额外保留:
1 2 /srv/yum-repo/snapshots/2026-07-31/ /srv/yum-repo/snapshots/2026-08-31/
客户端固定到快照路径后,不会因为上游仓库更新而自动漂移。
这种模式适合:
生产环境变更窗口;
等保或审计环境;
大规模 Kubernetes 节点升级;
离线交付;
需要可重复构建的基础镜像。
三、环境规划 本文使用以下示例:
配置项
示例值
操作系统
Rocky Linux 10 x86_64
主机名
repo01
仓库域名
yum.example.com
同步工作目录
/srv/yum-sync
发布目录
/srv/yum-repo
Nginx 用户
nginx
HTTP 端口
80
HTTPS 端口
443
默认架构
x86_64、noarch
Rocky 主版本
10
检查系统:
1 2 3 4 cat /etc/rocky-releaseuname -runame -m hostnamectl
设置主机名和时区:
1 2 3 4 5 hostnamectl set-hostname repo01 timedatectl set-timezone Asia/Shanghai timedatectl chronyc tracking
Rocky Linux 10 的常用仓库 ID 包括:
1 2 3 4 baseos appstream extras crb
其中 BaseOS、AppStream 和 Extras 默认启用,CRB 通常默认关闭。实际环境应以以下命令为准:
1 2 3 4 5 dnf repolist --all dnf repoinfo baseos dnf repoinfo appstream dnf repoinfo extras dnf repoinfo crb
四、容量与资源评估 仓库服务器主要消耗:
1 2 3 4 5 磁盘容量 磁盘读写吞吐 出口带宽 文件系统 inode 少量 CPU 和内存
Nginx 发布静态 RPM 的 CPU 开销通常不高,真正的大头是磁盘与网络。
建议起点:
规模
CPU
内存
数据盘
网络
HomeLab / 测试
2 核
2 GB
100 GB SSD
1 Gbps
小型团队
4 核
4 GB
500 GB SSD
1 Gbps
中大型内网
8 核
8~16 GB
1 TB 以上 SSD/NVMe
10 Gbps 或按并发评估
由于同步工作区和正式发布区可能同时存在,初始规划建议至少预留:
1 2 预计仓库总容量 × 2 + 20%~30% 扩展空间
不要根据网上几年前的仓库容量直接下结论,应在实际同步后统计:
1 2 3 4 du -sh /srv/yum-sync/*du -sh /srv/yum-repo/*df -hT /srvdf -ih /srv
五、安装所需软件 更新系统:
1 2 3 dnf clean all dnf makecache dnf upgrade -y
安装基础工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 dnf install -y \ nginx \ createrepo_c \ rsync \ curl \ wget \ jq \ vim \ tar \ gzip \ zstd \ findutils \ util-linux \ policycoreutils-python-utils \ firewalld
Rocky Linux 10 使用 DNF5。不同最小化镜像或兼容包组合中,reposync 插件包名称可能不同,因此可以使用以下兼容安装方式:
1 2 dnf install -y dnf5-plugins || \ dnf install -y dnf-plugins-core
确认命令:
1 2 3 4 5 dnf --version dnf reposync --help createrepo_c --version nginx -v rsync --version
如果 dnf reposync 不可用,但系统中存在独立的 dnf5 命令:
后文脚本会自动判断应该使用 dnf reposync 还是 dnf5 reposync。
六、创建目录与服务账户 创建同步账户:
1 2 3 4 5 useradd \ --system \ --home-dir /srv/yum-sync \ --shell /usr/sbin/nologin \ reposync
创建目录:
1 2 3 4 mkdir -p /srv/yum-syncmkdir -p /srv/yum-repomkdir -p /srv/yum-repo/keysmkdir -p /var/log/yum-repo
设置权限:
1 2 3 4 5 6 chown -R reposync:reposync /srv/yum-syncchown -R reposync:reposync /srv/yum-repochown -R reposync:reposync /var/log/yum-repo find /srv/yum-repo -type d -exec chmod 0755 {} \; find /srv/yum-repo -type f -exec chmod 0644 {} \;
Nginx 只需要读取发布目录,不需要写权限。
验证:
1 2 namei -om /srv/yum-reposudo -u nginx test -r /srv/yum-repo
七、先手工同步 Rocky Linux 10 仓库 7.1 定义 reposync 兼容函数 创建临时 Shell 函数:
1 2 3 4 5 6 7 8 9 10 11 repo_sync () { if dnf reposync --help >/dev/null 2>&1; then dnf reposync "$@ " elif command -v dnf5 >/dev/null 2>&1 && \ dnf5 reposync --help >/dev/null 2>&1; then dnf5 reposync "$@ " else echo "未找到可用的 reposync 插件" >&2 return 1 fi }
7.2 同步 BaseOS 1 2 3 4 5 6 7 8 9 10 11 mkdir -p /srv/yum-sync/rocky/10/x86_64/baseos repo_sync \ --repo=baseos \ --download-path=/srv/yum-sync/rocky/10/x86_64/baseos \ --norepopath \ --download-metadata \ --delete \ --gpgcheck \ --arch =x86_64 \ --arch =noarch
7.3 同步 AppStream 1 2 3 4 5 6 7 8 9 10 11 mkdir -p /srv/yum-sync/rocky/10/x86_64/appstream repo_sync \ --repo=appstream \ --download-path=/srv/yum-sync/rocky/10/x86_64/appstream \ --norepopath \ --download-metadata \ --delete \ --gpgcheck \ --arch =x86_64 \ --arch =noarch
1 2 3 4 5 6 7 8 9 10 11 mkdir -p /srv/yum-sync/rocky/10/x86_64/extras repo_sync \ --repo=extras \ --download-path=/srv/yum-sync/rocky/10/x86_64/extras \ --norepopath \ --download-metadata \ --delete \ --gpgcheck \ --arch =x86_64 \ --arch =noarch
7.5 同步 CRB CRB 即使默认关闭,也可以通过显式指定仓库进行同步:
1 2 3 4 5 6 7 8 9 10 11 mkdir -p /srv/yum-sync/rocky/10/x86_64/crb repo_sync \ --repo=crb \ --download-path=/srv/yum-sync/rocky/10/x86_64/crb \ --norepopath \ --download-metadata \ --delete \ --gpgcheck \ --arch =x86_64 \ --arch =noarch
检查结果:
1 2 3 find /srv/yum-sync/rocky/10/x86_64 -maxdepth 2 -type d find /srv/yum-sync/rocky/10/x86_64/baseos/repodata -maxdepth 1 -type f | head find /srv/yum-sync/rocky/10/x86_64/baseos -name '*.rpm' | head
7.6 为什么同时同步 x86_64 和 noarch 只写:
会漏掉大量 noarch 包。
因此 x86_64 仓库通常至少要同步:
1 2 --arch =x86_64 --arch =noarch
否则客户端安装某些包时会出现依赖缺失。
7.7 不要随意使用 –newest-only 以下组合存在明显风险:
1 2 3 reposync \ --newest-only \ --download-metadata
原因是:
--newest-only 只下载最新 RPM;
上游原始元数据仍可能引用旧版本 RPM;
客户端解析元数据后请求旧包;
私服上没有对应文件;
最终出现下载失败。
如果确实只想保留最新 RPM,需要重新生成与本地文件集合一致的元数据:
1 createrepo_c --update 仓库目录
但这会丢失或改变上游附加元数据,因此不适合作为完整 Rocky 官方镜像的默认方案。
完整镜像应优先保留上游元数据,并避免使用 --newest-only。
八、自动同步与安全发布脚本 直接让 Nginx 读取 reposync 正在写入的目录存在两个问题:
客户端可能在元数据更新一半时访问;
RPM 和 repodata 可能短时间不一致。
因此建议:
1 2 3 4 远端仓库 -> 同步工作区 -> 发布目录 -> Nginx
8.1 创建同步脚本 创建 /usr/local/sbin/sync-rocky-repos.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 cat > /usr/local/sbin/sync-rocky-repos.sh <<'SCRIPT' set -Eeuo pipefailumask 022 ROCKY_VERSION="${ROCKY_VERSION:-10} " ARCH="${ARCH:-x86_64} " SYNC_ROOT="${SYNC_ROOT:-/srv/yum-sync} " PUBLISH_ROOT="${PUBLISH_ROOT:-/srv/yum-repo} " LOG_DIR="${LOG_DIR:-/var/log/yum-repo} " LOCK_FILE="${LOCK_FILE:-/run/lock/yum-repo-sync.lock} " REPOSITORIES=( baseos appstream extras crb )mkdir -p "${LOG_DIR} " mkdir -p "$(dirname "${LOCK_FILE} " ) " exec 9>"${LOCK_FILE} " if ! flock -n 9; then echo "已有同步任务运行,本次退出" exit 0fi LOG_FILE="${LOG_DIR} /sync-$(date +%F) .log" exec > >(tee -a "${LOG_FILE} " ) 2>&1log () { printf '[%s] %s\n' "$(date '+%F %T') " "$*" }reposync_run () { if dnf reposync --help >/dev/null 2>&1; then dnf reposync "$@ " elif command -v dnf5 >/dev/null 2>&1 && \ dnf5 reposync --help >/dev/null 2>&1; then dnf5 reposync "$@ " else log "ERROR: 未找到可用的 reposync 命令" return 1 fi }publish_repo () { local source_dir="$1 " local target_dir="$2 " local timestamp local metadata_dir local metadata_link_tmp timestamp="$(date +%Y%m%d%H%M%S) " metadata_dir="${target_dir} /.metadata/repodata-${timestamp} " metadata_link_tmp="${target_dir} /.repodata-link-${timestamp} " mkdir -p "${target_dir} " mkdir -p "${target_dir} /.metadata" if [[ ! -f "${source_dir} /repodata/repomd.xml" ]]; then log "ERROR: ${source_dir} 缺少 repodata/repomd.xml" return 1 fi rsync -aH \ --exclude='/repodata/' \ "${source_dir} /" \ "${target_dir} /" mkdir -p "${metadata_dir} " rsync -aH --delete \ "${source_dir} /repodata/" \ "${metadata_dir} /" test -f "${metadata_dir} /repomd.xml" ln -s ".metadata/$(basename "${metadata_dir} " ) " \ "${metadata_link_tmp} " mv -Tf \ "${metadata_link_tmp} " \ "${target_dir} /repodata" rsync -aH \ --delete-delay \ --exclude='/repodata' \ --exclude='/.metadata/' \ "${source_dir} /" \ "${target_dir} /" find "${target_dir} /.metadata" \ -mindepth 1 \ -maxdepth 1 \ -type d \ -name 'repodata-*' \ -printf '%T@ %p\n' \ | sort -nr \ | awk 'NR > 3 {$1=""; sub(/^ /, ""); print}' \ | xargs -r rm -rf -- log "发布完成:${target_dir} " }for repo in "${REPOSITORIES[@]} " ; do sync_dir="${SYNC_ROOT} /rocky/${ROCKY_VERSION} /${ARCH} /${repo} " publish_dir="${PUBLISH_ROOT} /rocky/${ROCKY_VERSION} /${ARCH} /${repo} " mkdir -p "${sync_dir} " mkdir -p "${publish_dir} " log "开始同步仓库:${repo} " reposync_run \ --repo="${repo} " \ --download-path="${sync_dir} " \ --norepopath \ --download-metadata \ --delete \ --gpgcheck \ --arch ="${ARCH} " \ --arch =noarch publish_repo "${sync_dir} " "${publish_dir} " log "仓库完成:${repo} " done log "全部仓库同步完成" SCRIPT
设置权限:
1 2 chmod 0750 /usr/local/sbin/sync-rocky-repos.shchown root:reposync /usr/local/sbin/sync-rocky-repos.sh
先手工运行:
1 2 sudo -u reposync \ /usr/local/sbin/sync-rocky-repos.sh
观察:
1 tail -f /var/log/yum-repo/sync-$(date +%F).log
8.2 关于原子切换 发布脚本使用了以下结构:
1 2 3 4 5 6 7 baseos/ ├── Packages/ ├── .metadata/ │ ├── repodata-20260731120000/ │ ├── repodata-20260731200000/ │ └── repodata-20260801040000/ └── repodata -> .metadata/repodata-20260801040000
Nginx 访问:
1 /baseos/repodata/repomd.xml
实际上会通过符号链接读取最新元数据。
切换时使用:
在同一文件系统内,重命名操作是原子的,可以避免客户端读到半份元数据。
8.3 首次发布时的注意事项 如果目标目录中已经存在普通的 repodata 目录,而不是符号链接,应在维护窗口先迁移:
1 2 3 mv \ /srv/yum-repo/rocky/10/x86_64/baseos/repodata \ /srv/yum-repo/rocky/10/x86_64/baseos/repodata.backup
然后运行发布脚本。
确认:
1 2 readlink -f \ /srv/yum-repo/rocky/10/x86_64/baseos/repodata
九、使用 systemd Timer 定时同步 相比直接写 Cron,systemd Timer 具备:
日志统一进入 Journal;
可查看上次和下次执行时间;
支持错过任务后补执行;
服务失败状态更明确;
更容易配置资源限制和安全沙箱。
9.1 创建 Service 创建 /etc/systemd/system/yum-repo-sync.service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [Unit] Description =Synchronize Rocky Linux RPM repositoriesDocumentation =https://dnf-plugins-core.readthedocs.io/en/latest/reposync.htmlAfter =network-on line.targetWants =network-on line.target[Service] Type =on eshotUser =reposyncGroup =reposyncExecStart =/usr/local/sbin/sync-rocky-repos.shNice =10 IOSchedulingClass =best-effortIOSchedulingPriority =6 TimeoutStartSec =12 hNoNewPrivileges =true PrivateTmp =true ProtectHome =true ProtectSystem =strictReadWritePaths =/srv/yum-syncReadWritePaths =/srv/yum-repoReadWritePaths =/var/log/yum-repoReadWritePaths =/run/lock
9.2 创建 Timer 创建 /etc/systemd/system/yum-repo-sync.timer:
1 2 3 4 5 6 7 8 9 10 11 [Unit] Description =Run Rocky Linux repository synchronization[Timer] OnCalendar =*-*-* 02 ,10 ,18 :15 :00 RandomizedDelaySec =15 mPersistent =true Unit =yum-repo-sync.service[Install] WantedBy =timers.target
每天执行 3 次:
并随机延迟 0~15 分钟,避免多台镜像服务器同时冲击上游。
加载:
1 2 systemctl daemon-reload systemctl enable --now yum-repo-sync.timer
检查:
1 2 systemctl list-timers yum-repo-sync.timer systemctl status yum-repo-sync.timer
手工触发:
1 systemctl start yum-repo-sync.service
查看日志:
1 journalctl -u yum-repo-sync.service -n 300 --no-pager
十、配置 Nginx 发布仓库 10.1 创建 Nginx 配置 创建 /etc/nginx/conf.d/yum-repo.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 server { listen 80 ; server_name yum.example.com; return 301 https://$host $request_uri ; }server { listen 443 ssl; http2 on ; server_name yum.example.com; root /srv/yum-repo; ssl_certificate /etc/nginx/ssl/yum.example.com.crt; ssl_certificate_key /etc/nginx/ssl/yum.example.com.key; ssl_protocols TLSv1.2 TLSv1.3 ; ssl_session_cache shared:YUM_SSL:20m ; ssl_session_timeout 1d ; access_log /var/log/nginx/yum-access.log; error_log /var/log/nginx/yum-error .log warn ; sendfile on ; tcp_nopush on ; tcp_nodelay on ; keepalive_timeout 65 ; keepalive_requests 10000 ; open_file_cache max=10000 inactive=60s ; open_file_cache_valid 120s ; open_file_cache_min_uses 2 ; open_file_cache_errors on ; location = /healthz { access_log off ; default_type text/plain; return 200 "ok\n" ; } location / { autoindex on ; autoindex_exact_size off ; autoindex_localtime on ; try_files $uri $uri / =404 ; } location ~* \.(rpm|drpm)$ { expires 1h ; add_header Cache-Control "public, max-age=3600" ; try_files $uri =404 ; } location ~* /repodata/ { expires 5m ; add_header Cache-Control "public, max-age=300" ; try_files $uri =404 ; } location ~ /\. { deny all; } }
为什么 RPM 和元数据的缓存时间不同:
RPM 文件一旦发布通常不会变化,可以使用较长缓存;
repomd.xml 会随着同步更新,不应缓存过久;
客户端首先读取元数据,再根据元数据下载 RPM;
元数据缓存过长会导致客户端长期看不到新版本。
检查:
启动:
1 2 systemctl enable --now nginx systemctl reload nginx
验证:
1 2 3 curl -I http://127.0.0.1/healthz curl -I https://yum.example.com/healthz curl -I https://yum.example.com/rocky/10/x86_64/baseos/repodata/repomd.xml
10.2 不要给 RPM 启用动态 gzip RPM、XZ、Zstandard、Gzip 元数据已经压缩,再次动态压缩通常没有收益,只会浪费 CPU。
因此不需要为:
1 2 3 4 5 *.rpm *.gz *.xz *.zst *.bz2
开启 Nginx gzip。
10.3 SELinux 配置 不要直接关闭 SELinux。
为发布目录设置 Web 内容上下文:
1 2 3 4 5 6 semanage fcontext \ -a \ -t httpd_sys_content_t \ '/srv/yum-repo(/.*)?' restorecon -Rv /srv/yum-repo
检查:
1 2 ls -ldZ /srv/yum-repols -lZ /srv/yum-repo/rocky/10/x86_64/baseos/repodata
如果 Nginx 只读取静态文件,不需要给它写权限。
10.4 firewalld 1 2 3 4 5 6 systemctl enable --now firewalld firewall-cmd --permanent --add-service=ssh firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reload
查看:
十一、复制 Rocky Linux GPG 公钥 Rocky 官方 RPM 已由 Rocky 的 GPG Key 签名。镜像服务器不应重新签名这些 RPM,只需要把原始 RPM 和元数据完整发布。
复制公钥:
1 2 3 4 5 6 cp \ /etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 \ /srv/yum-repo/keys/chmod 0644 \ /srv/yum-repo/keys/RPM-GPG-KEY-Rocky-10
验证:
1 2 curl \ https://yum.example.com/keys/RPM-GPG-KEY-Rocky-10
随机检查 RPM:
1 2 3 rpmkeys --checksig \ /srv/yum-repo/rocky/10/x86_64/baseos/Packages/*/*.rpm \ | head
如果仓库结构没有字母分层:
1 2 3 4 5 6 find \ /srv/yum-repo/rocky/10/x86_64/baseos \ -name '*.rpm' \ -print0 \ | xargs -0 -r rpmkeys --checksig \ | head
十二、配置 Rocky Linux 10 客户端 12.1 创建私服 Repo 文件 创建 /etc/yum.repos.d/company-rocky10.repo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 [company-rocky10-baseos] name =Company Rocky Linux 10 - BaseOSbaseurl =https://yum.example.com/rocky/10 /$basearch /baseos/enabled =1 gpgcheck =1 repo_gpgcheck =0 gpgkey =https://yum.example.com/keys/RPM-GPG-KEY-Rocky-10 metadata_expire =6 hskip_if_unavailable =False [company-rocky10-appstream] name =Company Rocky Linux 10 - AppStreambaseurl =https://yum.example.com/rocky/10 /$basearch /appstream/enabled =1 gpgcheck =1 repo_gpgcheck =0 gpgkey =https://yum.example.com/keys/RPM-GPG-KEY-Rocky-10 metadata_expire =6 hskip_if_unavailable =False [company-rocky10-extras] name =Company Rocky Linux 10 - Extrasbaseurl =https://yum.example.com/rocky/10 /$basearch /extras/enabled =1 gpgcheck =1 repo_gpgcheck =0 gpgkey =https://yum.example.com/keys/RPM-GPG-KEY-Rocky-10 metadata_expire =6 hskip_if_unavailable =False [company-rocky10-crb] name =Company Rocky Linux 10 - CRBbaseurl =https://yum.example.com/rocky/10 /$basearch /crb/enabled =0 gpgcheck =1 repo_gpgcheck =0 gpgkey =https://yum.example.com/keys/RPM-GPG-KEY-Rocky-10 metadata_expire =6 hskip_if_unavailable =False
这里路径固定使用主版本 10,避免客户端 $releasever 被解析为 10.0、10.1 或其他值后与服务端目录不一致。
12.2 测试私服,不立即关闭官方仓库 先执行:
1 2 3 4 5 6 7 dnf clean allrm -rf /var/cache/dnf dnf \ --disablerepo='*' \ --enablerepo='company-rocky10-*' \ makecache
查看:
1 2 3 4 dnf \ --disablerepo='*' \ --enablerepo='company-rocky10-*' \ repolist
测试查询:
1 2 3 4 dnf \ --disablerepo='*' \ --enablerepo='company-rocky10-*' \ info nginx
测试安装:
1 2 3 4 dnf \ --disablerepo='*' \ --enablerepo='company-rocky10-*' \ install -y tree
确认下载 URL 指向:
1 https://yum.example.com/
12.3 正式切换 确认私服完整可用后,再关闭系统原始仓库。
DNF5 环境可以使用:
1 2 3 4 dnf config-manager setopt baseos.enabled=0 dnf config-manager setopt appstream.enabled=0 dnf config-manager setopt extras.enabled=0 dnf config-manager setopt crb.enabled=0
如果当前版本不支持该语法,应通过:
1 dnf config-manager --help
确认本机语法,或直接在 /etc/yum.repos.d/ 中创建 override 配置。
切换后:
1 2 3 4 dnf clean all dnf makecache dnf repolist dnf upgrade --refresh
12.4 批量分发 Repo 文件 可以将 Repo 配置打成 RPM,例如:
1 company-repos-1.0.0-1.el10.noarch.rpm
由 RPM 安装:
1 2 /etc/yum.repos.d/company-rocky10.repo /etc/pki/rpm-gpg/RPM-GPG-KEY-COMPANY
相比 Ansible 直接复制文件,Repo RPM 的优势是:
有版本号;
可以升级和回滚;
可以查询来源;
可审计;
适合批量交付。
十三、搭建企业自有 RPM 仓库 13.1 创建目录 1 2 3 4 5 6 7 8 mkdir -p \ /srv/yum-repo/custom/el10/x86_64/Packageschown -R reposync:reposync \ /srv/yum-repo/customchmod -R 0755 \ /srv/yum-repo/custom
上传 RPM:
1 2 3 scp \ example-app-1.0.0-1.el10.x86_64.rpm \ repo01:/srv/yum-repo/custom/el10/x86_64/Packages/
检查包信息:
1 2 rpm -qpi \ /srv/yum-repo/custom/el10/x86_64/Packages/example-app-1.0.0-1.el10.x86_64.rpm
检查签名:
1 2 rpmkeys --checksig \ /srv/yum-repo/custom/el10/x86_64/Packages/example-app-1.0.0-1.el10.x86_64.rpm
13.2 首次生成元数据 1 2 3 4 createrepo_c \ --database \ --checksum sha256 \ /srv/yum-repo/custom/el10/x86_64
检查:
1 2 3 4 5 ls -lah \ /srv/yum-repo/custom/el10/x86_64/repodatatest -f \ /srv/yum-repo/custom/el10/x86_64/repodata/repomd.xml
13.3 增量更新元数据 新增或替换 RPM 后:
1 2 3 4 5 createrepo_c \ --update \ --database \ --checksum sha256 \ /srv/yum-repo/custom/el10/x86_64
--update 会复用已有元数据,通常比全量扫描更快。
13.4 控制 workers createrepo_c 本身已经会并行处理大量任务,不需要盲目把线程数调到很大。
可以限制在 4~8 个 Worker:
1 2 3 4 5 6 createrepo_c \ --update \ --workers 4 \ --database \ --checksum sha256 \ /srv/yum-repo/custom/el10/x86_64
对于机械盘或共享存储,线程过多可能让随机 I/O 更差。
13.5 Rocky Linux 10 元数据压缩变化 Rocky/RHEL 10 的 createrepo_c 默认可能使用 Zstandard 生成元数据。对于 Rocky Linux 10 客户端,这是合理选择。
如果同一个自有仓库还要兼容更老的 EL 客户端,应先验证其 DNF/libsolv 是否支持对应压缩格式。需要保守兼容时,可以显式使用 Gzip:
1 2 3 4 5 createrepo_c \ --update \ --compress-type=gz \ --checksum sha256 \ /srv/yum-repo/custom/el10/x86_64
不要只看“压缩率更高”,兼容性比省下几 MB 元数据更重要。
十四、为企业自有 RPM 配置 GPG 签名 gpgcheck=1 校验的是 RPM 包签名。
repo_gpgcheck=1 校验的是仓库元数据签名。
两者不是一回事。
flowchart TD
Client[DNF 客户端] --> Meta[下载 repomd.xml]
Meta -->|repo_gpgcheck| MetaSig[校验 repomd.xml.asc]
Meta --> RPM[下载 RPM]
RPM -->|gpgcheck| RPMSig[校验 RPM 内嵌签名]
14.1 创建 GPG Key 安装:
1 dnf install -y gnupg2 rpm-sign
生成签名密钥:
1 2 3 4 5 6 gpg \ --quick-generate-key \ 'Company RPM Repository <rpm@example.com>' \ rsa3072 \ sign \ 2y
查看:
1 gpg --list-secret-keys --keyid-format LONG
导出公钥:
1 2 3 4 5 gpg \ --armor \ --export \ 'Company RPM Repository' \ > /srv/yum-repo/keys/RPM-GPG-KEY-COMPANY
14.2 配置 rpmsign 创建签名账户的 ~/.rpmmacros:
1 2 %_signature gpg %_gpg_name Company RPM Repository
签名:
1 2 3 rpmsign \ --addsign \ /srv/yum-repo/custom/el10/x86_64/Packages/*.rpm
校验:
1 2 3 rpmkeys \ --checksig \ /srv/yum-repo/custom/el10/x86_64/Packages/*.rpm
CI/CD 中不应把私钥直接塞进普通构建镜像。更合理的做法包括:
使用专门签名节点;
使用硬件密钥或 HSM;
使用短生命周期签名凭证;
构建与签名分离;
对签名操作保留审计日志。
14.3 签名仓库元数据 每次执行 createrepo_c 后,repomd.xml 都可能改变,需要重新签名:
1 2 3 4 5 6 7 gpg \ --batch \ --yes \ --armor \ --detach-sign \ --local-user 'Company RPM Repository' \ /srv/yum-repo/custom/el10/x86_64/repodata/repomd.xml
生成:
14.4 客户端配置 创建 /etc/yum.repos.d/company-custom.repo:
1 2 3 4 5 6 7 8 9 [company-custom-el10] name =Company Custom RPM - EL10baseurl =https://yum.example.com/custom/el10/$basearch /enabled =1 gpgcheck =1 repo_gpgcheck =1 gpgkey =https://yum.example.com/keys/RPM-GPG-KEY-COMPANYmetadata_expire =5 mskip_if_unavailable =False
验证:
1 2 3 4 5 6 dnf clean metadata dnf \ --disablerepo='*' \ --enablerepo='company-custom-el10' \ makecache
十五、自有仓库发布脚本 创建 /usr/local/sbin/publish-custom-rpm.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 cat > /usr/local/sbin/publish-custom-rpm.sh <<'SCRIPT' set -Eeuo pipefailumask 022 REPO_DIR="${REPO_DIR:-/srv/yum-repo/custom/el10/x86_64} " GPG_NAME="${GPG_NAME:-Company RPM Repository} " WORKERS="${WORKERS:-4} " exec 9>/run/lock/custom-rpm-publish.lockif ! flock -n 9; then echo "已有发布任务运行" exit 0fi find "${REPO_DIR} /Packages" \ -type f \ -name '*.rpm' \ -print0 \ | xargs -0 -r rpmkeys --checksigif [[ -d "${REPO_DIR} /repodata" ]]; then createrepo_c \ --update \ --workers "${WORKERS} " \ --database \ --checksum sha256 \ "${REPO_DIR} " else createrepo_c \ --workers "${WORKERS} " \ --database \ --checksum sha256 \ "${REPO_DIR} " fi rm -f "${REPO_DIR} /repodata/repomd.xml.asc" gpg \ --batch \ --yes \ --armor \ --detach-sign \ --local-user "${GPG_NAME} " \ "${REPO_DIR} /repodata/repomd.xml" test -f "${REPO_DIR} /repodata/repomd.xml" test -f "${REPO_DIR} /repodata/repomd.xml.asc" echo "企业自有 RPM 仓库发布完成" SCRIPTchmod 0750 \ /usr/local/sbin/publish-custom-rpm.sh
正式生产中,应先把新 RPM 上传到暂存目录,完成签名、恶意软件扫描、依赖检查和验收后,再移动到正式 Packages 目录。
十六、同步 EPEL 仓库 安装 EPEL Repo 配置:
1 dnf install -y epel-release
查看实际 Repo ID:
1 dnf repolist --all | grep -i epel
同步:
1 2 3 4 5 6 7 8 9 10 11 mkdir -p /srv/yum-sync/epel/10/x86_64/epel repo_sync \ --repo=epel \ --download-path=/srv/yum-sync/epel/10/x86_64/epel \ --norepopath \ --download-metadata \ --delete \ --gpgcheck \ --arch =x86_64 \ --arch =noarch
EPEL 与 Rocky 官方仓库不是同一个项目,建议:
独立目录;
独立 Repo 文件;
独立 GPG Key;
独立同步和变更策略;
不要把所有仓库混进一个目录重新生成元数据。
十七、版本快照与冻结发布 在线镜像会不断变化。生产环境若要求“测试和生产安装到完全相同版本”,应建立快照。
17.1 使用 Reflink 创建快照 如果文件系统支持 Reflink,例如 XFS reflink:
1 2 3 4 5 6 7 8 9 SNAPSHOT_DATE="$(date +%F) " mkdir -p \ "/srv/yum-repo/snapshots/${SNAPSHOT_DATE} " cp -a \ --reflink=always \ /srv/yum-repo/rocky \ "/srv/yum-repo/snapshots/${SNAPSHOT_DATE} /"
如果不支持,会报错,不要悄悄退化成全量复制:
1 2 cp --help | grep reflink xfs_info /srv | grep reflink
17.2 使用 LVM Snapshot 如果仓库数据位于独立 LVM LV,可以在同步完成后创建文件系统级快照,再挂载为只读目录。
17.3 客户端固定快照 1 2 3 4 5 6 [company-rocky10-baseos-20260731] name =Company Rocky Linux 10 BaseOS Snapshot 2026 -07 -31 baseurl =https://yum.example.com/snapshots/2026 -07 -31 /rocky/10 /$basearch /baseos/enabled =1 gpgcheck =1 gpgkey =https://yum.example.com/keys/RPM-GPG-KEY-Rocky-10
更新流程可以设计为:
flowchart LR
Upstream[上游仓库] --> Mirror[滚动镜像]
Mirror --> Snapshot[创建日期快照]
Snapshot --> Test[测试环境验证]
Test --> Approve{审批}
Approve -- 通过 --> Prod[生产环境切换快照]
Approve -- 不通过 --> Reject[丢弃或修复]
十八、Nginx 性能调优 18.1 Worker 配置 编辑 /etc/nginx/nginx.conf:
1 2 3 4 5 6 7 8 9 user nginx;worker_processes auto;worker_rlimit_nofile 131072 ;events { worker_connections 8192 ; multi_accept on ; use epoll ; }
理论最大连接数近似为:
1 worker_processes × worker_connections
但连接数还受到:
文件句柄;
内核端口;
客户端带宽;
磁盘吞吐;
TLS 性能;
上游交换机和网卡;
共同限制。
18.2 sendfile 静态 RPM 下载适合开启:
1 2 sendfile on ;tcp_nopush on ;
Nginx 可通过内核发送文件,减少用户态拷贝。
但如果仓库目录位于某些 NFS、FUSE 或特殊分布式文件系统上,sendfile 可能存在兼容或一致性问题,需要根据存储实现验证。
18.3 open_file_cache 仓库中有大量 RPM 和元数据文件,开启文件描述信息缓存可以减少重复 stat:
1 2 3 4 open_file_cache max=10000 inactive=60s ;open_file_cache_valid 120s ;open_file_cache_min_uses 2 ;open_file_cache_errors on ;
不要设置无限大。仓库文件数量可能达到数十万,缓存本身也消耗内存。
18.4 TLS 会话缓存 1 2 ssl_session_cache shared:YUM_SSL:20m ;ssl_session_timeout 1d ;
可以降低客户端大量短连接时重复 TLS 握手的成本。
18.5 HTTP/2 DNF 客户端是否利用 HTTP/2 取决于其 HTTP 库和版本,但启用 HTTP/2 对浏览器、其他客户端和未来兼容没有坏处。
18.6 限速与带宽治理 如果防止单台服务器占满带宽,可以配置:
1 2 3 4 location ~* \.(rpm|drpm)$ { limit_rate_after 10m ; limit_rate 50m ; }
这里 50m 表示单连接约 50 MB/s,不是总带宽。
企业网络中更推荐在:
网关;
交换机;
QoS;
CDN;
负载均衡器;
做整体带宽治理。
十九、DNF 客户端调优 编辑 /etc/dnf/dnf.conf:
1 2 3 4 5 6 7 8 9 [main] gpgcheck =True installonly_limit =3 clean_requirements_on_remove =True best =True skip_if_unavailable =False max_parallel_downloads =10 timeout =30 retries =5
19.1 max_parallel_downloads 内网私服可以适当提高并行下载:
1 max_parallel_downloads =10
不要在数百台机器同时更新时全部设置为极高值。
例如:
1 2 3 500 台服务器 × 每台 20 个并行下载 = 最多 10000 个并行请求
这可能把仓库服务器、交换机或存储直接打趴。跑得快不是问题,大家同时跑得快才是问题。
更合理的做法是:
客户端并行数保持 5~10;
批量升级分批执行;
使用 Ansible serial;
Kubernetes 节点滚动升级;
给更新任务增加随机延迟。
内部滚动镜像:
企业自有高频发布仓库:
冻结快照仓库:
冻结仓库内容不再变化,没有必要反复请求元数据。
19.3 fastestmirror 客户端只有一个企业私服时,启用 fastestmirror 没有实际价值,反而可能增加探测开销。
19.4 skip_if_unavailable 生产基础仓库建议:
1 skip_if_unavailable =False
如果私服不可用,应让更新明确失败,而不是悄悄跳过仓库后得到一个“看似成功、实际没有更新”的结果。
二十、系统与磁盘调优 20.1 文件句柄 创建 /etc/systemd/system/nginx.service.d/limits.conf:
1 2 [Service] LimitNOFILE =131072
加载:
1 2 systemctl daemon-reload systemctl restart nginx
检查:
1 systemctl show nginx -p LimitNOFILE
系统级配置:
1 2 3 4 5 6 7 8 cat > /etc/sysctl.d/99-yum-repo.conf <<'EOF' fs.file-max = 2097152 net.core.somaxconn = 4096 net.ipv4.tcp_max_syn_backlog = 8192 vm.swappiness = 1 EOF sysctl --system
不要复制几十项来历不明的 TCP 参数。现代内核的默认值并不蠢,错误调优通常比不调更差。
20.2 文件系统 推荐:
XFS 或经过验证的 ext4;
独立逻辑卷;
SSD/NVMe;
保留 20% 以上可用空间;
持续监控 inode;
大规模镜像考虑 Reflink 或快照能力;
同步工作区与发布区可在同一高容量文件系统,便于原子 rename。
检查:
1 2 3 4 5 findmnt /srvdf -hT /srvdf -ih /srv iostat -xz 1 iotop
20.3 挂载参数 可以考虑:
减少访问时间更新,但应结合文件系统和运维规范评估。
不要使用会导致 Nginx 或同步工具异常的奇怪挂载参数,也不要把仓库放在可靠性未知的廉价对象网关挂载盘上。
20.4 page cache Nginx 静态文件性能高度依赖 Linux Page Cache。
因此仓库服务器内存不应只看 Nginx RSS。即使 Nginx 只使用几百 MB,剩余内存也会缓存热门 RPM。
检查:
不要看到 buff/cache 很大就认为“Linux 内存泄漏”。
二十一、同步任务调优 21.1 增量同步 reposync 会避免重复下载本地已有 RPM,因此稳定运行后的日常流量通常远低于首次全量同步。
21.2 避免任务重叠 脚本使用:
保证同一时间只有一个同步任务。
如果不加锁,上一次同步尚未结束,下一次任务再次启动,可能导致:
重复下载;
临时文件冲突;
元数据切换异常;
带宽和磁盘被双倍占用。
21.3 降低 I/O 优先级 systemd Service 中设置:
1 2 3 Nice =10 IOSchedulingClass =best-effortIOSchedulingPriority =6
让仓库下载服务优先于后台同步任务。
21.4 错峰执行 使用:
防止多台镜像服务器在整点同时同步。
21.5 不要过度同步 Rocky 官方仓库没必要每分钟同步。
常见策略:
仓库类型
推荐频率
Rocky BaseOS/AppStream
每 6~12 小时
Extras/CRB
每 6~12 小时
EPEL
每 4~8 小时
企业自有 RPM
发布事件触发
冻结快照
不再同步
二十二、Docker 部署方案 Docker 方案由两个部分组成:
1 2 repo-web:Nginx 容器,只读发布仓库 repo-sync:Rocky Linux 10 工具容器,按需执行 reposync
不建议在一个容器里同时运行:
1 Nginx + Cron + reposync + SSH
一个容器硬塞四个进程,最终得到的不是“简单”,而是一只穿着 Docker 外套的虚拟机。
二十三、Rocky Linux 10 安装 Docker Engine 卸载冲突包:
1 2 3 4 5 6 7 8 9 10 dnf remove -y \ docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine \ podman-docker
安装仓库管理工具:
1 dnf install -y dnf-plugins-core
添加 Docker RHEL 仓库:
1 2 3 dnf config-manager \ --add-repo \ https://download.docker.com/linux/rhel/docker-ce.repo
安装:
1 2 3 4 5 6 dnf install -y \ docker-ce \ docker-ce-cli \ containerd.io \ docker-buildx-plugin \ docker-compose-plugin
启动:
1 systemctl enable --now docker
验证:
1 2 3 docker version docker compose version docker info
不要配置:
未启用 TLS 和认证的 Docker Remote API 基本等于把宿主机 root 权限暴露到网络。
二十四、创建 Docker 项目 创建目录:
1 2 3 4 5 6 mkdir -p /opt/yum-repo-docker/syncmkdir -p /opt/yum-repo-docker/nginxmkdir -p /data/docker/yum-repo/syncmkdir -p /data/docker/yum-repo/repositorycd /opt/yum-repo-docker
24.1 同步容器 Dockerfile 创建 /opt/yum-repo-docker/sync/Dockerfile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 FROM rockylinux/rockylinux:10 RUN dnf -y install \ dnf5-plugins \ createrepo_c \ rsync \ findutils \ util-linux \ ca-certificates \ curl \ || dnf -y install \ dnf-plugins-core \ createrepo_c \ rsync \ findutils \ util-linux \ ca-certificates \ curl \ && dnf clean all \ && rm -rf /var/cache/dnf COPY sync-rocky-repos.sh /usr/local/bin/sync-rocky-repos.sh RUN chmod 0755 /usr/local/bin/sync-rocky-repos.sh ENTRYPOINT ["/usr/local/bin/sync-rocky-repos.sh" ]
注意 Shell 中 && 与 || 的优先组合容易造成误解。更严谨的 Dockerfile 可以写成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 FROM rockylinux/rockylinux:10 RUN set -eux; \ if ! dnf -y install dnf5-plugins; then \ dnf -y install dnf-plugins-core; \ fi ; \ dnf -y install \ createrepo_c \ rsync \ findutils \ util-linux \ ca-certificates \ curl; \ dnf clean all; \ rm -rf /var/cache/dnf COPY sync-rocky-repos.sh /usr/local/bin/sync-rocky-repos.sh RUN chmod 0755 /usr/local/bin/sync-rocky-repos.sh ENTRYPOINT ["/usr/local/bin/sync-rocky-repos.sh" ]
推荐使用第二种。
24.2 容器同步脚本 创建 /opt/yum-repo-docker/sync/sync-rocky-repos.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #!/usr/bin/env bash set -Eeuo pipefailumask 022 ROCKY_VERSION="${ROCKY_VERSION:-10} " ARCH="${ARCH:-x86_64} " SYNC_ROOT="${SYNC_ROOT:-/sync} " PUBLISH_ROOT="${PUBLISH_ROOT:-/repository} " REPOSITORIES="${REPOSITORIES:-baseos appstream extras crb} " reposync_run () { if dnf reposync --help >/dev/null 2>&1; then dnf reposync "$@ " elif command -v dnf5 >/dev/null 2>&1; then dnf5 reposync "$@ " else echo "未找到 reposync" >&2 exit 1 fi }for repo in ${REPOSITORIES} ; do sync_dir="${SYNC_ROOT} /rocky/${ROCKY_VERSION} /${ARCH} /${repo} " publish_dir="${PUBLISH_ROOT} /rocky/${ROCKY_VERSION} /${ARCH} /${repo} " mkdir -p "${sync_dir} " mkdir -p "${publish_dir} " echo "同步 ${repo} " reposync_run \ --repo="${repo} " \ --download-path="${sync_dir} " \ --norepopath \ --download-metadata \ --delete \ --gpgcheck \ --arch ="${ARCH} " \ --arch =noarch test -f "${sync_dir} /repodata/repomd.xml" rsync -aH \ --delete-delay \ "${sync_dir} /" \ "${publish_dir} /" done echo "全部仓库同步完成"
设置权限:
1 2 chmod 0755 \ /opt/yum-repo-docker/sync/sync-rocky-repos.sh
24.3 Nginx 配置 创建 /opt/yum-repo-docker/nginx/default.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 server { listen 80 ; server_name _; root /usr/share/nginx/html; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error .log warn ; sendfile on ; tcp_nopush on ; keepalive_timeout 65 ; keepalive_requests 10000 ; open_file_cache max=10000 inactive=60s ; open_file_cache_valid 120s ; open_file_cache_min_uses 2 ; location = /healthz { access_log off ; default_type text/plain; return 200 "ok\n" ; } location / { autoindex on ; autoindex_exact_size off ; autoindex_localtime on ; try_files $uri $uri / =404 ; } location ~* \.(rpm|drpm)$ { expires 1h ; add_header Cache-Control "public, max-age=3600" ; try_files $uri =404 ; } location ~* /repodata/ { expires 5m ; add_header Cache-Control "public, max-age=300" ; try_files $uri =404 ; } location ~ /\. { deny all; } }
生产环境 HTTPS 可以:
在宿主机 Nginx 终止 TLS;
使用 Traefik;
使用 HAProxy;
在容器 Nginx 中挂载证书;
由企业负载均衡器终止 TLS。
24.4 创建 compose.yaml 创建 /opt/yum-repo-docker/compose.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 services: repo-web: image: nginx:stable-alpine container_name: yum-repo-web restart: unless-stopped ports: - "8080:80" volumes: - /data/docker/yum-repo/repository:/usr/share/nginx/html:ro,Z - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro,Z healthcheck: test: - CMD-SHELL - wget -qO- http://127.0.0.1/healthz | grep -q ok interval: 30s timeout: 5s retries: 3 ulimits: nofile: soft: 131072 hard: 131072 mem_limit: 512m cpus: 2.0 repo-sync: build: context: ./sync image: company/rocky10-reposync:1.0.0 profiles: - sync environment: ROCKY_VERSION: "10" ARCH: "x86_64" REPOSITORIES: "baseos appstream extras crb" SYNC_ROOT: /sync PUBLISH_ROOT: /repository volumes: - /data/docker/yum-repo/sync:/sync:Z - /data/docker/yum-repo/repository:/repository:Z network_mode: bridge mem_limit: 4g cpus: 4.0
说明:
repo-web 长期运行;
repo-sync 使用 Compose Profile,不会随 docker compose up -d 自动启动;
需要同步时执行一次;
仓库数据保存在宿主机;
:Z 为 SELinux 分配私有容器标签;
正式环境应固定经过测试的镜像版本或 Digest,而不是永久依赖可漂移标签。
24.5 构建并启动 1 2 3 4 5 cd /opt/yum-repo-docker docker compose config docker compose build repo-sync docker compose up -d repo-web
检查:
1 2 3 docker compose ps docker compose logs -f repo-web curl http://127.0.0.1:8080/healthz
执行首次同步:
1 2 3 4 5 docker compose \ --profile sync \ run \ --rm \ repo-sync
查看目录:
1 2 3 4 find \ /data/docker/yum-repo/repository/rocky/10/x86_64 \ -maxdepth 2 \ -type d
验证:
1 2 curl -I \ http://127.0.0.1:8080/rocky/10/x86_64/baseos/repodata/repomd.xml
二十五、使用 systemd 定时运行 Docker 同步任务 创建 /etc/systemd/system/docker-yum-repo-sync.service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [Unit] Description =Synchronize Rocky repositories with Docker ComposeAfter =docker.service network-on line.targetRequires =docker.serviceWants =network-on line.target[Service] Type =on eshotWorkingDirectory =/opt/yum-repo-dockerExecStart =/usr/bin/docker compose --profile sync run --rm repo-syncTimeoutStartSec =12 hNice =10 IOSchedulingClass =best-effortIOSchedulingPriority =6
创建 /etc/systemd/system/docker-yum-repo-sync.timer:
1 2 3 4 5 6 7 8 9 10 [Unit] Description =Run Docker based YUM repository synchronization[Timer] OnCalendar =*-*-* 02 ,10 ,18 :30 :00 RandomizedDelaySec =15 mPersistent =true [Install] WantedBy =timers.target
启用:
1 2 systemctl daemon-reload systemctl enable --now docker-yum-repo-sync.timer
查看:
1 2 systemctl list-timers docker-yum-repo-sync.timer journalctl -u docker-yum-repo-sync.service -n 200 --no-pager
二十六、Docker 方案生产化调优 26.1 不要把仓库放在容器可写层 错误方式:
容器删除后数据也可能丢失,而且 OverlayFS 不适合承载大量长期静态制品。
正确方式:
1 2 volumes: - /data/docker/yum-repo/repository:/usr/share/nginx/html:ro
26.2 固定镜像 快速实验可以使用:
1 image: nginx:stable-alpine
生产环境建议将经过验证的版本固定为:
1 image: nginx:<明确版本>-alpine
或者固定 Digest:
1 image: nginx@sha256:<digest>
26.3 只读挂载 Nginx 只需要读取:
同步容器才有仓库写权限。
26.4 限制资源 Nginx:
1 2 mem_limit: 512m cpus: 2.0
同步容器:
同步任务可能需要更多内存和 CPU,但真正瓶颈通常还是磁盘和网络。
26.5 容器日志 为 Docker 配置日志轮转:
1 2 3 4 5 6 7 8 9 10 11 cat > /etc/docker/daemon.json <<'EOF' { "log-driver" : "local" , "log-opts" : { "max-size" : "50m" , "max-file" : "5" } } EOF systemctl restart docker
不要让容器日志把系统盘写满——这类故障的幽默之处在于,仓库 RPM 一个没坏,服务却因为几 GB 访问日志先去世了。
二十七、权限控制与内网安全 27.1 IP 白名单 内部仓库可以在 Nginx 中限制网段:
1 2 3 4 5 6 7 8 location / { allow 192.168.9.0 /24 ; allow 10.0.0.0 /8 ; deny all; autoindex on ; try_files $uri $uri / =404 ; }
27.2 Basic Auth 安装:
1 dnf install -y httpd-tools
创建密码文件:
1 2 3 htpasswd -c \ /etc/nginx/.htpasswd \ repo-reader
Nginx:
1 2 3 4 5 6 7 location /custom/ { auth_basic "Company RPM Repository" ; auth_basic_user_file /etc/nginx/.htpasswd; autoindex on ; try_files $uri $uri / =404 ; }
客户端 URL 可以携带账号密码,但会带来配置文件泄漏风险。更推荐:
内网访问;
VPN;
mTLS;
短期凭证;
网关鉴权;
IP 白名单。
27.3 不允许匿名上传 Nginx 仓库目录应只读发布。
RPM 上传应通过:
CI/CD;
SCP/SFTP;
API 网关;
制品审批流水线;
专用发布账户;
完成。
不要开放 WebDAV 让所有人随手把 RPM 扔进仓库。软件供应链不是网盘。
二十八、备份与恢复 28.1 哪些内容必须备份 官方滚动镜像可以重新同步,优先级相对较低。
必须重点备份:
1 2 3 4 5 6 7 8 企业自有 RPM GPG 私钥及吊销证书 GPG 公钥 Repo 配置 同步和发布脚本 Nginx 配置 冻结快照 审计记录
28.2 备份目录 1 mkdir -p /backup/yum-repo
备份企业自有仓库:
1 2 3 4 5 6 7 tar \ --xattrs \ --acls \ -czf /backup/yum-repo/custom-$(date +%F-%H%M%S).tar.gz \ -C /srv/yum-repo \ custom \ keys
备份配置:
1 2 3 4 5 6 7 tar \ -czf /backup/yum-repo/config-$(date +%F-%H%M%S).tar.gz \ /etc/nginx \ /etc/systemd/system/yum-repo-sync.service \ /etc/systemd/system/yum-repo-sync.timer \ /usr/local/sbin/sync-rocky-repos.sh \ /usr/local/sbin/publish-custom-rpm.sh
28.3 GPG 私钥 导出私钥时必须加密保存:
1 2 3 4 5 6 7 8 gpg \ --armor \ --export-secret-keys \ 'Company RPM Repository' \ > /root/company-rpm-private-key.ascchmod 0600 \ /root/company-rpm-private-key.asc
然后放入受控的 Secret Vault、离线介质或加密备份系统。
不要把私钥和 RPM 仓库放在同一台服务器上长期裸奔。仓库被入侵后,如果签名私钥也在,攻击者就同时拿到了“伪造包”和“盖公章”的能力。
28.4 恢复验证 恢复后必须执行:
1 2 3 4 5 6 dnf clean all dnf makecache dnf repoquery --available dnf download 测试包 dnf install 测试包 rpmkeys --checksig 下载后的RPM
只看 Nginx 返回 200 不代表仓库可用。真正的验收是 DNF 能解析依赖并完成安装。
二十九、监控与告警 29.1 基础监控 监控指标至少包括:
1 2 3 4 5 6 7 8 9 磁盘使用率 inode 使用率 Nginx 2xx/4xx/5xx 下载流量 请求延迟 同步任务状态 最后成功同步时间 repomd.xml 更新时间 GPG 校验失败数量
29.2 最后同步时间 同步成功后写入:
1 2 date -Is \ > /srv/yum-repo/.last-successful-sync
Nginx 禁止访问点文件时,可以额外发布:
1 2 date -Is \ > /srv/yum-repo/status/last-successful-sync.txt
监控系统定期检查:
1 2 curl -fsS \ https://yum.example.com/status/last-successful-sync.txt
29.3 仓库完整性检查 检查所有 repomd.xml:
1 2 3 4 5 find \ /srv/yum-repo \ -path '*/repodata/repomd.xml' \ -type f \ -print
使用 DNF 从服务端自身验证:
1 2 3 4 dnf \ --repofrompath=repo-check,https://yum.example.com/custom/el10/x86_64/ \ --repo=repo-check \ makecache
29.4 Nginx 日志统计 1 2 3 4 5 awk '{print $9}' \ /var/log/nginx/yum-access.log \ | sort \ | uniq -c \ | sort -nr
统计下载最多的 RPM:
1 2 3 4 5 6 awk '$7 ~ /\.rpm$/ {print $7}' \ /var/log/nginx/yum-access.log \ | sort \ | uniq -c \ | sort -nr \ | head -20
三十、常见故障排查 30.1 repomd.xml 404 检查 URL:
1 2 curl -I \ https://yum.example.com/rocky/10/x86_64/baseos/repodata/repomd.xml
检查服务端:
1 2 3 4 5 ls -lah \ /srv/yum-repo/rocky/10/x86_64/baseos/repodatareadlink -f \ /srv/yum-repo/rocky/10/x86_64/baseos/repodata
常见原因:
baseurl 路径多了一层或少了一层;
--norepopath 与目录设计不一致;
Nginx root 配错;
符号链接失效;
SELinux 拒绝访问;
同步没有成功。
常见原因是重新执行 createrepo_c 时丢失上游模块元数据。
解决思路:
官方镜像使用 --download-metadata;
不要重建官方仓库元数据;
检查同步目录中是否包含附加元数据;
清理客户端缓存后重试。
Rocky Linux 10 已不再沿用旧版 DNF Module 架构,但在同时维护 EL8/EL9 仓库时,这个问题仍需注意。
30.3 软件包存在但客户端找不到 检查:
1 2 3 find \ /srv/yum-repo/custom/el10/x86_64 \ -name 'example-app*.rpm'
检查元数据更新时间:
1 2 stat \ /srv/yum-repo/custom/el10/x86_64/repodata/repomd.xml
重新生成:
1 2 3 createrepo_c \ --update \ /srv/yum-repo/custom/el10/x86_64
客户端:
1 2 3 dnf clean metadata dnf makecache --refresh dnf repoquery example-app
30.4 GPG check FAILED 检查 RPM:
1 rpmkeys --checksig package.rpm
检查客户端导入的 Key:
1 2 rpm -qa 'gpg-pubkey*' rpm -qi gpg-pubkey
重新导入:
1 2 rpm --import \ https://yum.example.com/keys/RPM-GPG-KEY-COMPANY
不要通过:
长期掩盖签名问题。那不是修复,是把烟雾报警器拆了。
可能原因:
同步时客户端正好访问;
CDN/Nginx 缓存了旧元数据;
repomd.xml 和数据文件来自不同同步批次;
反向代理改写或缓存策略错误。
客户端临时处理:
1 2 3 dnf clean allrm -rf /var/cache/dnf dnf makecache --refresh
服务端应检查原子发布和缓存配置,而不是要求所有客户端每天手工清缓存。
30.6 Curl 200,但 DNF 失败 HTTP 200 只能证明文件存在。
进一步执行:
1 2 3 4 dnf \ --repofrompath=test ,https://yum.example.com/custom/el10/x86_64/ \ --repo=test \ makecache
检查:
元数据 XML;
Checksum;
RPM 路径;
签名;
HTTPS 证书链;
Content-Length;
代理缓存。
30.7 同步任务占满磁盘 检查:
1 2 3 4 5 6 7 8 du -xh \ /srv/yum-sync \ /srv/yum-repo \ | sort -h \ | tail -30df -hTdf -ih
常见原因:
工作区和发布区双份存储;
快照没有生命周期;
历史元数据未清理;
同步了 Debug、Source、ISO 等不需要的仓库;
Docker 日志没有轮转;
--delete 没有生效;
EPEL 或第三方仓库范围过大。
30.8 reposync 漏包 如果仅指定:
可能漏掉 noarch。
应同时指定:
1 2 --arch =x86_64 --arch =noarch
30.9 证书错误 客户端错误:
1 2 SSL certificate problem certificate verify failed
应修复:
证书 SAN;
完整证书链;
企业 CA 分发;
客户端系统时间;
DNS 解析。
不要长期配置:
三十一、生产环境检查清单 系统
仓库
Nginx
Docker
备份与恢复
三十二、总结 在 Rocky Linux 10 上搭建企业级 YUM 私服,核心不是安装一个 Nginx,然后把 RPM 放进去,而是建立一条完整的软件供应链:
1 2 3 4 5 6 7 8 9 可信上游 -> GPG 校验 -> 增量同步 -> 一致性发布 -> HTTPS 分发 -> 客户端签名校验 -> 快照冻结 -> 监控 -> 备份恢复
最终建议如下:
Rocky 官方仓库使用 reposync --download-metadata 完整镜像;
不要用 createrepo_c 随意覆盖官方仓库元数据;
企业自有 RPM 单独建仓并生成元数据;
x86_64 仓库必须同时考虑 noarch;
不要错误混用 --newest-only 和上游完整元数据;
通过工作区与发布区隔离同步过程;
使用原子方式切换 repodata;
使用 systemd Timer 而不是堆叠不可观测的 Cron;
对 RPM 和仓库元数据分别建立 GPG 校验;
Docker 中让 Nginx 只读,仓库数据持久化到宿主机;
用快照解决“今天能装,明天版本变了”的环境漂移;
定期进行真实安装和恢复演练。
YUM 私服平时安静得像一台普通文件服务器,一旦它失效,批量装机、系统升级、镜像构建和节点扩容会一起停下来。它不是最显眼的基础设施,但往往是那种坏一次,所有人就都记住它名字的基础设施。
参考资料