Skip to content

Air-gapped installation

Images and CVE feeds with no internet access

Edit this page
On this page

Loomscope is designed to run with no internet access. Nothing phones home: no telemetry, no licence check, no update ping. Two things normally reach out, and both have an offline path.

What needs carrying across

ThingWhy it reaches outOffline path
Container imagesPulled from ghcr.iodocker save / docker load
CVE feedsOSV and NVD advisoriesA mirror directory, populated on a connected host

That is the whole list. The AI assistant would be a third, which is why it is off by default — and why the Ollama provider exists, so an air-gapped deployment that wants an assistant can have one without a network egress.

Container images

On a connected machine:

bash
VERSION=0.13.1
for img in server daemon cve-ingest; do
  docker pull ghcr.io/bendaamerahmed/loomscope-$img:$VERSION
done
docker pull postgres:17-alpine

docker save \
  ghcr.io/bendaamerahmed/loomscope-server:$VERSION \
  ghcr.io/bendaamerahmed/loomscope-daemon:$VERSION \
  ghcr.io/bendaamerahmed/loomscope-cve-ingest:$VERSION \
  postgres:17-alpine \
  -o loomscope-$VERSION-images.tar

Carry the tar across, then:

bash
docker load -i loomscope-0.13.1-images.tar

Pin the version in your compose file to the one you loaded. A moving tag in an air-gapped install is worse than useless: it will fail to pull rather than silently upgrading, but it will fail at the least convenient moment.

The CVE mirror

The ingest worker reads a local directory given by LOOMSCOPE_CVE_MIRROR instead of fetching from the network.

Build it on a connected machine:

bash
./scripts/airgap-cve-snapshot.sh /var/tmp/cve-mirror

The script fetches OSV and NVD data and packages it with a checksum. Copy the result to the air-gapped host and point the worker at it:

bash
LOOMSCOPE_CVE_MIRROR=/var/lib/loomscope/cve-mirror

In the compose stack this is a named volume, cve-mirror, so populating it means copying into the volume rather than editing a bind path.

Freshness is now your problem

This is the honest part. If the mirror is stale, matches are stale, and nothing in the interface will look wrong — the vulnerability list will show findings, the counts will be plausible, and none of it will include anything published since the last copy in.

Two habits make that visible:

  • Check the ingest worker's last successful sync. A mirror that has not been refreshed in a month is a month of advisories you have not seen.
  • Copy on a schedule you actually keep, and record the date somewhere a human reads.

The coverage check on the vulnerabilities page reports how old the advisory data is, precisely so this does not have to be remembered.

Installing with no registry at all

Nothing else in the stack fetches anything at runtime. The control plane serves its own assets, migrations are files in the image, and the daemon loads signatures from the image plus whatever YAML you mounted.

The one thing to check is that your compose file has no build: stanzas pointing at Dockerfiles you did not carry across, and no :latest anywhere.

Updating an air-gapped install

The same procedure as any upgrade, with the transfer added:

  1. Read the changelog for the target version.
  2. Back up the database and confirm you hold LOOMSCOPE_KMS_KEY. See Backup and restore.
  3. Save, carry and load the new images.
  4. Stop the daemons — they tolerate a control plane that is briefly away.
  5. Start the new control plane, run migrations, then start the daemons.

Migrations follow expand-and-contract, so a new schema stays readable by the previous release. Upgrade one minor version at a time; skipping several is not tested.

What is still manual

There is no single bundling script that produces one air-gap archive containing images, chart and mirror together. infra/AIRGAP.md in the repository covers both transfers in full, and the roadmap tracks the bundle.