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.
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.
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
| File | What it is |
|---|---|
<번들>.tar.gz | The bundle |
.sha256 | Checksum of the bundle |
.sha256.sig | cosign signature over that checksum — the signature covers the checksum file, not the multi-gigabyte bundle |
.pub | Public key for verification |
.cosign.sha256 | sha256 of the cosign executable shipped inside the bundle (see "cosign inside the bundle" below) |
-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.
- Whether cosign can be executed —
COSIGN_BIN→cosignon PATH →tools/cosign-linux-<arch>from the bundle, in that order - Whether the sidecars (
.sha256·.sha256.sig· public key) sit next to the bundle. If both the repository's public key and the downloaded.pubare present, they must be byte-identical - Whether the signature is valid for the checksum file
- Whether the name recorded in the checksum file matches this bundle's name
- Whether the bundle's bytes match the signed checksum
| Exit code | Meaning |
|---|---|
0 | Verified |
1 | Verification failed — do not use that bundle |
2 | Could 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.
| Where | Nature |
|---|---|
The tools-cosign line in the bundle's MANIFEST.txt | A copy we produced |
<번들>.cosign.sha256 on the CDN | A 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 sigstore | The 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.
| Stage | What it does |
|---|---|
| 0 | Architecture check · bundle signature verification (verify-bundle.sh) |
| 1 | Offline 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) |
| 2 | Image load — docker load everything, then confirm that every image reference in the bundle resolves by name |
| 3 | Call install.sh — SKIP_LOGIN=1. Does not contact a registry and skips image pull and automatic model download |
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.
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
- 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.envwithsudoand leave the permissions at0600. - Place the vLLM models — see "Models are not in the bundle" below.
- Bring it up with
bin/up.sh.
cd /opt/kopens/plantpulse-ai-images/repo
sudo bin/up.sh
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_TUNINGinstall.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 againstSHA256SUMSand then unpacked (zstdis required). Specify the destination withMODEL_BASEand the name withMODELS.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.shMove them on media and place them under
/opt/kopens/modelson the air-gapped machine, in directories with the same names (if you change the location, recordLLM_MODEL_PATH·EMB_MODEL_PATH·RERANK_MODEL_PATHin the host env). -
Time series inference model — nothing to import. It is inside the
plantpulse-timeseries-insight-serverimage. -
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.shMove it on media, place it at
/opt/kopens/models/Aura-ASR-Turboon the air-gapped machine, addPI_MEDIA_ENABLED=trueto the host env, and start → Video · Audio Collection. To fetch only that model by name, you can passMODELS="Aura-ASR-Turbo". A run that passes theCOMPOSE_PROFILES=mediafrom the older instructions still downloads this model.
download-model.shThe 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 variable | Default | Meaning |
|---|---|---|
ARCH | This host | arm64 · amd64. Compared against the host; stops on a mismatch |
OS_TARGET | Follows the architecture — arm64 → deb, amd64 → rpm | rpm · deb · both — the offline Docker package to include |
EXPORT_TAG | Today's date | Used only in the artifact name |
OUT_DIR | <저장소>/dist | Where 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.
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
| Order | What to do | Command · Document |
|---|---|---|
| 1 | Change the default passwords | Change the Initial Password |
| 2 | Check that the stack is alive | bin/status.sh · bin/health.sh |
| 3 | Pinpoint where it is stuck | bin/doctor.sh |
Common pitfalls
| Symptom | Cause | Action |
|---|---|---|
verify-bundle.sh returns exit code 2 | cosign is missing or a sidecar was not downloaded | This 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 elsewhere | Point at it with BUNDLE_TARBALL=/경로/<번들>.tar.gz |
| Import stops on an architecture mismatch | Bundle for the other architecture | Download the bundle matching uname -m |
| After loading, it stops with "image does not resolve by name" | Defective bundle | Do not use that bundle; download a new one |
| Stack is green but only the LLM fails | The models were not imported | See "Models are not in the bundle" above |
Started with PI_MEDIA_ENABLED=true but only plantpulse-asr fails to come up | The transcription model (Aura-ASR-Turbo) was not imported | See the transcription model under "Models are not in the bundle" above |
| The stack is only prepared and never starts | START=1 was not passed | This 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_*) remain | Fill in the printed keys and start again |
bin/update.sh fails at image pull | There is no registry on an air-gapped network | Use bin/up.sh |
| Host settings were not changed | The host runs other containers, so it was HOST_TUNING=plan | Review the output and re-run with HOST_TUNING=apply |
Related documents
- Installation Overview — what gets installed where
- One-Line Installation — for internet-connected environments
- System Requirements