Migrate from createrepo_c / reprepro

Take over an existing repository in place or move a reprepro archive into a workspace, with a real side-by-side of what changes.

You already have a repository. This tutorial replaces the tool that builds it without breaking the clients that consume it — including the leftover files nobody warns you about and the layout differences you should know before, not after.

Two paths, depending on what you have. Read the one that matches, then the comparison at the end.

Choose your target

You haveMigrate toRead
createrepo_c on a directory of RPMsPlain mode, in placePath A
dpkg-scanpackages on a flat DEB directoryPlain mode, in placePath A
A reprepro archive with pool/ and dists/Managed workspacePath B
createrepo_c plus scripts for multiple architecturesManaged workspacePath B

Plain mode is the drop-in replacement: same directory, same URLs, one command instead of a toolchain. Managed mode is a change of shape — a workspace, a pool, per-architecture views — and gives you membership policy, transactional builds, and an audit trail. Migrate to Plain first if you just want the dependency gone; move to Managed when the repository’s lifecycle is what hurts.

Path A: createrepo_c, in place

sow create indexes a directory of packages exactly where they sit. Nothing moves, nothing is renamed, and no package is deleted.

The starting state

A repository built by createrepo_c 0.20.1:

ls repodata
6167bcf3bf31ac553056a6c60004f52f33391fcf4a0d67fa12e3b812e2c85541-primary.sqlite.bz2
796b249defdd03d9775a6c23db45a09215af2d88aabeadca123f237127d6a5ef-filelists.xml.gz
bf4a15eb0869179894d9c80cd4326dc4c5a562434f4d282ad26d5617062d16cb-other.xml.gz
c29e1e85aa291940096864c80fedcb7e7f53723913a9551b16c29f4ae39373a3-primary.xml.gz
c8ed67b5858a8eca91f6a760412c04f8f1d7be441e2b0b98722350f2929965c2-other.sqlite.bz2
fbb08a2d958c266757ab4eee4aec65ee15973cb9d13f67d0c16265afe52309b1-filelists.sqlite.bz2
repomd.xml
grep -o 'type="[a-z_]*"' repodata/repomd.xml | sort -u
type="filelists_db"
type="filelists"
type="other_db"
type="other"
type="primary_db"
type="primary"

Six data entries: the three XML indexes plus three SQLite databases. SOW does not generate the SQLite ones — that is a deliberate non-goal, not a gap. Every dnf and yum release still receiving updates falls back to the XML indexes when *_db entries are absent.

Take it over

sow create /srv/yum
created /srv/yum: rpm=3 deb=0 signed=0 removed=0 marker=false noop=false recovered=false

removed=0 is the guarantee: no package was touched. signed=0 because no --sign-with was given, so no RPM was re-signed either.

What is on disk now

ls repodata
6167bcf3bf31ac553056a6c60004f52f33391fcf4a0d67fa12e3b812e2c85541-primary.sqlite.bz2
796b249defdd03d9775a6c23db45a09215af2d88aabeadca123f237127d6a5ef-filelists.xml.gz
bf4a15eb0869179894d9c80cd4326dc4c5a562434f4d282ad26d5617062d16cb-other.xml.gz
c29e1e85aa291940096864c80fedcb7e7f53723913a9551b16c29f4ae39373a3-primary.xml.gz
c8ed67b5858a8eca91f6a760412c04f8f1d7be441e2b0b98722350f2929965c2-other.sqlite.bz2
c92f116ebd1c410ed2433551357c5bd66153e4bdc27f1668ac84cb892fbc22b1-other.xml.gz
f7d8b4a3b21af9298a9888aede0de034690ff5bddc2a1aa52f858345b8e4897a-primary.xml.gz
fbb08a2d958c266757ab4eee4aec65ee15973cb9d13f67d0c16265afe52309b1-filelists.sqlite.bz2
fd91d28cf6b949747fc38ef391122dd10cbb3e1e14ec4b295629b004627d39e3-filelists.xml.gz
repomd.xml

Ten files where there were seven. SOW added its three indexes and left all six of createrepo_c’s in place. This is not an oversight — SOW does not delete bytes it did not write. The rule keeps a failed or interrupted run from destroying a working repository, and it is the same rule that makes --pigsty the only mode that removes packages.

The new repomd.xml is a clean replacement that references only the new files:

grep -o 'type="[a-z_]*"' repodata/repomd.xml | sort -u
type="filelists"
type="other"
type="primary"
grep -o '<location href="[^"]*"' repodata/repomd.xml
<location href="repodata/f7d8b4a3b21af9298a9888aede0de034690ff5bddc2a1aa52f858345b8e4897a-primary.xml.gz"
<location href="repodata/fd91d28cf6b949747fc38ef391122dd10cbb3e1e14ec4b295629b004627d39e3-filelists.xml.gz"
<location href="repodata/c92f116ebd1c410ed2433551357c5bd66153e4bdc27f1668ac84cb892fbc22b1-other.xml.gz"

The repository is already correct and serving. The stale files are dead weight — nothing points at them, and a client that never saw the old repomd.xml will never ask for them.

Clean up the leftovers

This is the one manual step in Path A. The safe rule is: delete every file in repodata/ that the current repomd.xml does not reference.

cat > prune-legacy-repodata.sh <<'SH'
#!/bin/sh
# Delete every repodata file the current repomd.xml no longer references.
set -eu
cd "${1:-.}/repodata"
grep -o 'href="repodata/[^"]*"' repomd.xml | sed 's|.*repodata/||; s|"$||' > .keep
printf 'repomd.xml\n' >> .keep
for f in *; do
  [ "$f" = ".keep" ] && continue
  grep -qxF "$f" .keep || { echo "removing $f"; rm -f "$f"; }
done
rm -f .keep
SH
chmod +x prune-legacy-repodata.sh
sh prune-legacy-repodata.sh /srv/yum
removing 6167bcf3bf31ac553056a6c60004f52f33391fcf4a0d67fa12e3b812e2c85541-primary.sqlite.bz2
removing 796b249defdd03d9775a6c23db45a09215af2d88aabeadca123f237127d6a5ef-filelists.xml.gz
removing bf4a15eb0869179894d9c80cd4326dc4c5a562434f4d282ad26d5617062d16cb-other.xml.gz
removing c29e1e85aa291940096864c80fedcb7e7f53723913a9551b16c29f4ae39373a3-primary.xml.gz
removing c8ed67b5858a8eca91f6a760412c04f8f1d7be441e2b0b98722350f2929965c2-other.sqlite.bz2
removing fbb08a2d958c266757ab4eee4aec65ee15973cb9d13f67d0c16265afe52309b1-filelists.sqlite.bz2
ls /srv/yum/repodata
c92f116ebd1c410ed2433551357c5bd66153e4bdc27f1668ac84cb892fbc22b1-other.xml.gz
f7d8b4a3b21af9298a9888aede0de034690ff5bddc2a1aa52f858345b8e4897a-primary.xml.gz
fd91d28cf6b949747fc38ef391122dd10cbb3e1e14ec4b295629b004627d39e3-filelists.xml.gz
repomd.xml

Run this once, right after the first sow create. Every subsequent run is self-cleaning: SOW knows which files it wrote and replaces them.

Confirm it is stable

sow create /srv/yum
created /srv/yum: rpm=3 deb=0 signed=0 removed=0 marker=false noop=true recovered=false

noop=true means nothing changed. Output is deterministic — the same packages produce byte-identical metadata, with a fixed <revision> and zeroed timestamps — so re-running is free and diffable. A cron job that used to invoke createrepo_c becomes:

sow create /srv/yum

Flat DEB directories

The same command handles dpkg-scanpackages output, and a directory containing both formats in one pass:

sow create /srv/mixed

Packages and Packages.gz are rewritten, gzip content matching the plain file exactly. The fields match dpkg-scanpackages with two differences: no MD5sum or SHA1 (SHA-256 is what modern clients verify), and fields the package does not declare are omitted rather than emitted empty.

What Plain mode does not carry over

modulemd and module metadata, SQLite repodata, zchunk, comps/groups, and SRPM source indexes are non-goals — they are not planned and not partially implemented. If your repository publishes AppStream modules or comps groups, Plain mode cannot replace createrepo_c for it today.

Path B: reprepro, to a workspace

reprepro maintains a Berkeley DB alongside the tree. SOW cannot read that database, and there is no import command — but you do not need one. The packages in pool/ are the whole state that matters, and sow add re-derives everything else from the package headers.

The starting state

cd /srv/apt && find pool dists -type f | sort
dists/trixie/main/binary-amd64/Packages
dists/trixie/main/binary-amd64/Packages.gz
dists/trixie/main/binary-amd64/Release
dists/trixie/main/binary-arm64/Packages
dists/trixie/main/binary-arm64/Packages.gz
dists/trixie/main/binary-arm64/Release
dists/trixie/Release
pool/main/a/agentsview/agentsview_0.37.5-1_amd64.deb
pool/main/a/agentsview/agentsview_0.37.5-1_arm64.deb
pool/main/b/blackbox-exporter/blackbox-exporter_0.28.0_amd64.deb
pool/main/b/blackbox-exporter/blackbox-exporter_0.28.0_arm64.deb
pool/main/c/caddy/caddy_2.11.4-1_amd64.deb
pool/main/c/caddy/caddy_2.11.4-1_arm64.deb
pool/main/p/postgresql-18/libpq5_18.4-1.pgdg24.04+1_arm64.deb
pool/main/p/postgresql-common/postgresql-client-common_291.pgdg24.04+1_all.deb

Build the workspace and import

mkdir -p ~/repo && cd ~/repo
sow init .
sow repo new archive
sow dist new trixie --format deb -r archive
initialized /home/you/repo: config_created=true repositories_initialized=0 dists_initialized=0
created archive: path=/home/you/repo/archive protected=false dists=0 generation=0 status=clean packages=0 memberships=0
created trixie: format=deb architectures=x86_64,aarch64 members=0/0 generation=1 dirty=false

Name the Dist after the codename your clients already write in their sources line, so nothing changes on their side.

Then point sow add at the old pool with -R to recurse:

sow add /srv/apt/pool -R -d trixie
add repository=archive operation=2553339663345297053 accepted=8 failed=0 memberships=+8/-0 revision=2 generation=2 dirty=false
item input="/srv/apt/pool/main/a/agentsview/agentsview_0.37.5-1_amd64.deb" status=accepted format=deb coordinate="agentsview=0.37.5-1:amd64" sha256:9f489369bbff02cde4b09397b91bbf367429d8e8cd9d97fc75ba5ea79bb9225a dists=trixie:accepted
item input="/srv/apt/pool/main/a/agentsview/agentsview_0.37.5-1_arm64.deb" status=accepted format=deb coordinate="agentsview=0.37.5-1:arm64" sha256:164fbda74eb82cedacc42902387aa0552c72286dc9e8daa964f2f09e356b3324 dists=trixie:accepted
item input="/srv/apt/pool/main/b/blackbox-exporter/blackbox-exporter_0.28.0_amd64.deb" status=accepted format=deb coordinate="blackbox-exporter=0.28.0:amd64" sha256:1ca6db58a2ca839d1bc1e0f843e971c049f664388c111af5481015baeb9bb120 dists=trixie:accepted
item input="/srv/apt/pool/main/b/blackbox-exporter/blackbox-exporter_0.28.0_arm64.deb" status=accepted format=deb coordinate="blackbox-exporter=0.28.0:arm64" sha256:dd3d06c3b32017b47b721e02b24954dab6179399c3cecc2ce7c5c9a27510f3f3 dists=trixie:accepted
item input="/srv/apt/pool/main/c/caddy/caddy_2.11.4-1_amd64.deb" status=accepted format=deb coordinate="caddy=2.11.4-1:amd64" sha256:79de4b2dda79161164b9b437a0f6d4339c1236f806e5750e8e488fa1e0ede679 dists=trixie:accepted
item input="/srv/apt/pool/main/c/caddy/caddy_2.11.4-1_arm64.deb" status=accepted format=deb coordinate="caddy=2.11.4-1:arm64" sha256:3a21855bb702ffaaafa30f2b626808b43e220ea26cf53ed2ea5aabed0f1aa1dc dists=trixie:accepted
item input="/srv/apt/pool/main/p/postgresql-18/libpq5_18.4-1.pgdg24.04+1_arm64.deb" status=accepted format=deb coordinate="libpq5=18.4-1.pgdg24.04+1:arm64" sha256:923e440808f148f7e44a29fe4c036f836911afdfeffa9dd8cb2009918b614a21 dists=trixie:accepted
item input="/srv/apt/pool/main/p/postgresql-common/postgresql-client-common_291.pgdg24.04+1_all.deb" status=accepted format=deb coordinate="postgresql-client-common=291.pgdg24.04+1:all" sha256:8cae086c805e44272004d111f9f1177789dc14f0bcd07fd901471915a4eed001 dists=trixie:accepted

The old tree is untouched — add reads its inputs and copies them. Keep it until you have verified the new one.

Compare the pools

cd ~/repo/archive && find pool -name "*.deb" | sort
pool/a/agentsview/agentsview_0.37.5-1_amd64.deb
pool/a/agentsview/agentsview_0.37.5-1_arm64.deb
pool/b/blackbox-exporter/blackbox-exporter_0.28.0_amd64.deb
pool/b/blackbox-exporter/blackbox-exporter_0.28.0_arm64.deb
pool/c/caddy/caddy_2.11.4-1_amd64.deb
pool/c/caddy/caddy_2.11.4-1_arm64.deb
pool/p/postgresql-18/libpq5_18.4-1.pgdg24.04+1_arm64.deb
pool/p/postgresql-common/postgresql-client-common_291.pgdg24.04+1_all.deb

Every path is identical to reprepro’s except for one missing segment: reprepro writes pool/main/p/postgresql-18/…, SOW writes pool/p/postgresql-18/…. Both derive the grouping the same way — Debian prefix rule, then source package name — and both put libpq5 under postgresql-18 because that is its Source: field.

SOW has no component level in the pool. The component is fixed at main, so a directory named after it carries no information. The consequence for migration is concrete: Filename values change, which means clients must fetch the new indexes rather than reusing cached ones. Since a new Release is published with new hashes anyway, this happens on the next apt update and requires nothing from you — but it does mean you cannot serve the new indexes over the old pool directory, or vice versa.

Verify before cutting over

sow check
repository=archive status=clean ready_to_copy=true revision=2 generation=2
config	ok=true	checked=3
state	ok=true	checked=1
public-modes	ok=true	checked=41
package-bytes	ok=true	checked=8
desired-membership	ok=true	checked=8
index	ok=true	checked=1
signature	ok=true	checked=1
generation-manifest	ok=true	checked=2

Serve the new tree on a temporary URL, point one client at it, run apt update and install something. Then swap the document root and delete the old tree — not before.

Index differences

reprepro’s Release publishes four digests and per-architecture Release stubs:

Codename: trixie
Date: Tue, 04 Aug 2026 04:33:33 UTC
Architectures: amd64 arm64
Components: main
Description: legacy reprepro archive
MD5Sum:
 a4b9d08caee42b0a2764d4d0ab58c914 3670 main/binary-amd64/Packages
 1fab9172bc826e3054cf6661138732b4 10240 main/binary-amd64/Packages.gz
 b2424f6aef8e120796d78fbabf067a86 73 main/binary-amd64/Release

SOW’s publishes SHA-256 only, and adds Acquire-By-Hash: yes — see Build an APT Repository for the full file. It emits no main/binary-*/Release stubs; APT does not need them, and reprepro’s contain three lines of information already present in the Dist Release.

Per-package, the difference is the same story:

# reprepro
Package: libpq5
Source: postgresql-18
Filename: pool/main/p/postgresql-18/libpq5_18.4-1.pgdg24.04+1_arm64.deb
Size: 248592
SHA256: 923e440808f148f7e44a29fe4c036f836911afdfeffa9dd8cb2009918b614a21
SHA1: bbbbcd35976ba44fdf423553b59cc3679c4f2183
MD5sum: 3f234b897f88f495314768956a73a055

# sow
Package: libpq5
Source: postgresql-18
Filename: pool/p/postgresql-18/libpq5_18.4-1.pgdg24.04+1_arm64.deb
Size: 248592
SHA256: 923e440808f148f7e44a29fe4c036f836911afdfeffa9dd8cb2009918b614a21

Same size, same SHA-256, different Filename, no weak digests.

Habits that change

repreproSOW
reprepro includedeb trixie foo.debsow add foo.deb -d trixie
reprepro remove trixie foosow rm foo -d trixie
reprepro list trixiesow ls -d trixie
reprepro checksow check
Limit: in conf/distributionslimit: per Dist in sow.yml
FilterList / FilterFormulaexclude: rules in sow.yml
SignWith: in conf/distributionssigning.deb.metadata.key in sow.yml
--export=never then reprepro export--skip then sow build
reprepro _listchecksumssow changes BASE

reprepro’s database has no counterpart you need to manage: SOW’s SQLite state is derived from the packages and the configuration, and sow check proves it still matches the tree.

Full comparison

Measured against createrepo_c 0.20.1 and reprepro on identical package sets.

SOWcreaterepo_creprepro
RPM metadataprimary/filelists/other, semantically equivalentbaseline
SQLite repodatanot generated (non-goal)generated by default
DEB Packages fieldsequivalent, SHA-256 onlybaseline (MD5 + SHA1 + SHA256)
by-hash indexessupported (Acquire-By-Hash: yes)not supported
Pool layoutpool/<c>/<source>/pool/main/<c>/<source>/
Per-arch Release stubsnot generated (APT does not need them)generated
PlatformsLinux and macOS, single static binaryLinux in practiceLinux
Transactionsjournal with forward-complete and rollbacknonedatabase can be corrupted
Auditoperation ledger plus JSONL exportnonelimited logging
DependenciesnoneC libraries: libxml2, libcurl, sqlite, …Berkeley DB, gpgme, libarchive

RPM metadata was compared field by field on 9 synthetic and 87 real production packages: name, arch, EVR, checksums, sizes, provides, requires flags, files, changelog, and header ranges all match. One difference showed up: where an RPM header lists /bin/sh twice in both a pre and a non-pre context, SOW keeps one entry.

What does not migrate

These are non-goals, not roadmap items:

  • modulemd / AppStream module metadata, and repo2module / modifyrepo_c workflows
  • SQLite repodata and zchunk
  • SRPM and DSC source indexes
  • remote publishing, CDN or object-storage endpoints
  • multi-host or multi-writer operation
  • snapshots, freezes, and channels
  • a web UI or any long-running service
  • building packages

SOW builds and manages repositories on a local POSIX filesystem, with one writer at a time. If a requirement above is load-bearing for you, keep the tool that provides it.

Where to go next

Last modified: 2026-08-08: init commit (fe725aa)