Replace the metadata generator for a flat RPM repository while preserving publication timestamps, signatures, and client caches.
This is the multi-page printable view of this section. .
Tutorials
- 1: Build a YUM Repository
- 2: Migrate an Existing YUM Repository
- 3: Build an APT Repository
- 4: Sign Your Repository
- 5: Serve and Publish Repositories
- 6: Build the pigsty-infra Repository
These tutorials cover repository creation, signing, publication, and updates. Start with Installation if you have not installed SOW.
Create RPM architecture views, filter packages, set version limits, and configure DNF.
Create a DEB pool with by-hash indexes and configure an APT client.
Sign metadata and RPM packages, then configure clients to verify signatures.
Publish to a filesystem target and serve it with Nginx.
Use both package formats and architectures, test installation, and maintain rolling channels, stable releases, and monthly snapshots.
Follow the examples
Run commands in order. Replace uppercase placeholders and /path/to/ inputs with your values. Examples under /srv require a writable directory; you can substitute a path under your home directory.
The YUM and APT tutorials each start with a new workspace. To combine them, create the second Dist in the existing pigsty Repository and keep both Dist definitions in sow.yml.
Each tutorial shows how to check its result. For a command error or interrupted operation, see Troubleshooting.
1 - Build a YUM Repository
This tutorial creates a new Managed RPM repository. Install SOW and Python 3, and prepare RPM files built for your clients. Use an empty /srv/sow directory that your user can create and write to, or substitute another writable path.
1. Create the workspace
el9 names this RPM package set. You can choose another name; it does not change which
operating systems the packages support.
2. Set membership policy
Edit the generated sow.yml. This example keeps one version per package and architecture
and excludes debug and LLVM JIT packages:
Check the configuration:
exclude runs before limit. SOW includes noarch packages in every enabled architecture view. Keep only CPU architecture names in architectures.
3. Add RPMs
Replace the input path with your RPM directory. add stores accepted packages in the pool, updates membership, and builds the indexes. Review its per-package output: excluded files are reported without failing the whole command.
The public tree has this shape:
RPM indexes point back to pool/ through relative paths. Serve or copy the complete pigsty/ tree so clients can download the packages.
4. Preview over HTTP
Run this in a separate terminal and leave it running:
The preview serves only the Repository root and is reachable from the same host. For a persistent service or clients on other hosts, use Serve Repositories.
5. Configure dnf
On the Linux host running the preview, save this as /etc/yum.repos.d/pigsty.repo. For a remote client, first publish the Repository and substitute its public base URL:
Then refresh and query the repository:
This example uses unsigned packages and metadata. Follow Sign Your Repository to enable verification. After the preview, remove this source file and stop the Python server.
6. Publish or export
Use sow publish for a filesystem or R2 target. It uploads package files and metadata in the order clients need. That guide also covers manual delivery to isolated hosts.
Some mirroring tools, including default dnf reposync, reject rpm-md package locations
that traverse to the root pool. Export a self-contained RPM leaf when such a consumer is
required:
The destination must be absent or empty. Export copies package files by default. Use --hardlink only on the same filesystem, and keep exported files read-only because they share storage with the source pool.
Update the repository
add builds the update automatically. With limit: 1, the newest version replaces the previous member for each package and architecture. Run sow build -r pigsty after changing configuration.
To withdraw a package, preview the selection with sow rm PACKAGE_NAME -r pigsty -d el9 --check, then repeat without --check. A package name selects all its versions and architectures; use a SHA-256 reference for one package object.
See Platforms & Integrations for tested clients.
2 - Migrate an Existing YUM Repository
This guide covers a flat directory of RPMs and repodata/, such as a repository previously
built with createrepo_c. It uses Plain sow create, without creating a Managed workspace.
The publication-time option requires SOW 0.5.0 or later.
1. Prepare a working copy
Back up the current packages and metadata. Make a separate working copy for the migration; copy RPM files without hard links if you plan to re-sign them. Check the installed binary:
Confirm that the version is 0.5.0 or later.
| Check | Action |
|---|---|
| Package layout | Plain reads top-level regular RPMs; migrate nested package layouts separately |
| Client features | Plain emits primary/filelists/other XML, not SQLite databases or module streams |
| Package trust | Keep valid upstream signatures, or deliberately replace them with your own key |
| Index trust | If clients use repo_gpgcheck=1, sign and verify the new repomd.xml separately |
| Existing clients | Include clients with cached metadata, using their existing URL and repository ID |
If your clients use modular filtering, keep the repository’s existing module_hotfixes=1 setting where needed. Plain metadata does not include module streams or profiles.
2. Preserve publication history
EL7 YUM can reject an index whose largest data timestamp is older than the cached index. Read <data><timestamp> in the current repomd.xml and retained copies, then use the largest value clients may have seen. If the live index was reset to zero, recover that value from earlier copies or publication records. Use data timestamps, not revision or file modification times.
Have the maintenance script serialize updates and save the greatest published timestamp outside the served tree. For changed content, choose:
Save the chosen timestamp before switching the public index. A failed publication may leave a gap in the sequence. Keep this record when you restore old content or move the repository to another directory. The +1 distinguishes publications; EL7 also accepts equal timestamps.
For unchanged content, reuse the timestamp after checking the indexes, referenced checksums, and signature. Repair a zero or regressed timestamp. Passing the current time on every run changes repomd.xml, so create can no longer report a no-op.
3. Build and sign a candidate
For this one-off example, prepare /srv/yum.candidate as an independent copy.
Set HISTORICAL_MAX to the verified maximum from the live index, the saved maximum publication time, and trusted history.
Set SIGN_KEY to the full fingerprint of your metadata signing key.
The following commands use Python 3 to choose a strictly newer time:
Record PUBLISH_TIME in the maintenance system before publishing the candidate. Compare the fingerprint from gpg --verify with the expected signing key.
--sign-with KEY signs unsigned RPMs and preserves existing signatures, including vendor signatures. To replace every package signature with your key, add --overwrite in the candidate copy. This changes package bytes; create builds indexes from the signed result. Generate the detached index signature after the final create run.
4. Test the candidate
Point a test client’s original repository URL at the candidate directory. Keep its old cache, repository ID, and signature settings. Refresh the index, then download and install a package. This checks cache compatibility, package paths, and signatures.
If you are migrating an automated update script, also check two cases:
- With the same inputs, options, and timestamp, a repeat build reports
noop=true. Preserve the existing valid detached signature. - After package content changes, the script advances the timestamp and the same client can fetch the new package.
When comparing with createrepo_c, check dependency meaning and client behavior; XML
formatting can differ. See Platforms & Integrations.
5. Publish the repository
Plain replaces multiple files; it does not make the whole directory switch atomically. Coordinate package replacement, metadata, detached signatures, and cache visibility in your publication layer. Retain metadata objects that cached clients may still request; raw Plain rebuilds may remove old SOW checksum-named metadata from the directory.
Keep the original backup and the saved maximum publication time. Roll back package content by rebuilding
and signing it as a newer publication. Restoring an old repomd.xml beside changed RPMs
can reintroduce checksum or timestamp failures.
Use the checks above for Plain repositories; sow check applies to Managed repositories. See sow create for options and Signing for package and metadata signatures.
3 - Build an APT Repository
This tutorial creates a new Managed DEB repository. Install SOW and Python 3, and prepare DEB files built for your clients. Use an empty /srv/sow directory that your user can create and write to, or substitute another writable path.
1. Create the workspace
The Dist name trixie becomes the client’s Suites value. You can choose another name;
it does not change which distributions the packages support.
2. Set membership policy
Edit the generated sow.yml if you need filtering or version limits:
Then validate it:
Use x86_64 and aarch64 in configuration. They generate Debian indexes for amd64
and arm64. Architecture-independent all packages enter both views.
3. Add DEBs
Replace the input path with your DEB directory. add stores accepted packages once and builds the indexes. The public tree is:
Paths in Packages are relative to the Repository root. Serve the complete pigsty/
directory so APT can reach both the indexes and the package files.
4. Preview over HTTP
Run this in a separate terminal and leave it running:
The preview serves only the Repository root and is reachable from the same host. For a persistent service or clients on other hosts, use Serve Repositories.
5. Configure APT
On the Linux host running the preview, save this as /etc/apt/sources.list.d/pigsty.sources. Use arm64 in Architectures for ARM64 clients. For a remote client, first publish the Repository and substitute its public URL:
Then refresh and query it:
The output should include trixie/main as a package source. Trusted: yes is used for
this unsigned example. To add signatures, follow Sign Your Repository
to sign metadata, install the public key, and configure Signed-By. After the preview,
remove the test source file and stop the Python server.
6. Publish the repository
Use sow publish for a filesystem or R2 target. It uploads packages and indexes before updating Release and its signatures. That guide also covers manual delivery to isolated hosts.
Update the repository
add builds the update automatically. With limit: 1, the newest version replaces the previous member for each package and architecture. Run sow build -r pigsty after changing configuration.
To withdraw a package, preview the selection with sow rm PACKAGE_NAME -r pigsty -d trixie --check, then repeat without --check. A package name selects all its versions and architectures; use a SHA-256 reference for one package object.
See Platforms & Integrations for tested clients.
4 - Sign Your Repository
Repository signing has two layers: metadata signatures and RPM package signatures. Configure each layer separately:
| Path | Output | Client control |
|---|---|---|
| RPM metadata | repodata/repomd.xml.asc |
repo_gpgcheck=1 |
| APT metadata | InRelease and Release.gpg |
Signed-By |
| RPM package body | embedded RPM signature | gpgcheck=1 |
APT verifies package hashes through the signed Release file, so DEBs need no separate package signature. Start with metadata signing. Add RPM package signing when clients require gpgcheck=1.
This guide uses the pigsty Repository from the YUM or APT tutorial. Run only the commands for the formats and Dists you created.
1. Create a dedicated key
Install GPG first. These commands create an unencrypted test key and export it to a private directory. For an existing protected key, use a passphrase reference as described below.
SOW reads the private key from /srv/sow-secrets, outside the public Repository. Give the SOW service account access to this directory if it differs from your user. Distribute repo-signing.pub to clients.
2. Configure metadata signing
In /srv/sow/sow.yml, merge these metadata entries into the Repository’s existing signing block. Keep its package policy and Dist definitions. Include only the formats you use:
Validate the configuration, rebuild, and check:
For a protected key, add passphrase: env://SOW_METADATA_PASSPHRASE next to key, then supply that environment variable to SOW. You can also use a passphrase file or agent://FINGERPRINT to sign through GPG. See passphrase references. Configuration stores references, not secret values.
3. Optional: inspect the metadata signer
The preceding sow check verifies metadata signatures. To inspect the signer with GPG,
run the commands for your Dists:
Compare the displayed fingerprint with the public key you distribute to clients.
4. Optional: sign RPM package bodies
For a new repository, configure package signing before importing RPMs. Add this block under the existing signing.rpm configuration:
Replace the placeholder with the 40-hex fingerprint in $FPR. Install rpm and gpg,
and make the secret key available in the GPG keyring used by rpm.
fill signs newly imported packages unless their signatures already verify against the
configured key or trusted_keys. See RPM signing policy for
other modes.
SOW signs a private copy during add; the input file stays unchanged. build checks stored RPMs against the policy but does not re-sign them.
If you already imported RPMs, check their signatures before enabling fill. Keep trusted upstream public keys in trusted_keys. For unsigned or untrusted stored packages, prepare correctly signed packages with new versions and filenames. Alternatively, configure signing in a new repository, then import the originals.
After configuring the policy, import a new package and check the result:
To inspect a resulting package independently, run rpmkeys --checksig on its file in
pigsty/pool/.
5. Enable dnf verification
First publish the signed Repository. Then transfer the public key to the client through a trusted channel:
Then enable the checks that correspond to what you signed:
If you sign only metadata, use repo_gpgcheck=1 and gpgcheck=0. With package checking enabled, install each trusted upstream package key as well as your own key. Then run sudo dnf makecache --refresh and install a package.
6. Enable APT verification
After publishing the signed Repository, install its public key on the client as a dedicated keyring:
Reference it from deb822 configuration and do not set Trusted: yes:
Run sudo apt update and install a package. If verification fails, compare the client keyring fingerprint with the signer shown by gpg --verify. For ARM64 clients, change Architectures to arm64.
Plain-mode RPM signing
Plain mode can sign RPM package bodies, but it does not sign repository metadata or create
an APT Release:
Replace 0123456789ABCDEF with your key ID or fingerprint. The matching private key must
be available to rpm and GPG. Existing signatures, including upstream signatures, are
preserved. To replace all RPM signatures with your key, use --overwrite on a separate
working copy. See sow create for key formats.
For an existing YUM repository, follow the migration guide
to preserve publication time and sign repomd.xml after the final create run.
Key changes
To rotate a metadata key, distribute the new public key, update the key reference, then run build, check, and publish. Changing the reference or resolved fingerprint marks affected Dists dirty.
For RPM package keys, use fill and keep the old public key in trusted_keys until packages signed by it are withdrawn or replaced. Keep the corresponding client key available during this period. Existing package objects remain unchanged.
Test the final repository with the DNF or APT versions your users run. See Platforms & Integrations for automated coverage.
5 - Serve and Publish Repositories
SOW generates static repository files. This guide publishes a Managed Repository to a separate directory, then serves that directory with Nginx. It continues from First Workspace.
The source is /srv/sow/local; the public copy will be /srv/repo-public/local.
Both contain the complete pool/ and dists/ tree. The workspace configuration and
private state stay outside the server root.
1. Build and check
Continue when check succeeds.
2. Add a filesystem target
Create the target’s parent directory and give the SOW operator write access:
Use the account that runs sow publish if it differs from your current user. The endpoint must already exist at its physical path; use no symlink components.
Add this target to /srv/sow/sow.yml:
SOW creates /srv/repo-public/local from endpoint and prefix. The three required booleans declare that this workspace is the source of truth and the only writer for that target. Use a separate prefix for other tools.
This example checks the published files through file:// and declares no intermediary cache. If you later add a proxy or CDN, use its public Repository URL for public_endpoint
and its longest cache lifetime for max_cache_ttl. Apply these changes through
publish --rebind. SOW uses them to check delivery and decide
when old files can be removed.
Publish the Repository:
SOW copies package files and immutable metadata first, then updates client entry points. It checks the result and records the published generation. Repeating publication without changes reports a no-op.
3. Configure Nginx
Install Nginx and add a server configuration like this. Replace repo.example.com with your hostname:
Check and reload the configuration:
These commands assume Nginx is already running. If this is a new installation, start it through your operating system’s service manager after nginx -t succeeds.
The client addresses are:
| Client | Address |
|---|---|
DNF baseurl |
http://repo.example.com/local/dists/el9/$basearch/ |
APT URIs |
http://repo.example.com/local |
APT Suites / Components |
bookworm / main |
Use the YUM or APT tutorial for the full source configuration. To verify signatures, distribute the public key and follow Sign Your Repository.
4. Check client access
Configure a client with the addresses above. Refresh its indexes and install a package.
This checks access to both dists/ and pool/. Apply the same access rules to both
directories.
Publish an update
After changing membership or configuration, build any pending changes and publish again:
If publication stops, rerun sow publish public to resume it. See sow publish for recovery and target changes, and sow gc for cleanup after the cache grace period.
Deliver to an isolated host
When SOW cannot reach the destination, stop source writers while creating and checking the copy:
- Run
sow checkon the source. - Copy the complete Repository to a new release directory on the destination. Preserve hard links, for example with
rsync -aHortar. - Verify transferred files against
sow changes 0or an archive checksum manifest. - Switch the server’s path to the new release directory.
- Keep package and immutable metadata URLs referenced by old indexes reachable until the maximum cache lifetime expires, then clean up the old release.
For paths absent from the new release, the HTTP server can fall back to the previous release directory during that interval.
Copying directly over a live tree can expose indexes before their package files arrive. Prepare the new directory first and switch it as a unit.
Serve a Plain repository
For Plain mode, serve the directory passed to sow create. Keep temporary hidden files
inaccessible; the Nginx dotfile rule above does this.
Publish to R2
Use provider: r2 for Cloudflare R2. Configure its endpoint, bucket, credential reference, public URL, and cache lifetime; see the target configuration reference. Run an initial publication to a test prefix and check it from your package clients.
R2 target garbage collection reports deletion candidates; SOW does not delete those objects. See Platforms & Integrations for tested storage operations.
6 - Build the pigsty-infra Repository
pgsty/infra-pkg is the upstream build source for Pigsty Infra packages.
This tutorial uses already-built RPMs and DEBs for x86-64 and ARM64. It creates an infra Repository with rolling channels, stable releases, and monthly snapshots. Install SOW, jq, and Docker first. The installation examples use pg-exporter; include that package and any dependencies not already in the client images.
1. Put the packages under ~/repo
Use a new ~/repo directory for this workspace. Copy packages from your infra-pkg checkout into two input directories:
Include both x86-64 and ARM64 packages. Step 3 checks their formats and architectures from package metadata.
2. Create the infra Repository and two Dists
Initialize a Workspace and create the Repository named infra:
Set limit: 1 for both Dists in ~/repo/sow.yml:
limit: 1 keeps only the newest version for each package name and native architecture. The rpm and deb
Dists are therefore rolling latest channels while still retaining both x86-64 and ARM64.
3. Ingest once and build once
Import both package formats, then build their indexes together:
After sow check succeeds, list the formats and architectures read from package metadata:
The output must include at least:
SOW reports canonical architecture names, so DEB amd64/arm64 appear here as x86_64/aarch64.
4. Read the generated filesystem tree
The important structure is:
SOW places all Dists under dists/ and packages under pool/. Publish or mount the complete ~/repo/infra tree so every Dist can reach its package files.
5. Serve the repository read-only with Nginx
Use the official nginx:alpine image and mount the Repository Root read-only at /infra:
Nginx can see only ~/repo/infra; it cannot see sow.yml or .sow/, and it cannot modify the repository.
6. Install an RPM from only infra on EL9
The Rocky Linux 9 container below is attached to the --internal network. The script removes every preconfigured
repository and enables only infra, so a successful installation cannot fall back to a public mirror.
An RPM baseurl must point to a concrete architecture view. dnf expands $basearch to x86_64 or aarch64.
7. Install a DEB from only infra on Ubuntu 24.04
APT points URIs at the Repository Root and uses the Dist name deb as Suites:
The lab uses unsigned repositories. Follow Sign Your Repository to enable repo_gpgcheck=1 for RPM metadata and Signed-By for APT. Use gpgcheck=1 when RPM package signatures are also available. Remove Trusted: yes from the signed APT source.
Docker containers use the host architecture by default. To test the other architecture,
add --platform linux/amd64 or --platform linux/arm64 to the corresponding docker run.
Cross-architecture execution requires binfmt/QEMU support.
8. Routine maintenance: add a new version
To update the repository, run add with the new packages. Suppose the four new pg-exporter
payloads are ready:
Because the latest Dists have limit: 1, the new version wins and the old version automatically leaves that
Dist’s Desired Membership. The old bytes are not deleted immediately, and relaxing policy later does not make
the old membership reappear automatically.
Rerun the EL9 and Ubuntu clients from sections 6 and 7, refresh metadata, then install or upgrade to complete the update acceptance test.
To withdraw a bad package, use sow ls -r infra -d rpm --json to find its SHA-256. Preview with sow rm sha256:DIGEST -r infra -d rpm --check, then repeat without --check. Use -d deb for DEBs. A SHA-256 reference selects one package object; a bare name selects all its versions and architectures. sow gc handles unused pool files separately.
9. Two retention layers: latest and stable
limit: 1 makes rpm and deb good rolling channels but cannot express “keep every formally released version.”
Create two more Dists for that purpose:
The new Dists default to limit: 0, which means retain every version. The resulting policy is:
| Dist | Format | limit |
Role |
|---|---|---|---|
rpm |
RPM | 1 | RPM latest |
deb |
DEB | 1 | DEB latest |
rpm-stable |
RPM | 0 | Accumulate promoted RPMs |
deb-stable |
DEB | 0 | Accumulate promoted DEBs |
Stable accumulates the versions you explicitly promote after creating the Dists.
10. Promote latest into stable
Promote packages by adding the source Dist’s pool files to the target Dist. SOW verifies and reuses the existing package objects, so promotion adds no second copy. Pause other repository writers before exporting the manifests. Resume them after promotion finishes.
First require clean source state and save the promotion manifests:
Add the recorded pool objects to the stable Dists:
Each add should report reused. If the loop stops midway, the source Dists are unchanged; correct the failure
and rerun the same manifest. Over subsequent promotions, rpm/deb retain only the latest version while
rpm-stable/deb-stable accumulate release history.
11. Take the 2026-08 snapshot from stable
A client-visible monthly snapshot is another pair of Dists:
Pause other repository writers while creating the snapshot. First save the stable package lists:
Add those manifests to the corresponding snapshot Dists:
To keep this build after you eventually remove the snapshot Dists, also retain the current Repository Generation:
retain stores this build’s metadata and protects its package files from garbage collection.
Clients still use the snapshot Dists. SOW does not make these Dists read-only; keep their
membership and effective configuration unchanged to preserve their contents.
12. Client address map
One Nginx service and one shared infra/pool support every 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 |
Use sow dist ls -r infra to see all six Dists.
The rolling Dists keep the latest packages, stable Dists accumulate promoted releases, and monthly Dists keep the selected snapshot. All views share the same package pool.
Stop the temporary service when the lab is complete: