Skip to content

Split View: 폐쇄망 반입을 위해 밖에서 받기 — dnf download, reposync, yumdownloader는 서로 다른 문제를 풉니다

✨ Learn with Quiz
|

폐쇄망 반입을 위해 밖에서 받기 — dnf download, reposync, yumdownloader는 서로 다른 문제를 풉니다

들어가며 — 세 도구는 각각 다른 문제를 풉니다

1편에서 폐쇄망 설치는 "계산할 수 있는 쪽에서 계산을 끝내는 문제"라고 정리했습니다. 이번 편은 그 계산을 실제로 끝내는 방법입니다.

도구가 셋 있고 이름이 비슷해서 자주 혼동되지만, 셋은 서로 다른 질문에 답합니다.

  • dnf download — "이 패키지와 그게 필요로 하는 것들을 파일로 주세요"
  • dnf reposync — "이 저장소 전체를 로컬에 복제해 주세요"
  • yumdownloader — RHEL 7 시절부터 쓰던 이름. RHEL 8 이후에는 호환 패키지로 제공됩니다

무엇을 고를지는 규모가 아니라 앞으로 무엇을 깔지 알고 있느냐로 갈립니다.

dnf download — 필요한 것만 골라 받기

가장 단순한 형태부터 보겠습니다.

# 패키지 하나만 현재 디렉터리에 내려받는다 (의존성은 따라오지 않음)
dnf download httpd

# 의존성까지 함께 해결해서 받는다
dnf download --resolve httpd

# 이미 설치된 것도 건너뛰지 않고 전부 받는다
dnf download --resolve --alldeps --destdir ./bundle httpd

공식 dnf-plugins-core 문서는 두 옵션을 이렇게 정의합니다. --resolve는 "Resolves dependencies of specified packages and downloads missing dependencies in the system", --alldeps는 "When used with --resolve, download all dependencies (do not skip already installed ones)"입니다.

두 문장의 차이를 놓치면 안 됩니다. --resolve만 쓰면 이 장비에 없는 것만 받습니다. 폐쇄망에 들고 갈 번들을 만드는 목적이라면 이건 거의 항상 틀린 결과입니다. 받는 장비와 설치할 장비가 다른 시스템이기 때문입니다.

--destdir는 dnf 전역 옵션이고 "Redirect downloaded packages to provided directory"로 문서화되어 있습니다. download 플러그인 문서에는 --downloaddir도 나오는데, 예제에서는 --destdir를 씁니다. 둘 다 같은 전역 옵션의 표기입니다.

--alldeps만으로는 부족합니다

--alldeps를 붙였는데도 폐쇄망에서 막히는 경우가 있습니다. 이유는 이렇습니다.

--alldeps는 "이미 설치된 것을 건너뛰지 말라"고 지시하지만, 의존성 해결 자체는 여전히 이 시스템의 상태를 전제로 이루어집니다. 연결망 장비가 RHEL 9.4이고 설치 대상이 RHEL 9.2라면 해결 결과가 달라질 수 있고, 연결망 장비에 켜져 있는 저장소가 폐쇄망 쪽과 다르면 또 달라집니다.

확실한 방법은 아무것도 설치되지 않은 빈 루트에서 계산하는 것입니다. dnf 공식 문서는 --installroot를 이렇게 설명합니다. "Think of this like doing chroot <root> dnf, except using --installroot allows dnf to work before the chroot is created. It requires absolute path."

그리고 바로 다음 문단에 반드시 읽어야 할 경고가 있습니다.

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).

빈 루트에는 rpmdb가 없고, 릴리스 버전은 rpmdb에서 유도되므로, 명시하지 않으면 값이 비어서 트랜잭션이 실패한다는 뜻입니다. 실제 명령은 이렇게 됩니다.

# 반입용 빈 루트를 만든다
sudo mkdir -p /var/tmp/airgap-root

# 빈 루트 기준으로 의존성을 전부 풀어서 받는다
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

--setopt=reposdir=를 붙인 이유도 문서에 있습니다. "configuration file and reposdir are searched inside the installroot first. If they are not present, they are taken from the host system." 빈 루트에는 저장소 설정이 없으므로 호스트 것을 쓰게 되는데, 문서는 명령줄로 경로를 지정하면 "this path is always relative to the host with no exceptions"라고 못박습니다. 즉 명시적으로 적어 두는 편이 예측 가능합니다.

모듈러 시스템, 즉 RHEL 8이나 모듈을 쓰는 RHEL 9라면 한 줄이 더 필요합니다. 문서는 --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)"라고 경고합니다. 모듈 콘텐츠가 조용히 빠진다는 뜻이라 특히 위험합니다. 이 부분은 5편에서 이어서 다룹니다.

dnf reposync — 저장소를 통째로 복제하기

앞으로 무엇을 설치할지 모른다면 저장소 자체를 옮기는 편이 낫습니다. 공식 문서의 설명은 "reposync makes local copies of remote repositories. Packages that are already present in the local directory are not downloaded again"입니다. 두 번째 문장 덕분에 재실행이 증분으로 동작합니다.

# BaseOS 저장소를 메타데이터까지 함께 로컬로 복제한다
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

옵션별로 문서가 말하는 바는 이렇습니다.

옵션문서상 동작
-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저장소 메타데이터를 함께 받아 그대로 저장소로 쓸 수 있게 합니다
-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."

--gpgcheck의 종료 코드 동작은 자동화에서 유용합니다. 서명 검증에 실패한 패키지가 하나라도 제거되면 종료 코드가 1이므로, 반입 스크립트에서 그대로 게이트로 쓸 수 있습니다.

--newest-only--download-metadata를 같이 쓸 때는 문서의 단서를 기억해야 합니다. 최신 패키지만 내려받지만 메타데이터에는 이전 패키지 정보가 남습니다. 안쪽에서 특정 옛 버전을 요청하면 메타데이터에는 있는데 파일이 없는 상태가 됩니다. 특정 버전 고정이 필요한 환경이라면 --newest-only를 빼는 편이 안전합니다.

반입 전에 복제본이 제대로 저장소로 인식되는지는 문서의 예제 형태로 확인할 수 있습니다.

# 복제한 디렉터리를 임시 저장소로 붙여 목록이 나오는지 본다
dnf --repofrompath=synced,/var/tmp/airgap-bundle/repos/rhel-9-for-x86_64-baseos-rpms \
    --repoid=synced list --available | head

--repofrompath는 "Specify a repository to add to the repositories for this query", --repoid는 "Enable just specific repositories by an id or a glob"로 문서화된 전역 옵션입니다.

yumdownloader는 언제 쓰나

yumdownloader는 RHEL 7의 yum-utils 시절 이름입니다. RHEL 8 이후에도 yum-utils 패키지를 설치하면 같은 이름의 명령을 쓸 수 있고, 기존 운영 스크립트를 그대로 유지해야 할 때 의미가 있습니다.

다만 정직하게 적자면, yumdownloader는 dnf-plugins-core 공식 문서에 별도 항목으로 존재하지 않습니다. 문서화된 플러그인 목록에는 download, reposync, repoclosure 등이 있고 yumdownloader는 없습니다. 그래서 이 글은 yumdownloader의 개별 옵션을 단정하지 않습니다. 사용 중인 시스템에서 다음처럼 직접 확인하세요.

# 이 명령이 어느 패키지에서 오는지 확인
dnf provides '*/yumdownloader'

# 실제로 지원하는 옵션 확인
man yumdownloader
yumdownloader --help

새로 절차서를 쓴다면 dnf download를 쓰는 편이 낫습니다. 문서가 있고, 옵션 의미가 명확하며, dnf5에도 대응되는 명령이 있습니다.

받은 것이 완전한지 확인하기

여기가 이 글에서 가장 중요한 부분입니다. 반입 매체를 만들기 전에, 이 묶음만으로 의존성이 다 풀리는지 확인해야 합니다. 폐쇄망에 들어간 뒤에 발견하면 다음 심의까지 기다려야 하기 때문입니다.

dnf repoclosure가 정확히 이 일을 합니다. 문서의 정의는 "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"입니다.

# 반입 예정 디렉터리를 저장소로 만들고 (3편에서 자세히 다룹니다)
createrepo_c /var/tmp/airgap-bundle/rpms

# 그 저장소만으로 모든 의존성이 풀리는지 검사한다
dnf repoclosure \
  --repofrompath=bundle,/var/tmp/airgap-bundle/rpms \
  --repo=bundle

출력이 비어 있으면 통과입니다. 해결되지 않은 의존성이 있으면 패키지별로 나열됩니다. 옵션은 --check <repoid>("Specify repo ids to check, can be specified multiple times"), --pkg <pkg-spec>("Check closure for this package only"), --best("Check only the newest packages per arch")가 있습니다.

이 한 단계를 절차에 넣는 것만으로 반입 재시도의 상당수가 사라집니다.

RHEL 10과 dnf5에서 달라지는 것

RHEL 10은 dnf5 기반입니다. download 명령의 옵션 일부가 이름이 바뀌었으므로 절차서를 그대로 옮기면 안 됩니다.

기능dnf4 (RHEL 8 / 9)dnf5 (RHEL 10)
소스 RPM 받기--source--srpm
의존성 해결--resolve--resolve
설치된 것도 포함--alldeps--alldeps
받을 위치--destdir / --downloaddir--destdir
특정 저장소로 제한--repo--from-repo
없는 패키지 건너뛰기문서에 해당 옵션 없음--skip-unavailable

dnf5 문서에는 --allmirrors("To be used together with --url. It prints out space-separated URLs from all available mirrors")처럼 dnf4에 없던 옵션도 있습니다. 어느 쪽이든 실행 전에 사용 중인 버전의 man dnf 또는 dnf download --help로 확인하는 습관이 안전합니다.

서브스크립션 없이 Red Hat 콘텐츠를 재배포하는 것은 계약 위반일 수 있으니 조직의 라이선스 조건을 먼저 확인하세요. 이 글의 절차는 엔타이틀먼트가 붙은 연결망 장비에서 실행하는 것을 전제로 합니다.

마치며 — 빈 루트에서 풀고, 반입 전에 닫힘을 확인합니다

이번 편의 결론은 두 줄입니다.

받는 장비의 상태가 결과를 오염시킵니다. --installroot로 빈 루트를 만들고 --releasever를 명시하면 그 오염이 사라집니다. 모듈을 쓰는 시스템이라면 module_platform_id까지 지정해야 모듈 콘텐츠가 빠지지 않습니다.

반입 전에 dnf repoclosure를 돌립니다. 이 검사는 몇 초 걸리고, 통과하지 못한 번들을 들고 들어가면 며칠이 걸립니다.

명령과 옵션은 2026-08-15에 공식 문서에서 확인했습니다. RHEL 버전에 따라 다르므로 사용 중인 버전의 문서로 다시 확인하세요.

직접 해보기

이전 / 다음 편

참고 자료

Downloading on the Outside for an Air-Gapped Transfer — dnf download, reposync, and yumdownloader Solve Different Problems

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.

OptionWhat 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-metadataDownloads 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.

Capabilitydnf4 (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 packagesno 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

References