Air-Gapped Mirrors and a Private CA
Why you stand up one more registry
In one line
npm's answer to air-gapped networks is standing up one more registry. While connected, it pulls from upstream (npmjs) to fill itself, and once disconnected it serves only what it has filled. A project looks at that registry through the single registry line in .npmrc. npm rewrites the addresses baked into the lock file on its own, but if you do not know that default, accidents happen.
Why this was needed
The node_modules of a single front-end repository holds hundreds of packages. Run npm ci in an air-gapped network and it stops at the first package. A common stopgap is to compress the whole node_modules on a connected PC and carry it in, but if the operating system or node version differs, native modules break, and there is no list of what went in. What you need is a repository that holds packages one by one and can reinstall them exactly as the lock file says.
How it works
Pointing to the registry. npm follows configuration in the order command line → environment variables → npmrc files → defaults, and among npmrc files the priority is project (.npmrc) → user (~/.npmrc) → global ($PREFIX/etc/npmrc) → built-in. The default of registry is https://registry.npmjs.org/. If you write registry= in the project .npmrc, everyone who opens that repository sees the same registry.
Three settings of verdaccio. uplinks is the list of upstream registries, and packages sets access, publish, and proxy for each name pattern. According to the documentation, the order of patterns matters and ** is placed last as the catch-all. A pattern whose proxy names an uplink pulls what is missing locally from upstream and keeps it in storage, and a pattern without proxy serves only what is in the local storage. That is what "disconnecting" really means. The default port is 4873.
Translated into Nexus or Artifactory terms, it goes like this. The Nexus documentation divides repositories into three kinds: proxy, which caches a remote, hosted, the source we upload to, and group, which bundles several behind one address. The verdaccio pattern carrying proxy: npmjs is the proxy repository, the pattern that lets you publish internal packages is hosted, and serving the two behind one address plays the group role. Nexus 3 officially requires 8GB of host memory and a default heap of 2703MB, so it does not fit in this lab Pod (2Gi). Learn the principle with a lightweight tool and map it onto the menus of the equipment in the field.
Lock file and addresses. The resolved field of package-lock.json holds the full address of the tarball. A lock file made outside has https://registry.npmjs.org/... baked in. npm's replace-registry-host setting deals with this problem. The default npmjs rewrites only addresses that point to the default registry into the configured registry and downloads from there. never goes to the address exactly as written, and always rewrites any host. It also means the default does not help when you move a lock file with one internal registry address baked in to a different internal registry.
Install commands. npm ci requires a lock file, stops with an error instead of fixing the lock when it disagrees with package.json, and deletes node_modules and installs fresh. This is the right one for reproducing an import. --offline makes no network request at all and uses only the cache, while --prefer-offline looks at the cache first but goes to download if something is missing. The npm documentation says not to rely on the cache "as a reliable permanent store" — which is why you stand up a registry rather than use the cache as the import vehicle.
What it looks like in the field
I measured this in this lab Pod (npm 10.9.0, verdaccio 6.1.6). With a lock file that has registry.npmjs.org addresses baked in, running npm ci --registry http://127.0.0.1:4873/ with the outside blocked installed 6 packages from the internal registry under the default, while giving --replace-registry-host=never stopped with a proxy connection error. Same lock file, same registry setting, yet a one-line default decides the result. Also, if you request a package that was not in the repository from a verdaccio whose uplink is cut, you get a 404. It does not mean "the registry is broken" but "it was not on the import list", so a 404 is a signal to rewrite the import request.
Verdaccio itself is an npm package, so it too is an import. Installing it in this lab pulled in about 270 dependencies and took 59MB of disk. Only if you install it with the version pinned and also keep that lock file can you stand up the same verdaccio again inside the air-gapped network.
What you will do in the next lab
You install verdaccio 6.1.6, serve a registry with npmjs as upstream at npm.airgap.internal:4873, point to it with the project .npmrc, and pull chalk 4.1.2 to fill it. You cut the uplink, and the grader runs npm ci again with a fresh cache and the outside blocked. Finally, using a lock file made outside, you compare the replace-registry-host default with never yourself and record the result.
Reference documents: npmrc · npm config (registry, replace-registry-host, offline) · npm ci · verdaccio Configuration · verdaccio Packages · Nexus Repository Types · Nexus System Requirements