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.minisigreducewaste-1.0.0-x86_64.rwb # x86 servers and RTX workstationsreducewaste-1.0.0-x86_64.rwb.minisigInstall from a bundle
Section titled “Install from a bundle”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:
sudo bash install.sh --bundle /media/usb/reducewaste-1.0.0-aarch64.rwbThe installer:
- 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); - verifies the signature with
opensslagainst the release key pinned insideinstall.sh. A bundle that fails is refused before any of its bytes are used; - installs the
rwcommand line from the bundle’s own Python wheels into/opt/reducewaste/cli/(no package index, no network); - runs
rw install --bundle ..., which does the rest (below); - 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:
rw install --bundle /media/usb/reducewaste-1.0.0-aarch64.rwb \ --admin-email you@plant.example --admin-site mainrw install, in order, and loading nothing until steps 1 and 2 pass:
- verifies the
.minisigagainst the pinned release key (or--pubkey FILE); - 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; docker loads each image, checks that it loaded as the image the manifest pins, and tags itreducewaste-bundle/<name>:<version>. A generatedbundle.override.yamlpins every service to those tags withpull_policy: never, so Compose never pulls or builds;- writes the release to
/opt/reducewaste/releases/<version>/and keeps the site’s own files beside it:.env(generated secrets, mode 0600) andstate/(broker credentials, hardened-profile secrets, inference server flags); - copies the model weights into the
rw-infer-modelsvolume (skipped when unchanged), migrates the historian, then starts everything withdocker compose up -d --wait; - points
/opt/reducewaste/currentat 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:
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.
The first admin
Section titled “The first admin”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:
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-1Or at any time after the install:
rw admin create --email you@plant.example --site plant-1 --site-name "Plant 1" \ --install-dir /opt/reducewasteThe 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.
Upgrade and roll back
Section titled “Upgrade and roll back”An upgrade is the same command with the newer bundle:
rw install --bundle /media/usb/reducewaste-1.1.0-aarch64.rwbThe 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.
What is inside a bundle
Section titled “What is inside a bundle”reducewaste-<version>-<arch>.rwb is a zstd-compressed tar:
| Path | Contents |
|---|---|
manifest.json | version, 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.
Verify a bundle by hand
Section titled “Verify a bundle by hand”With the minisign tool:
minisign -Vm reducewaste-1.0.0-aarch64.rwb -p deploy/keys/minisign.pubor with rw, against the pinned release key:
rw sign verify reducewaste-1.0.0-aarch64.rwbBoth print Signature and comment signature verified and the signed comment, which carries
the version and architecture.
Build a bundle
Section titled “Build a bundle”On a connected build box with a source checkout, for the box’s own architecture (build the
aarch64 bundle on an arm64 machine):
uv run rw models pull ornith-1.5-9b --dest ./.rw/modelsuv run rw models pull qwen3-embedding-0.6b --dest ./.rw/modelsbash scripts/vendor_wheels.sh "$(uname -m)"docker compose -f compose.yaml -f deploy/compose/hardened.yaml \ --profile hardened --profile demo builddocker build --network none --build-arg PACKAGE=rw-cli \ -f docker/python.Dockerfile -t reducewaste/rw-cli:dev .npm -w docs run builduv 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.
Signing keys
Section titled “Signing keys”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:
rw sign keygen --pub deploy/keys/minisign.pub --sec ~/.rw/minisign.keyminisign -G -p deploy/keys/minisign.pub -s ~/.rw/minisign.keyrw 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.
Confirming nothing reaches out
Section titled “Confirming nothing reaches out”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:
docker network inspect rw-core --format '{{.Internal}}' # trueThe 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.