필사 모드: Downloading on the Outside for an Air-Gapped Transfer — dnf download, reposync, and yumdownloader Solve Different Problems
English- Opening — the three tools solve three different problems
- dnf download — picking out only what you need
- --alldeps alone is not enough
- dnf reposync — cloning a repository wholesale
- When do you use yumdownloader
- Checking that what you downloaded is complete
- What changes on RHEL 10 and dnf5
- Closing — resolve in an empty root, verify closure before you carry it in
- Try it yourself
- Previous / next in the series
- References
Opening — the three tools solve three different problems
Post 1 framed air-gapped installation as "a problem of finishing the computation on the side that is able to compute". This post is about actually finishing that computation.
There are three tools, their names look alike, and they get mixed up constantly. But they answer three different questions.
dnf download— "give me this package and the things it needs, as files"dnf reposync— "clone this entire repository to a local directory"yumdownloader— the name people have used since RHEL 7. On RHEL 8 and later it ships as a compatibility package
Which one you reach for is not decided by scale. It is decided by whether you already know what you are going to install.
dnf download — picking out only what you need
Start with the simplest form.
# Download a single package into the current directory (dependencies do not come along)
dnf download httpd
# Resolve dependencies and download them too
dnf download --resolve httpd
# Download everything, without skipping what is already installed
dnf download --resolve --alldeps --destdir ./bundle httpd
The official dnf-plugins-core documentation defines the two options like this. --resolve is "Resolves dependencies of specified packages and downloads missing dependencies in the system", and --alldeps is "When used with --resolve, download all dependencies (do not skip already installed ones)".
Do not miss the difference between those two sentences. With --resolve alone you get only the things this machine does not already have. If the goal is a bundle to carry into an air-gapped network, that is almost always the wrong result, because the machine you download on and the machine you install on are different systems.
--destdir is a dnf global option, documented as "Redirect downloaded packages to provided directory". The download plugin documentation also mentions --downloaddir, while its examples use --destdir. Both are spellings of the same global option.
--alldeps alone is not enough
Sometimes you add --alldeps and still get blocked inside the air-gapped network. Here is why.
--alldeps instructs dnf not to skip what is already installed, but the dependency resolution itself is still performed on the assumption of this system's state. If the connected machine is RHEL 9.4 and the install target is RHEL 9.2, the resolution can come out differently; if the repositories enabled on the connected machine differ from the ones on the air-gapped side, it comes out differently again.
The reliable approach is to compute against an empty root where nothing is installed. The official dnf documentation describes --installroot like this: "Think of this like doing chroot <root> dnf, except using --installroot allows dnf to work before the chroot is created. It requires absolute path."
And the very next paragraph carries a warning you have to read.
Note: You may also want to use the command-line option --releasever=<release>
when creating the installroot, otherwise the $releasever value is taken from
the rpmdb within the installroot (and thus it is empty at the time of creation
and the transaction will fail).
An empty root has no rpmdb, and the release version is derived from the rpmdb, so if you do not state it explicitly the value comes out empty and the transaction fails. The actual command looks like this.
# Create an empty root for the transfer
sudo mkdir -p /var/tmp/airgap-root
# Resolve every dependency against the empty root and download it all
sudo dnf download \
--installroot=/var/tmp/airgap-root \
--releasever=9.4 \
--setopt=reposdir=/etc/yum.repos.d \
--resolve --alldeps \
--destdir=/var/tmp/airgap-bundle/rpms \
httpd mod_ssl
The reason for adding --setopt=reposdir= is in the documentation too: "configuration file and reposdir are searched inside the installroot first. If they are not present, they are taken from the host system." An empty root has no repository configuration, so the host's is used — and the documentation is blunt that when you give the path on the command line, "this path is always relative to the host with no exceptions". In other words, writing it out explicitly is the predictable option.
On a modular system — RHEL 8, or a RHEL 9 that uses modules — you need one more line. The documentation warns that if you do not set --setopt=module_platform_id=, "the module_platform_id value will be taken from the /etc/os-release file within the installroot (and thus it will be empty at the time of creation, the modular dependency could be unsatisfied and modules content could be excluded)". That means module content silently drops out, which makes it especially dangerous. Post 5 picks this up in detail.
dnf reposync — cloning a repository wholesale
If you do not know what you will be installing later, moving the repository itself is the better bet. The official documentation puts it this way: "reposync makes local copies of remote repositories. Packages that are already present in the local directory are not downloaded again". Thanks to that second sentence, re-running it behaves incrementally.
# Clone the BaseOS repository locally, metadata included
sudo dnf reposync \
--repoid=rhel-9-for-x86_64-baseos-rpms \
--download-path=/var/tmp/airgap-bundle/repos \
--download-metadata \
--gpgcheck \
--newest-only \
--arch=x86_64 --arch=noarch \
--remote-time
Here is what the documentation says about each option.
| Option | What the documentation says |
|---|---|
-p, --download-path | "Root path under which the downloaded repositories are stored, relative to the current working directory. Defaults to the current working directory." |
--download-metadata | Downloads the repository metadata as well, so the copy can be used as a repository as-is |
-g, --gpgcheck | "Remove packages that fail GPG signature checking after downloading. Exit code is 1 if at least one package was removed." |
-n, --newest-only | "Download only newest packages per-repo." |
-a, --arch | "Download only packages of given architectures (default is all architectures). Can be used multiple times." |
--delete | "Delete local packages no longer present in repository." |
--norepopath | "Don't add the reponame to the download path. Can only be used when syncing a single repository." |
--remote-time | "Try to set the timestamps of the downloaded files to those on the remote side." |
-u, --urls | "Just print urls of what would be downloaded, don't download." |
--source | "Download only source packages." |
The exit-code behaviour of --gpgcheck is useful in automation. If even one package fails signature verification and gets removed, the exit code is 1, so you can wire it straight into a transfer script as a gate.
When you combine --newest-only with --download-metadata, remember the caveat in the documentation. You download only the newest packages, but the metadata still carries entries for the older ones. Ask for a specific old version from the inside and you land in a state where the metadata has it but the file does not. In an environment that needs version pinning, leaving --newest-only off is the safer choice.
Before the transfer, you can confirm that the clone is recognised as a proper repository using the form shown in the documentation examples.
# Attach the cloned directory as a temporary repository and see whether a listing comes back
dnf --repofrompath=synced,/var/tmp/airgap-bundle/repos/rhel-9-for-x86_64-baseos-rpms \
--repoid=synced list --available | head
--repofrompath is documented as "Specify a repository to add to the repositories for this query" and --repoid as "Enable just specific repositories by an id or a glob"; both are global options.
When do you use yumdownloader
yumdownloader is the name from the yum-utils era on RHEL 7. On RHEL 8 and later you can still install the yum-utils package and get a command by that name, which matters when you have to keep existing operational scripts running unchanged.
To be honest about it, though: yumdownloader does not exist as its own entry in the official dnf-plugins-core documentation. The documented plugin list includes download, reposync, repoclosure, and others; yumdownloader is not among them. So this post will not make claims about individual yumdownloader options. Check directly on the system you are running.
# Find out which package this command comes from
dnf provides '*/yumdownloader'
# Check the options it actually supports
man yumdownloader
yumdownloader --help
If you are writing a new procedure, use dnf download. It is documented, its option semantics are clear, and there is a corresponding command in dnf5.
Checking that what you downloaded is complete
This is the most important part of the post. Before you build the transfer media, confirm that this bundle alone resolves every dependency. If you discover the gap after you are inside the air-gapped network, you wait for the next review board.
dnf repoclosure does exactly this job. The documentation defines it as "repoclosure is a program that reads package metadata from one or more repositories, checks all dependencies, and displays a list of packages with unresolved dependencies".
# Turn the directory you plan to transfer into a repository (post 3 covers this in detail)
createrepo_c /var/tmp/airgap-bundle/rpms
# Check whether every dependency resolves using that repository alone
dnf repoclosure \
--repofrompath=bundle,/var/tmp/airgap-bundle/rpms \
--repo=bundle
Empty output means it passed. If there are unresolved dependencies, they are listed per package. The options are --check <repoid> ("Specify repo ids to check, can be specified multiple times"), --pkg <pkg-spec> ("Check closure for this package only"), and --best ("Check only the newest packages per arch").
Putting this single step into the procedure eliminates a large share of transfer retries.
What changes on RHEL 10 and dnf5
RHEL 10 is dnf5-based. Some of the download command options were renamed, so you cannot port a procedure across verbatim.
| Capability | dnf4 (RHEL 8 / 9) | dnf5 (RHEL 10) |
|---|---|---|
| Download source RPMs | --source | --srpm |
| Resolve dependencies | --resolve | --resolve |
| Include already installed | --alldeps | --alldeps |
| Download location | --destdir / --downloaddir | --destdir |
| Restrict to a specific repository | --repo | --from-repo |
| Skip unavailable packages | no such option in the documentation | --skip-unavailable |
The dnf5 documentation also has options dnf4 never had, such as --allmirrors ("To be used together with --url. It prints out space-separated URLs from all available mirrors"). Either way, the safe habit is to check man dnf or dnf download --help for the version you are running before you execute anything.
Redistributing Red Hat content without a subscription may breach your agreement, so check your organisation's licence terms first. The procedures in this post assume you are running them on a connected machine with an entitlement attached.
Closing — resolve in an empty root, verify closure before you carry it in
This post comes down to two lines.
The state of the machine you download on contaminates the result. Create an empty root with --installroot and state --releasever explicitly, and that contamination goes away. On a system that uses modules you also have to set module_platform_id, or module content drops out.
Run dnf repoclosure before the transfer. The check takes seconds; carrying in a bundle that would not have passed it takes days.
Commands and options were verified against the official documentation on 2026-08-15. Behaviour differs by RHEL version, so re-check against the documentation for the version you are running.
Try it yourself
- Linux Terminal — practise assembling long dnf commands with line continuations
- Linux Command Quiz — review package management options
- Hash Generator — build checksums for the rpms you downloaded and rehearse the post 4 manifest
Previous / next in the series
- Previous: Why air-gapped installation is genuinely hard
- Next: Building a local repository — createrepo_c, repodata, and GPG keys
References
현재 단락 (1/100)
Post 1 framed air-gapped installation as "a problem of finishing the computation on the side that is...