Skip to main content

Air-Gapped Installation

For networks with no internet access, install from a bundle. On the internet-connected side, download (or build) the bundle, verify its signature, move it on removable media, and unpack it inside the air-gapped network.

Decide the architecture first

There are two bundles — arm64 (GB10 family) · amd64 (AWS g7e and similar). Check uname -m on the target server and pick accordingly (aarch64 → arm64, x86_64 → amd64). A bundle for the other architecture will not start its images. The import script compares the architecture recorded in the bundle's MANIFEST.txt against this host before installing Docker, and stops on a mismatch.

The bundle is large — about 25GB

Most of it is the vLLM image. Check the capacity of your transfer media and the free space on the target disk in advance.

Step 1 — Download the bundle (internet side)​

Download five files from the distribution server: the bundle itself plus four sidecars.

ARCH=arm64 # 또는 amd64
B=plantpulse-ai-images-latest-$ARCH.tar.gz
for f in "$B" "$B.sha256" "$B.sha256.sig" "$B.pub" "$B.cosign.sha256"; do
curl -fL -C - --retry 10 --retry-delay 5 --retry-all-errors \
-O "https://product.kopens.io/plantpulse-ai/$f"
done
FileWhat it is
<번들>.tar.gzThe bundle
.sha256Checksum of the bundle
.sha256.sigcosign signature over that checksum — the signature covers the checksum file, not the multi-gigabyte bundle
.pubPublic key for verification
.cosign.sha256sha256 of the cosign executable shipped inside the bundle (see "cosign inside the bundle" below)
If the download drops, just run the same command again

-C - resumes and --retry-all-errors retries. Large files sometimes drop because of a CDN-side error; running the command again continues from where it stopped.

Step 2 — Verify the signature​

Never use an unverified bundle. Put the five downloaded files in one directory, then unpack and verify.

sudo -i
mkdir -p /opt/kopens && tar -xzf "$B" -C /opt/kopens
bash /opt/kopens/plantpulse-ai-images/repo/bin/verify-bundle.sh "$B"

Here is what verify-bundle.sh checks, in order. It stops at the first failure.

  1. Whether cosign can be executed — COSIGN_BIN → cosign on PATH → tools/cosign-linux-<arch> from the bundle, in that order
  2. Whether the sidecars (.sha256 · .sha256.sig · public key) sit next to the bundle. If both the repository's public key and the downloaded .pub are present, they must be byte-identical
  3. Whether the signature is valid for the checksum file
  4. Whether the name recorded in the checksum file matches this bundle's name
  5. Whether the bundle's bytes match the signed checksum
Exit codeMeaning
0Verified
1Verification failed — do not use that bundle
2Could not verify (tool or sidecar missing) — this is not a pass

cosign inside the bundle, and the limits of trust​

An air-gapped network has no cosign, so the bundle carries a version-pinned tools/cosign-linux-<arch>. But the cosign inside the bundle is not covered by the bundle signature — it is the very tool that checks that signature. That is why its sha256 is published in three places.

WhereNature
The tools-cosign line in the bundle's MANIFEST.txtA copy we produced
<번들>.cosign.sha256 on the CDNA copy we produced. If placed next to the bundle, verify-bundle.sh compares it automatically and fails on a mismatch
cosign_checksums.txt from upstream sigstoreThe only independent reference

The first two are useless against an attacker who can alter both the bundle and the CDN. Compare against the upstream checksum once on the internet side before importing. If you already have a cosign you trust separately, pass it with COSIGN_BIN=/경로/cosign.

Step 3 — Move to the air-gapped network and import​

Move the bundle together with its sidecars — the import script verifies again before loading.

sudo -i
mkdir -p /opt/kopens && tar -xzf "$B" -C /opt/kopens
cd /opt/kopens/plantpulse-ai-images/repo/bin
BUNDLE_TARBALL="/경로/$B" ./airgap-import.sh

The unpacked layout looks like this. Both sibling repositories are included in the bundle, so there is nothing to prepare separately (the exact commit is in the sibling line of MANIFEST.txt).

/opt/kopens/plantpulse-ai-images/
├── repo/ ← 설치 본체 (bin · compose)
├── plantpulse-ai-inference-server/ ← 형제 저장소
├── plantpulse-timeseries-insight-server/ ← 형제 저장소
├── images/ ← 이미지마다 docker save 한 파일 하나씩
├── pkgs/{rpm,deb}/ ← Docker CE 오프라인 설치 패키지
├── tools/cosign-linux-<arch>
└── MANIFEST.txt

airgap-import.sh processes these in order. If a stage fails, it stops there.

StageWhat it does
0Architecture check · bundle signature verification (verify-bundle.sh)
1Offline Docker installation — only if Docker is absent. If it is already present, the repositories and configuration are left alone and no restart occurs (allow with ALLOW_DOCKER_RECONFIGURE=1)
2Image load — docker load everything, then confirm that every image reference in the bundle resolves by name
3Call install.sh — SKIP_LOGIN=1. Does not contact a registry and skips image pull and automatic model download
It stops if the bundle cannot be found

The signature is checked against the sidecars next to the bundle, so the tar.gz must be present at import time. The script looks for one in the parent of the unpacked directory (/opt/kopens); if none or several are found, it asks you to point at it with BUNDLE_TARBALL and stops. There is a ALLOW_UNVERIFIED=1 that proceeds without verification, but do not use it — it is a switch for loading an unverified bundle.

The default is "prepare only"

install.sh creates /etc/kopens/plantpulse-ai.env and stops. To go all the way to startup in one pass, do export START=1 first and then run airgap-import.sh. If you only prepared, follow "Startup sequence after import" below — the installer also prints the same sequence at the end.

From 2026.10, the values to fill in include the platform credential retrieval settings (PP_DATALAKE_ADMIN_URL · PP_DATALAKE_ADMIN_API_KEY) and the platform trust anchor /etc/kopens/ca/root.crt. Even inside an air-gapped network, the AI host must be able to reach :4950 on the platform host → Installation Overview

Startup sequence after import​

  1. Fill in the remaining placeholders. The import output prints the remaining keys with CHANGE_ME_* (admin and Neo4j passwords, platform token, platform credential key, and so on). Edit /etc/kopens/plantpulse-ai.env with sudo and leave the permissions at 0600.
  2. Place the vLLM models — see "Models are not in the bundle" below.
  3. Bring it up with bin/up.sh.
cd /opt/kopens/plantpulse-ai-images/repo
sudo bin/up.sh
If even one placeholder remains, bin/up.sh will not start the stack (2026.10)

Earlier versions blocked placeholders only on the START=1 path and did not watch the route used in air-gapped setups, where the stack is brought up later with compose-up.sh. As a result, the management console and Neo4j could be exposed on LAN ports with the passwords printed in the public template. Now bin/up.sh also prints the remaining key names and refuses to start.

Do not use bin/update.sh — it tries to pull images first and therefore fails on an air-gapped network with no registry.

Host-wide settings follow HOST_TUNING

install.sh changes locale · time zone (PP_TZ) · swap · chrony · limits · sysctl · tmpfiles · firewall. On a host that already runs containers for other services, the default is plan, so it only prints what would change. Review the output and re-run with HOST_TUNING=apply if needed → Installation Overview

Models are not in the bundle​

🔴 The vLLM models (LLM · embedding · reranker, over 20GB combined) are not included in the bundle. An online installation downloads the models automatically during START=1, but an air-gapped import skips that stage.

  • vLLM models — download them on an internet-connected machine with bin/fetch-models.sh. These are the same release weights (model.kopens.ai) that the online installation downloads; they are compared against SHA256SUMS and then unpacked (zstd is required). Specify the destination with MODEL_BASE and the name with MODELS.

    MODEL_BASE=/mnt/usb/models MODELS="Aura-Lite-NVFP4 Aura-Embedding-4B Aura-Reranker-0.6B" \
    bash /opt/kopens/plantpulse-ai-images/repo/bin/fetch-models.sh

    Move them on media and place them under /opt/kopens/models on the air-gapped machine, in directories with the same names (if you change the location, record LLM_MODEL_PATH · EMB_MODEL_PATH · RERANK_MODEL_PATH in the host env).

  • Time series inference model — nothing to import. It is inside the plantpulse-timeseries-insight-server image.

  • Transcription model (Aura-ASR-Turbo, about 1.5 GB) — import it only for installations that will use video/audio collection and voice queries. The transcription image (plantpulse-asr-audio; the transcription vLLM uses the same image as the generation vLLM) is always shipped in the bundle, so only the model needs to be moved. On an internet-connected machine, set the switch in the shell environment and it is downloaded along with the vLLM models (anything already present at the destination is skipped).

    PI_MEDIA_ENABLED=true MODEL_BASE=/mnt/usb/models \
    bash /opt/kopens/plantpulse-ai-images/repo/bin/fetch-models.sh

    Move it on media, place it at /opt/kopens/models/Aura-ASR-Turbo on the air-gapped machine, add PI_MEDIA_ENABLED=true to the host env, and start → Video · Audio Collection. To fetch only that model by name, you can pass MODELS="Aura-ASR-Turbo". A run that passes the COMPOSE_PROFILES=media from the older instructions still downloads this model.

Do not download it with download-model.sh

The tools/download-model.sh in the inference server repository downloads the upstream original weights and merely labels them Aura-*. The names match, so no check will catch the difference, but these are not the release weights. Always use bin/fetch-models.sh.

If you start without the models, the stack comes up green and only the LLM dies. Confirm the models are in place before starting.

Building the bundle yourself​

Use this only when you must build in-house instead of using the bundle from the distribution server. Run it on a machine with the same architecture as the target — cross builds are not supported.

cd /opt/kopens/plantpulse-ai-docker/bin
./airgap-export.sh
Environment variableDefaultMeaning
ARCHThis hostarm64 · amd64. Compared against the host; stops on a mismatch
OS_TARGETFollows the architecture — arm64 → deb, amd64 → rpmrpm · deb · both — the offline Docker package to include
EXPORT_TAGToday's dateUsed only in the artifact name
OUT_DIR<저장소>/distWhere artifacts are written

The artifacts are dist/plantpulse-ai-images-<태그>-<아키텍처>.tar.gz and .sha256. The image list is extracted with docker compose config --images, so it is never written by hand — changing a model or bumping the vLLM version requires no change to the script. Both sibling repositories are packed exactly as checked out for the build.

A self-built bundle has no signature

The signature (.sha256.sig) is applied by the release pipeline; a manual build skips the signing stage (the log prints "signing skipped"). An unsigned bundle will not pass stage 0 verification at import, so use the bundle from the distribution server unless you have a specific reason not to.

After installation​

OrderWhat to doCommand · Document
1Change the default passwordsChange the Initial Password
2Check that the stack is alivebin/status.sh · bin/health.sh
3Pinpoint where it is stuckbin/doctor.sh

Common pitfalls​

SymptomCauseAction
verify-bundle.sh returns exit code 2cosign is missing or a sidecar was not downloadedThis is not a pass. Put all four sidecars next to the bundle and verify again
Import stops with "bundle not found"The tar.gz was not moved, or it is elsewherePoint at it with BUNDLE_TARBALL=/경로/<번들>.tar.gz
Import stops on an architecture mismatchBundle for the other architectureDownload the bundle matching uname -m
After loading, it stops with "image does not resolve by name"Defective bundleDo not use that bundle; download a new one
Stack is green but only the LLM failsThe models were not importedSee "Models are not in the bundle" above
Started with PI_MEDIA_ENABLED=true but only plantpulse-asr fails to come upThe transcription model (Aura-ASR-Turbo) was not importedSee the transcription model under "Models are not in the bundle" above
The stack is only prepared and never startsSTART=1 was not passedThis is correct behavior. Fill in the values as in "Startup sequence after import" above and start with bin/up.sh
bin/up.sh stops with «still has template placeholders»Placeholders (CHANGE_ME_*) remainFill in the printed keys and start again
bin/update.sh fails at image pullThere is no registry on an air-gapped networkUse bin/up.sh
Host settings were not changedThe host runs other containers, so it was HOST_TUNING=planReview the output and re-run with HOST_TUNING=apply