Skip to content

Air-gapped install

ReduceWaste needs no internet access once it runs: every service sits on an internal Docker network, and the only two outbound paths (a signed update check and a time-boxed support tunnel) are off by default. See Architecture & isolation and Security model. This page covers getting it onto a box that has no network access at all.

The unit of delivery is one file per release and CPU architecture, plus its signature:

reducewaste-1.0.0-aarch64.rwb # GB10-class boxes (DGX Spark, ASUS GX10, ...)
reducewaste-1.0.0-aarch64.rwb.minisig
reducewaste-1.0.0-x86_64.rwb # x86 servers and RTX workstations
reducewaste-1.0.0-x86_64.rwb.minisig

On the offline box you need Docker 24+ with the Compose plugin, zstd, openssl 3 and python3.12 (Ubuntu 24.04 and DGX OS have all of them). Copy the .rwb, its .minisig and install.sh across the gap, then:

Terminal window
sudo bash install.sh --bundle /media/usb/reducewaste-1.0.0-aarch64.rwb

The installer:

  1. checks Docker, Compose, disk, memory and the app port, and reads the GPU (nvidia-smi) and memory to report which tier the box is sized for (Hardware sizing);
  2. verifies the signature with openssl against the release key pinned inside install.sh. A bundle that fails is refused before any of its bytes are used;
  3. installs the rw command line from the bundle’s own Python wheels into /opt/reducewaste/cli/ (no package index, no network);
  4. runs rw install --bundle ..., which does the rest (below);
  5. asks for the first admin: an email, a site id (default main) and a password that is not echoed. Everyone else is added later in Settings.

If rw is already installed, the same thing without the script is:

Terminal window
rw install --bundle /media/usb/reducewaste-1.0.0-aarch64.rwb \
--admin-email you@plant.example --admin-site main

rw install, in order, and loading nothing until steps 1 and 2 pass:

  1. verifies the .minisig against the pinned release key (or --pubkey FILE);
  2. extracts the bundle (regular files and directories only) and checks the sha256 of every file against manifest.json. A missing, extra or changed file stops the install;
  3. docker loads each image, checks that it loaded as the image the manifest pins, and tags it reducewaste-bundle/<name>:<version>. A generated bundle.override.yaml pins every service to those tags with pull_policy: never, so Compose never pulls or builds;
  4. writes the release to /opt/reducewaste/releases/<version>/ and keeps the site’s own files beside it: .env (generated secrets, mode 0600) and state/ (broker credentials, hardened-profile secrets, inference server flags);
  5. copies the model weights into the rw-infer-models volume (skipped when unchanged), migrates the historian, then starts everything with docker compose up -d --wait;
  6. points /opt/reducewaste/current at the new release.

Running the same bundle again changes nothing: images that are already present are not reloaded, no migration runs twice, and the containers are left as they are.

Check it:

Terminal window
curl -s localhost:4441/api/health # {"ok":true}

Use --install-dir, --app-port and --app-bind 0.0.0.0 (or the install.sh flags --install-dir, --port, --bind) to change where it lives and who can reach it.

Settings adds people only from an admin’s session, so a new site gets its first admin from the command line. install.sh asks for it; unattended, pass a password file:

Terminal window
bash install.sh --bundle reducewaste-1.0.0-x86_64.rwb --no-interactive \
--admin-email you@plant.example --admin-password-file ./pw.txt --admin-site plant-1

Or at any time after the install:

Terminal window
rw admin create --email you@plant.example --site plant-1 --site-name "Plant 1" \
--install-dir /opt/reducewaste

The account is a local one with the admin role on the given site (created when missing), or on every site when you give none. The password must be 12 to 200 characters. It is read from the file or a no-echo prompt and is never printed. An audit-log entry records the creation. The command refuses while any admin exists; --force-another adds a second one anyway.

An upgrade is the same command with the newer bundle:

Terminal window
rw install --bundle /media/usb/reducewaste-1.1.0-aarch64.rwb

The new release is migrated and started next to the old one’s files. If any service fails to become healthy, rw install brings the previous release back up with its own images, which are still loaded, and exits with an error that names both versions. current only moves after a successful start. Migrations only add to the schema, so a rolled-back release runs against the newer schema; take a database backup before a major upgrade if you may need the old one.

reducewaste-<version>-<arch>.rwb is a zstd-compressed tar:

PathContents
manifest.jsonversion, architecture, the Compose plan (files, profiles, services), every image’s ID, OCI manifest digest and registry digests, the model profiles, and the sha256 of every other file
images/one docker save per image, including the rw-cli tool image and the agent sandbox image
wheels/<arch>/, wheels/members/the vendored Python wheels and the rw command line
models/the weights of the bundled model profiles, in the volume’s layout
profiles/model profiles; rw install renders the inference server flags from them
compose/compose.yaml, deploy/compose/ and every repository file a service mounts
recipes/, knowledge/, db/the sensor recipe library, the offline knowledge pack, migrations
docs-site/this documentation, built
helm/the Helm chart
sbom/CycloneDX SBOMs for the Python and npm dependencies and for each image, and licence-report.txt

The bundle is only built when the licence check passes: any component outside the permissive allowlist stops rw bundle with the list of violations.

With the minisign tool:

Terminal window
minisign -Vm reducewaste-1.0.0-aarch64.rwb -p deploy/keys/minisign.pub

or with rw, against the pinned release key:

Terminal window
rw sign verify reducewaste-1.0.0-aarch64.rwb

Both print Signature and comment signature verified and the signed comment, which carries the version and architecture.

On a connected build box with a source checkout, for the box’s own architecture (build the aarch64 bundle on an arm64 machine):

Terminal window
uv run rw models pull ornith-1.5-9b --dest ./.rw/models
uv run rw models pull qwen3-embedding-0.6b --dest ./.rw/models
bash scripts/vendor_wheels.sh "$(uname -m)"
docker compose -f compose.yaml -f deploy/compose/hardened.yaml \
--profile hardened --profile demo build
docker build --network none --build-arg PACKAGE=rw-cli \
-f docker/python.Dockerfile -t reducewaste/rw-cli:dev .
npm -w docs run build
uv run rw bundle --version 1.0.0 --key ~/.rw/minisign.key --hardened --profile demo \
--model rw-infer=ornith-1.5-9b --model rw-embed=qwen3-embedding-0.6b --out dist/

--service limits the bundle to some services (their dependencies come along), --compose-file adds a site’s own override file, and --extra-image ships another image. Without --bundle, install.sh runs exactly these steps from the checkout with a key it makes for that box, then installs the result.

The release key is a minisign key. Only its public half is in the repository, at deploy/keys/minisign.pub, and the same value is pinned in rw and in install.sh. The private half never goes into a bundle, an image or the repository.

Make a key pair with either tool:

Terminal window
rw sign keygen --pub deploy/keys/minisign.pub --sec ~/.rw/minisign.key
minisign -G -p deploy/keys/minisign.pub -s ~/.rw/minisign.key

rw sign keygen asks for a password (or reads RW_SIGN_PASSWORD); --no-password writes an unencrypted key for a CI secret store. Keys made by either tool work with both. rw bundle --key and rw sign file read the password from RW_SIGN_PASSWORD or ask for it. To rotate the key, replace deploy/keys/minisign.pub, the RELEASE_PUBKEY constant in packages/rw-cli/src/rw_cli/sign.py and in install.sh (a test checks that the three match), and sign the next release with the new key.

A site can trust its own key instead: pass --pubkey site.pub to install.sh and to rw install.

Every network the services use except the operator app’s and the edge reader’s is an internal Docker network, which Docker does not route outbound:

Terminal window
docker network inspect rw-core --format '{{.Internal}}' # true

The bundle path is tested end to end on a Docker daemon that has no network interface at all (tests/airgap/run.sh): install, re-install, upgrade, rollback and a tampered-bundle refusal all pass there, so no step depends on a registry or a download.