How to Create a Local YUM/DNF Repository on RHEL 9/10 and AlmaLinux 9/10

Updated for 2026: this guide was originally written for RHEL 7 in 2017. RHEL 7 went end-of-life in June 2024, and package management has moved on — yum is now dnf, and the install media ships two repositories instead of one. The recipe below is the modern version, for RHEL 8/9/10 and AlmaLinux 8/9/10 (Rocky works the same way).

What changed since RHEL 7

  • yum → dnf. On EL8 and later, yum is just a symlink to dnf. The commands are near-identical; the config files still live in /etc/yum.repos.d/.
  • Two repos on the media, not one. The ISO/DVD now carries BaseOS (the core OS) and AppStream (applications — httpd, PHP, PostgreSQL, and friends). Configure only BaseOS and installs start failing with “No match for argument”. You need both.
  • RHEL needs a subscription; AlmaLinux doesn’t. A local repo is the classic workaround for air-gapped or unregistered RHEL systems, and the standard way to build an internal mirror for AlmaLinux/Rocky fleets.

What changes on RHEL 10 / AlmaLinux 10 (EL10 coverage added Oct 26, 2026)

On EL10, dnf is DNF5 — but for this guide, almost nothing changes. The ISO still carries BaseOS and AppStream, the .repo files still live in /etc/yum.repos.d/, createrepo_c still builds the metadata, and the httpd and SELinux steps are identical. Two things to know:

  • Mirror builds and reposync. Method 2 uses dnf reposync from dnf-plugins-core. On EL10 (DNF5), the plugin package is dnf5-plugins, and some option names differ (--destdir instead of -p). If your EL10 box has no reposync at all, copy BaseOS and AppStream off the ISO as in Method 2’s note — the rest of the mirror build is unchanged.
  • Never mix major releases in one repo directory. Keep EL9 and EL10 trees separate (e.g. /var/www/html/repo/el9 and /var/www/html/repo/el10) — a directory that mixes them will resolve packages across releases and break installs in confusing ways.

Method 1: local repository from the ISO (single machine)

1. Mount the installation ISO. If you’re on a VM, attach the ISO to the virtual CD drive; the loop-mount works either way:

[root@repo-server ~]# mkdir -p /mnt/iso
[root@repo-server ~]# mount -o loop /dev/cdrom /mnt/iso
[root@repo-server ~]# ls /mnt/iso
BaseOS  AppStream  RPM-GPG-KEY-redhat-release  media.repo  ...

2. Create the repo file. Note there are two stanzas — one per repository on the media:

[root@repo-server ~]# cat /etc/yum.repos.d/local-iso.repo
[local-baseos]
name = Local BaseOS (ISO)
baseurl = file:///mnt/iso/BaseOS
enabled = 1
gpgcheck = 1
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

[local-appstream]
name = Local AppStream (ISO)
baseurl = file:///mnt/iso/AppStream
enabled = 1
gpgcheck = 1
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

On AlmaLinux the key is RPM-GPG-KEY-AlmaLinux in the same directory. Keep gpgcheck = 1 — the old habit of disabling it was never a good one, and the key ships on the media itself.

3. Refresh the metadata and confirm both repos are visible:

[root@repo-server ~]# dnf clean all
[root@repo-server ~]# dnf repolist
repo id              repo name
local-appstream      Local AppStream (ISO)
local-baseos         Local BaseOS (ISO)

4. Test it with a package that lives in AppStream — this proves both halves work:

[root@repo-server ~]# dnf install -y httpd
...
Installed:
  httpd-2.4.x-1.el9.x86_64

Complete!

Done. Anything on the media is now installable with no network and no subscription.

Method 2: a central mirror for the whole fleet

The 2017 version promised a central repository server; here’s the modern build. One machine hosts the packages over HTTP, every other machine points at it.

1. On the mirror server, sync the packages down. With an entitled RHEL system (or any AlmaLinux box with network access), dnf reposync (from dnf-plugins-core) does the heavy lifting:

[root@repo-server ~]# dnf install -y dnf-plugins-core createrepo_c httpd
[root@repo-server ~]# mkdir -p /var/www/html/repo/el9
[root@repo-server ~]# dnf reposync --repoid=baseos --repoid=appstream 
    -p /var/www/html/repo/el9 --download-metadata

No upstream access? Copy the BaseOS and AppStream directories off the ISO into the same location instead — for a small fleet that’s often enough.

2. If you assembled the tree by hand, (re)build the metadata with createrepo_c:

[root@repo-server ~]# createrepo_c /var/www/html/repo/el9/BaseOS
[root@repo-server ~]# createrepo_c /var/www/html/repo/el9/AppStream

3. Serve it. Mind SELinux — this is the step everyone forgets:

[root@repo-server ~]# semanage fcontext -a -t httpd_sys_content_t "/var/www/html/repo(/.*)?"
[root@repo-server ~]# restorecon -Rv /var/www/html/repo
[root@repo-server ~]# systemctl enable --now httpd
[root@repo-server ~]# firewall-cmd --add-service=http --permanent && firewall-cmd --reload

4. Point each client at the mirror:

[root@client1 ~]# cat /etc/yum.repos.d/internal-mirror.repo
[internal-baseos]
name = Internal BaseOS
baseurl = http://repo.example.local/repo/el9/BaseOS
enabled = 1
gpgcheck = 1
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

[internal-appstream]
name = Internal AppStream
baseurl = http://repo.example.local/repo/el9/AppStream
enabled = 1
gpgcheck = 1
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

[root@client1 ~]# dnf clean all && dnf repolist

Run reposync from cron on the mirror (weekly is plenty for most shops) and your fleet patches from one controlled, local source.

Troubleshooting

  • “No match for argument: httpd” (or php, postgresql…) — you only configured BaseOS. AppStream carries the application packages; add the second stanza.
  • “Failed to download metadata” / repodata errors — the repodata/ directory is missing or stale. Re-run createrepo_c on that repo directory.
  • GPG key errors — the gpgkey= path doesn’t match your distro (RHEL vs AlmaLinux key names differ), or the key file isn’t where the repo file says it is.
  • 403/404 from the mirror — check SELinux first (ausearch -m AVC -ts recent), then the httpd error log. Nine times out of ten it’s the file context from step 3.
  • Client still hits the internet repos — disable the stock repo files while testing (dnf config-manager --disable baseos appstream or move them aside) so you know the mirror is really doing the work.

The shape of the thing hasn’t changed since the RHEL 7 days — mount media, write a .repo file, clean, install. What changed is the details: dnf, two repos instead of one, and metadata you build with createrepo_c. Get those three right and a local repo is still a 15-minute job.


Running your own server? These guides assume you have a Linux box somewhere — see our guide to choosing a VPS in 2026 for the 7 specs that actually matter before you spin one up.

Scroll to Top