托管 RPM 仓库:分架构视图、noarch 中性投影、debuginfo 过滤、版本数量上限,以及可用的 dnf 客户端配置。
这是本节的多页打印视图。 .
教程
- 1: 搭建 YUM 仓库
- 2: 搭建 APT 仓库
- 3: 仓库签名
- 4: 服务与发布仓库
- 5: 演练构建 pigsty-infra 仓库
这里的教程都从全新工作区开始。请按顺序执行命令,并按你的环境替换大写占位符与包路径。
如果还没装 SOW,先看安装与快速上手。 下面的教程介绍 Managed 仓库路径。
托管 DEB 仓库:Debian 风格包池、by-hash 索引与 deb822 客户端配置。
生成专用 GPG 签名钥,为仓库元数据与 RPM 包签名,并配置客户端拒绝一切未签名内容。
用 Nginx 服务 Repository,并把已校验 Generation 发布到配置好的 filesystem target, 同时避免暴露工作区私有状态。
把 infra-pkg 已产出的双架构 RPM 与 DEB 组织成真实仓库,并演练本地安装、滚动更新、 Stable 晋升与月度快照。
先看哪篇
| 你的处境 | 从这里开始 |
|---|---|
| 你要向 dnf 客户端分发 RPM | 搭建 YUM 仓库 |
| 你要为 Debian 或 Ubuntu 分发 DEB | 搭建 APT 仓库 |
| 需要已签名元数据或已签名 RPM 包体 | 仓库签名 |
| 树已经建好,但外部无法访问 | 对外服务 |
| 想把现有双架构包池变成可维护的 Infra 仓库 | 演练构建 pigsty-infra 仓库 |
YUM 与 APT 两篇是彼此独立的全新 Workspace 路径。实际使用中,如果它们适合共用同一所有权 边界,一个 Workspace 的同一 Repository 可以同时容纳 RPM 与 DEB Dist。
本板块约定
Shell 代码块里的命令不带 $ 提示符,方便整块复制。输出单独成块放在命令下方,只有一行时用注释
标注。需要你自行替换的值一律写成 大写。
每篇教程结尾都有验证步骤。sow check 返回 0,才证明所选 Repository 完整且与记录的
Generation 一致;非零结果不应作为交付物。
1 - 搭建 YUM 仓库
本教程从零创建一个 Managed RPM 仓库。你需要一个可写目录,以及一个或多个 RPM 文件。
1. 创建工作区
Dist 名称由你定义。SOW 不会根据 el9 推断操作系统版本。
2. 配置成员策略
编辑生成的 sow.yml。下面的配置按包名与架构各保留一个版本,并排除调试包:
手工修改配置后,先校验再写仓库状态:
策略先执行 exclude,再执行 limit。noarch 包会投影到每个启用的架构视图,不能写进
architectures。
3. 添加 RPM
add 解析包头,将每个接纳的包只保存一次,更新 Desired 成员关系并落成新 Generation。
被策略排除的输入会逐项报告,但不算命令失败。
公共树如下:
rpm-md 的 location href 通过相对路径访问根目录下的 pool/。不要只复制某个架构目录;
它不是独立仓库。
4. HTTP 预览
本地预览可以直接使用:
在另一个终端检查入口:
长期服务应使用持续维护的 HTTP Server。它必须完整暴露 pigsty/ 树,确保客户端解析后落到
pigsty/pool/ 的软件包 URL 可访问。
5. 配置 dnf
将 REPO_HOST 替换为客户端可访问的地址:
刷新并查询仓库:
这里有意使用未签名配置。只有完成仓库签名后,才应打开客户端验签。
6. 发布或导出
交付前必须通过深度校验:
向已配置的 filesystem 或 R2 目标交付时,使用 sow publish。
只有先复制到离线 staging、再原子切换上线时,才适合整根复制;不要对在线仓库做无序原地同步。
默认 dnf reposync 等工具会拒绝指向根包池的父级相对路径。遇到这种消费者时,导出一份
自包含 RPM Leaf:
目标目录必须不存在或为空。默认会复制包体;--hardlink 只适用于同一文件系统、可信且只读的
显式优化场景。
更新仓库
add 与 rm 修改 Desired 成员关系;策略或签名配置变化后用 build 收敛;发布门禁是
check,不能只看 status。
自动化客户端与平台覆盖见平台与集成。
2 - 搭建 APT 仓库
本教程从零创建一个 Managed DEB 仓库。你需要一个可写目录,以及一个或多个 DEB 文件。
1. 创建工作区
Dist 名称会成为 APT Suite。它由你定义;SOW 不会根据 trixie 推断发行版语义。
2. 配置成员策略
如果需要过滤或限制版本,编辑生成的 sow.yml:
然后校验:
配置中保存规范架构名,渲染时使用 Debian 生态名称:x86_64 对应 amd64,aarch64
对应 arm64;中立架构 all 包会进入两个视图。
3. 添加 DEB
接纳的包体只保存一次。公共树如下:
pool/ 下的路径按规范化源码包名分组;Packages 中的 Filename 相对 Archive Root;
SOW 会写入 SHA-256 by-hash 副本,并在 Release 中声明。
4. HTTP 预览
本地预览可以直接使用:
检查协议入口:
长期服务应使用持续维护的 HTTP Server,并完整暴露 pigsty/ 树。
5. 配置 APT
将 REPO_HOST 替换为客户端可访问的地址。未签名测试仓库可使用显式信任的 deb822 配置:
刷新并查询:
Trusted: yes 会关闭真实性校验,只适合受控测试。签名仓库应删除该行并配置 Keyring:
打开 Signed-By 前,请先完成仓库签名。
6. 安全发布
交付前必须通过深度校验:
向已配置的 filesystem 或 R2 目标交付时,使用 sow publish。
如果使用其他传输方式,应把完整仓库复制到离线 staging,再原子切换上线。不要逐文件更新在线
dists/ 树,否则客户端可能同时看到不同 Generation 的元数据与包体。
更新仓库
策略或签名配置变化后用 build 收敛;发布门禁是 check,不能只看 status。
自动化客户端与平台覆盖见平台与集成。
3 - 仓库签名
SOW 提供两条互相独立的签名路径:
| 路径 | 产物 | 客户端开关 |
|---|---|---|
| RPM 元数据 | repodata/repomd.xml.asc |
repo_gpgcheck=1 |
| APT 元数据 | InRelease 与 Release.gpg |
Signed-By |
| RPM 包体 | RPM 内嵌签名 | gpgcheck=1 |
APT 通过签名的 Release 信任包哈希;SOW 不重签 DEB 包体。先配置元数据签名;只有当你负责
这些 RPM 字节的签名策略时,再启用包体签名。
1. 创建专用密钥
下面生成一把无口令的示例密钥。生产环境应使用受保护密钥并配置 passphrase 引用,详见
配置参考。
私钥必须放在 Workspace 公共 Repository 树与所有 Web Root 之外。若 SOW 由专用服务账户运行,
目录 owner 应是该账户,而不是交互用户。只向客户端分发 repo-signing.pub。
2. 配置元数据签名
在 /srv/sow/sow.yml 的 Repository 下添加所需配置;未使用的包生态可以省略:
解析密钥引用、重建并执行发布门禁:
受口令保护的密钥可在 key 旁增加 passphrase: env://SOW_METADATA_PASSPHRASE,或使用
有界文件引用。SOW 不会把密钥或口令内容写入配置、SQLite、JSON 或日志。
3. 手工验证元数据
按实际 Dist 与架构调整路径:
sow check 会在深度一致性校验中检查配置的签名身份。建立客户端信任根时,仍应手工验证一次。
4. 可选:签署 RPM 包体
只有客户端要求内嵌包签名时,才添加 rpm.packages:
将占位符替换为 $FPR 中的 40 位十六进制指纹。该操作要求:
- 安装
rpm与gpg; - 匹配的私钥存在于
rpm使用的环境 GPG Keyring 中; fill保留已经由配置 key 或trusted_keys签好的包;always重签所有未由配置身份签好的包;never保持输入字节不变。
SOW 只会对私有 staged 副本调用 rpm --addsign 或 rpm --resign,不会修改输入文件。
修改策略后重新校验并构建:
用 rpmkeys --checksig /path/to/package.rpm 检查结果。
5. 启用 dnf 验签
通过可信通道把公钥传到客户端:
再打开与你实际签名范围对应的检查:
没有配置包体签名时,将 gpgcheck 设为 0;既然已经配置元数据签名,就不要关闭
repo_gpgcheck。
6. 启用 APT 验签
将公钥安装为独立 Keyring:
在 deb822 配置中引用它,且不要设置 Trusted: yes:
执行 apt update。任何签名错误都应视为部署失败,不能靠削弱客户端配置绕过。
Plain 模式 RPM 签名
Plain 模式可以签署 RPM 包体,但不会签署仓库元数据,也不会生成 APT Release:
key 必须是恰好 16、40 或 64 位十六进制字符,不能带 0x 前缀;匹配私钥必须能被环境中的
rpm/GPG 使用。不带 --overwrite 时,已有签名的 RPM 保持字节不变;带上该参数则显式重签
所有 RPM。SOW 先签署私有 staged 副本,再替换包体与元数据。
更换密钥
改变 key 引用或解析出的指纹会让相关 Dist 变为 dirty。元数据 key 可以先分发新公钥,再重建、
校验并切换客户端。RPM 包 key 必须分阶段轮换:Package Object 不可变,build 遇到不满足新策略的
既有 RPM 会拒绝,而不是原地重签。旧软件包坐标尚未下架或由新 Release 替代前,应使用 fill
并把旧公钥保留在 trusted_keys。最后在目标环境做真实客户端验收。
最后应使用生产中的确切 dnf/APT 版本与信任策略验收签名仓库。自动化覆盖见 平台与集成。
4 - 服务与发布仓库
SOW 只生成静态文件,不是 HTTP 服务器。本指南把可写 Workspace 与 Nginx 服务路径分开。
公共与私有路径
| 模式 | 公共单元 | 绝不能服务 |
|---|---|---|
| Plain | 传给 sow create 的目录 |
临时 .sow-plain-stage-* 输出;没有持久 journal |
| Managed | 一个 Repository 的完整 pool/ + dists/ 树 |
Workspace sow.yml、.sow/、SQLite、锁、日志与 staging |
第一个工作区里的源 Repository 是 /srv/sow/local。不要把
/srv/sow 设为 document root。
1. 校验源 Generation
只有 check 返回 0 才继续。status 适合诊断;check 才是完整只读交付证明。
2. 配置 filesystem target
先创建 endpoint 目录。它必须是真实、规范目录,不能是 symlink;SOW 不会替你创建缺失 endpoint。
第二条命令把写权限交给当前操作者;若 sow publish 由专用服务账户执行,应改为该账户。
在 /srv/sow/sow.yml 中增加 target:
三个布尔字段都是必填安全确认。endpoint 与 prefix 合并为 /srv/repo-public/local;
SOW 会在预先存在的 endpoint 下创建并拥有该 prefix。
校验并发布:
发布先复制不可变包体和元数据,再更新可变协议指针,随后校验结果并记录 target checkpoint。 同一 Generation 重复发布是幂等空操作。
不要让其他工具写入同一 target prefix。target 契约是单 writer、独占写入。
3. 用 Nginx 服务 target
校验配置后 reload Nginx。客户端 URL 为:
如果元数据或包体已签名,请单独发布对应公钥并配置 gpgkey/Signed-By;私钥绝不能放在
document root 下。
4. 验证服务入口
随后从客户端主机运行真实包管理器。HTTP 可达不等于客户端已验证,两层都要检查。
完整 Repository prefix 必须使用同一访问策略。RPM 元数据可能通过 ../../../pool/...
解析包路径,APT Filename 也直接指向 pool/...。只保护 dists/ 而误放开或拦截 pool/
都会破坏仓库。
手工与隔离交付
如果 sow publish 无法到达目标:
- 在源端运行
sow check; - 把完整 Repository 复制到新的、非 live staging/release 目录;
- 用
sow changes 0或 archive manifest 校验传输哈希; - 原子切换操作者拥有的父级引用到新目录;
- 保留上一版,直到客户端与缓存越过它。
不要直接对 live Repository root 执行无序 rsync --delete。它不保留 SOW 的指针顺序、
target checkpoint、缓存 grace 或恢复状态。sow changes 描述 Generation 差异,不代表可以
绕过这些控制直接修改 live target。
R2 target
provider: r2 使用 S3 兼容存储传输与只报告的 target GC。传输集成会针对固定 MinIO fixture
验证 list、HEAD、GET 与条件 PUT。启用生产目标前,请先在非生产 prefix 验证凭据、bucket
policy、公共 endpoint、缓存行为、重放与恢复。详见平台与集成。
下一步
5 - 演练构建 pigsty-infra 仓库
pgsty/infra-pkg 是 Pigsty Infra 软件包的上游构建源码。
本教程假设双架构 RPM 与 DEB 已经构建完成,只处理后半程:从一堆包开始,用 SOW 建成真正可消费、可维护的 infra 仓库。
1. 把包集中到 ~/repo
本教程固定使用 ~/repo,不再为每条路径定义环境变量。先把已有包复制进两个输入目录:
先确认四个“格式 × 架构”象限都有真实包体:
四个结果都必须大于零。此时目录只有输入包池:
2. 创建 infra Repository 与两个 Dist
初始化 Workspace,并创建名为 infra 的 Repository:
现在模型已经确定:
打开 ~/repo/sow.yml,把配置整理为:
limit: 1 按“包名 + 原生架构”只保留最新一个版本。因此 rpm 与 deb 就是两个滚动更新的
latest channel;它们仍会同时保留 x86-64 与 ARM64 两个架构。
3. 一次性导入并构建
先更新 Desired Membership,最后只构建一次:
sow check 返回 0,才算初始化完成。再核对 SOW 从包头读出的真实格式与架构:
预期至少出现:
SOW 使用规范化架构名,因此 DEB 的 amd64/arm64 在这里显示为 x86_64/aarch64。
4. 看懂生成的目录
打印实际目录:
关键结构应当是:
这里有一个容易混淆、但必须记住的路径规则:SOW 的 Dist 固定放在 dists/ 下。因此逻辑上的
infra/rpm 与 infra/deb,真实视图路径分别是 /infra/dists/rpm/ 和 /infra/dists/deb/;
包体则统一放在 /infra/pool/。发布或挂载时必须使用完整的 ~/repo/infra,不能只拿走某个 Dist。
5. 用 Nginx 只读服务仓库
使用官方 nginx:alpine 镜像,把 Repository Root 只读挂载到 /infra:
直接检查两种索引入口:
Nginx 只看得到 ~/repo/infra,既看不到 sow.yml 与 .sow/,也无法修改仓库。
6. 在 EL9 中只用 infra 安装 RPM
下面的 Rocky Linux 9 容器位于 --internal 网络中。脚本先删除所有预置仓库,再只启用刚创建的
infra,因此成功安装不能依赖公网软件源。
RPM 的 baseurl 必须落到具体架构视图。$basearch 会由 dnf 展开为 x86_64 或 aarch64。
7. 在 Ubuntu 24.04 中只用 infra 安装 DEB
APT 的 URI 指向 Repository Root,Suites 才是 Dist 名 deb:
本教程使用隔离 HTTP 仓库,所以临时关闭了验签。正式服务应配置 RPM/APT 元数据签名,并移除
gpgcheck=0 与 Trusted: yes。
Docker 默认验证宿主机架构。若要完成四格运行矩阵,分别给两条 docker run 增加
--platform linux/amd64 与 --platform linux/arm64 后各跑一次;跨架构运行需要 Docker 的
binfmt/QEMU 支持。仓库清单检查与客户端安装检查是两个独立门禁。
8. 日常维护:添加一个新版本
更新仓库的正常动作是 add,不是先删除旧包。假设已经拿到新版 pg-exporter 的四个包体:
因为 latest Dist 配了 limit: 1,新版本胜出后,旧版本会自动退出该 Dist 的 Desired Membership。
旧字节不会被立即删除,也不会因为以后放宽策略而自动回来。
可以重新运行第 6、7 节的客户端,先 makecache/update,再安装或升级,完成更新验收。
正常发布不要先 sow rm。如果某个错误包必须紧急撤回,先用 sow ls -r infra -d rpm --json
或对应的 -d deb 找到精确 SHA-256,
再依次执行 sow rm sha256:... -r infra -d rpm --check 与不带 --check 的同一命令。
rm 只删除 Dist Membership,pool 字节仍由保守的 sow gc 独立回收;不要用裸包名误删所有版本与架构。
9. 两层保留策略:latest 与 stable
rpm、deb 的 limit: 1 适合持续滚动,但不能表达“保留所有正式发布历史”。为此再创建两个 Dist:
新 Dist 的默认 limit 是 0,表示保留所有版本。最终策略是:
| Dist | 格式 | limit |
角色 |
|---|---|---|---|
rpm |
RPM | 1 | RPM latest |
deb |
DEB | 1 | DEB latest |
rpm-stable |
RPM | 0 | 累积所有已晋升 RPM |
deb-stable |
DEB | 0 | 累积所有已晋升 DEB |
注意:stable 不是把“包池里的所有历史”自动放回来,而是从现在开始,累积每次明确晋升的版本。
10. 把 latest 晋升到 stable
SOW 没有独立的 promote 命令。当前可靠做法是先冻结写入并导出源 Dist 的精确 Membership,
再把这些对象加入目标 Dist。输入直接取自 infra/pool;SOW 会校验并复用已有 Package Object,
不会重新打包,也不会在 pool 中复制第二份包体。
先确保源状态干净,并保存本次晋升清单:
在晋升结束前暂停对 rpm 与 deb 的写入,然后复用这些 pool 对象:
每次 add 应报告 reused。若中途失败,源 Dist 不受影响;修复问题后对同一清单重跑即可。
随着以后重复晋升,rpm/deb 仍只保留最新版本,而 rpm-stable/deb-stable 会逐次累积历史版本。
11. 从 stable 创建 2026-08 快照
客户端可见的月度快照也是两个新的 Dist:
在快照窗口内暂停 stable 写入,先把其精确 Membership 固化为清单:
再把清单加入对应快照 Dist:
把这次已验证的完整 Repository Generation 也加入保留集合,防止后续 GC 把它当作不可达历史处理:
retain 保留的是整个 Repository Generation,用于恢复与 GC 安全根;客户端可见的固定 URL 仍由
rpm-202608 与 deb-202608 两个 Dist 提供。SOW 尚不强制快照 Dist 只读,因而创建后不再对它们
执行 add 或 rm 是维护规约的一部分。
12. 客户端地址总表
同一个 Nginx 与同一份 infra/pool 支撑所有 channel:
| Channel | dnf baseurl |
APT URIs / Suites |
|---|---|---|
| latest | http://infra-nginx/infra/dists/rpm/$basearch/ |
http://infra-nginx/infra / deb |
| stable | http://infra-nginx/infra/dists/rpm-stable/$basearch/ |
http://infra-nginx/infra / deb-stable |
| 2026-08 | http://infra-nginx/infra/dists/rpm-202608/$basearch/ |
http://infra-nginx/infra / deb-202608 |
最终验收:
到这里,我们得到的不是一次性演示目录,而是一个可以继续收包、晋升与做月度快照的真实 Infra
Repository:rpm/deb 负责快速更新,rpm-stable/deb-stable 负责积累正式历史,月度 Dist 提供固定入口,
所有视图复用同一份不可变包体。
实验结束后可停止临时服务: