createrepo_c and repodata
In one line
createrepo_c scans an rpm directory and builds repodata/. The repomd.xml inside it is the table of contents that points to the other metadata files.
Why this exists
Merely copying RPM files into a directory does not make it a dnf repository. dnf does not open every package each time; it looks up the versions, dependencies, and file lists in repodata. So if you add packages and do not refresh the metadata, the files on the server and the list the client sees differ. If you know this difference, you can tell cache and permission problems apart from a missed repository creation.
How it works
dnf install -y createrepo_c
createrepo_c /srv/repo/labhub
ls -1 /srv/repo/labhub/repodata/
# repomd.xml
# <checksum>-primary.xml.gz
# <checksum>-filelists.xml.gz
# <checksum>-other.xml.gz
| File | Contents |
|---|---|
repomd.xml |
The table of contents. The location, checksum, and time of each metadata file |
primary |
Name, version, dependencies, summary |
filelists |
The list of all files a package contains |
other |
changelog |
The reason file-based search such as dnf provides '*/nginx.conf' works is filelists.
The checksum prefix before the file names is the result of --unique-md-filenames (the default). It prevents the incident in which an intermediate proxy or CDN returns an old copy. The file name changes with the content, so the cache cannot get confused.
Main options
| Option | Behavior per the documentation |
|---|---|
--update |
"Reuse existing metadata for rpms that have not changed, judged by file size and mtime" |
--workers N |
Number of workers that read rpms |
-g, --groupfile |
Include a comps group file |
-s, --checksum |
The checksum type of repomd and packages. Default sha256 |
--compress-type |
bz2, gz, zck, zstd, xz |
-i, --pkglist |
A file listing the packages to include |
--retain-old-md N |
Number of old repodata to keep |
-x, --excludes |
Exclude patterns |
-d/--database (creating sqlite) is deprecated. If you see this option in an old procedure document, you can delete it.
Tying --pkglist to the manifest is a good practice. If you use the import manifest as the pkglist as it is, it structurally prevents the incident where the list and the actual repository disagree.
Updating
After adding packages, always rebuild the metadata.
createrepo_c --update /srv/repo/labhub
dnf clean expire-cache
dnf repolist
Forgetting --update is the number one cause of the most frequent report on air-gapped networks. The files are visible, but dnf does not know.
The repo file and verification
[labhub-local]
name=LabHub local (snapshot 2026-08-20)
baseurl=file:///srv/repo/labhub
enabled=1
gpgcheck=0
metadata_expire=-1
Three checks that it works.
dnf repolist -v
dnf repoclosure --repo=labhub-local
dnf install --assumeno <패키지>
dnf repoclosure lists packages that have dependencies that cannot be resolved inside the repository. It is the best tool to use as a gate before an import.
createrepo_c /srv/bundle/rpms
dnf repoclosure --repofrompath=bundle,/srv/bundle/rpms --repo=bundle
--repofrompath specifies a repository temporarily without creating a .repo file. It is useful in verification scripts.
Cleaning up old packages
dnf repomanage --old --keep 2 /srv/repo/labhub
dnf repomanage --old --keep 2 /srv/repo/labhub | xargs -r rm -v
createrepo_c --update /srv/repo/labhub
How many you keep is the rollback range. If the import interval is once a month and you keep 3, about 3 months of rollback is possible.
What it looks like in the field
A snapshot date in the repository display name. If you write it like name=... (snapshot 2026-08-20), six months later one dnf repolist -v line can pin down the point in time.
What you will do in the next lab
You collect rpms and build a repository with createrepo_c, write a .repo file, enable only that repository and install, and after adding a package, refresh it with --update.