Air-Gapped GPU Driver Installation
Why Air-Gapped Installation Is Hard
In one line
The real reason air-gapped installation is hard is that the side that computes the dependencies and the side that installs are separated. The information needed for the computation is split between the two sides.
Why this was needed
The request always comes like this. "Just bring me the one driver file."
When you put it in after downloading, you get an error like this.
The following packages have unmet dependencies:
nvidia-driver-550 : Depends: nvidia-kernel-dkms-550 but it is not installable
You go back out and download that, and it says something else is missing. After three or four round trips with the USB drive, half a day has gone by, and import review happens only once a day.
How it works
Separating computation from installation
A dependency resolver (depsolver) has two inputs.
- The entire repository metadata — what each package provides and what it requires
- The current state of the target system — what is already installed
The air-gapped side lacks input 1, and the outside lacks input 2. This separation is the essence of the problem.
There are two ways out.
- Move the resolved result — compute the list of needed packages outside and carry in only those files
- Move the whole repository — replicate even the metadata and have the inside compute again
If you start small you take the first, but most organizations end up at the second. That is because, once imports become repeated, the management cost of the first approach grows sharply.
Things that silently drop out
What is most dangerous in approach 1 is packages that drop out silently, without an error.
First, what is already installed disappears from the result. Dependency computation presupposes "the current state of the system doing the computation". Libraries already installed on the connected machine are treated as "not needed" and dropped from the list, and that fact only comes to light inside the air-gapped network.
Second, the result changes if the version differs. If the connected machine is on 24.04 and the install target is on 22.04, the resolved result can differ.
Third, you forget the architecture. If you omit noarch/all packages from the arch specification, Python modules and configuration packages drop out wholesale.
Fourth, the handling of weak dependencies (Recommends). There are cases where they were excluded at computation time but are included by default at install time and so are demanded. The reverse happens too.
So the standard approach is to compute from an empty root. On the apt side, you first extract the list to download with --print-uris and review it, then download with --download-only and check whether that bundle closes within itself (closure).
Integrity — two different questions
There are two questions to ask about a file that has passed through the media.
| Question | What answers it | Tool |
|---|---|---|
| Was it damaged in transit? | Checksum | sha256sum -c |
| Did this really come from that side? | Signature | GPG verification, dpkg-sig, rpm --checksig |
A checksum contained only on the same media changes along with it if the whole media is swapped. So a checksum alone cannot guarantee the origin. The real guarantee comes from the signature.
And you must also check for files that are on the media but not in the manifest. A missing file soon shows up as an installation failure, but an added file passes without anyone knowing. From the point of view of import review, this is the bigger problem.
The import record
The bundle must carry not only files but also a record.
| Item | Example | Why |
|---|---|---|
| Bundle ID | gpu-airgap-2026-08-20 |
The key linking servers and bundles |
| Snapshot date | 2026-08-20 |
Which point in time the content is from |
| Target versions | ubuntu 24.04 / driver 550.90.07 |
The premise of reproduction |
| Creation command | apt-get install --download-only ... |
The core of reproduction |
| Media hash | SHA-256 of the whole archive | Integrity per media |
| Import date and person in charge | — | An audit requirement |
You must be able to answer the question, six months later, "which point in time's content was this server installed from?"
How to make the list before import
The most common failure in air-gapped imports is not integrity but omission. Once something goes in it is hard to bring it out again, so the way you make the list itself becomes the core of the procedure.
Do not count dependencies by hand. Ask the package manager "what is needed to install this" and put its answer in as it is.
# 데비안 계열: 의존성까지 통째로 내려받는다
apt-get install --download-only --reinstall -o Dir::Cache::archives=./pkgs <패키지>
# RHEL 계열
dnf download --resolve --alldeps --destdir ./pkgs <패키지>
# 파이썬: 해시까지 고정해서
pip download -r requirements.txt -d ./wheels --require-hashes
Put container images in by digest. If you put them in by tag, what you downloaded outside and what you use inside can differ.
skopeo copy --all docker://registry/app@sha256:... dir:./images/app
Without --all, only the architecture of this computer right now is put in. If the import target has a different architecture, you meet "manifest unknown" inside.
For the GPU side, version pairing is especially strict. The combination of driver, container toolkit, CUDA, and framework must fit. If even one is off, it shows up as "cannot find the GPU", and inside an air-gapped network you cannot try downloading a different version. Stand up the same combination once outside and import that list as it is.
Put in the installation order as well. Put in together a script that builds an offline repository, configures the system to point at that repository, and installs in order. Finding the order by hand inside takes time and cannot be reproduced.
Put in a way back too. If you raise the driver and a problem occurs, you have to go back to the previous version, and if the previous version's packages are not inside, you cannot roll back. Import the packages of the currently installed version as well.
What it looks like in the field
The epoch trap. Both Debian and RPM can put an epoch before the version (1:550.90.07-0ubuntu1). The epoch does not appear in the file name. Yet in version comparison the epoch takes the highest priority. If you judge "this is newer" by the file name alone, you can be wrong. That is why the manifest must carry the complete version string.
What to look for in the next check
In the quiz that follows, you first distinguish the two inputs of dependency computation, the roles of checksums and signatures, and the two-way verification of the manifest. After checking those criteria of judgment, from the next module on you actually produce the dependency graph and installation order of a package bundle.