Install with Helm (Kubernetes)
The chart lives in deploy/helm/reducewaste. It is the Kubernetes form of the
hardened Compose profile: the same services, environment, secrets
and hardening, placed on nodes by role. The agent sandboxes run as gVisor pods in their
own namespace.
Install one release per namespace. The services call each other by their Compose
names (rw-historian, rw-broker, rw-infer, …), so the chart keeps those names.
What the chart sets up
Section titled “What the chart sets up”| Area | Behaviour |
|---|---|
| Agent sandboxes | One pod per site, in the rw-sandboxes namespace, under the RuntimeClass gvisor. If that RuntimeClass is missing, rw-brain refuses to start. |
| Sandbox network | Each sandbox pod gets its own NetworkPolicy. It allows no ingress, and egress only to the relay port of the rw-brain pod. There is no DNS, no other sandbox, no database and no internet. A namespace-wide default-deny policy covers every pod there. |
| Pod Security | The sandbox namespace enforces the restricted Pod Security standard. |
| Gateway | The gateway listens on a unix socket only. The relay container in the rw-brain pod serves /gw/* from that socket and answers 404 to everything else. |
| Control-plane rights | The rw-brain service account can create, read and delete pods and network policies in rw-sandboxes. It can also read the one RuntimeClass. Nothing else. |
| Other services | Default-deny for the whole release. Pods may talk only to other pods of the release and to the cluster DNS. Ingress is per service, matching the Compose wiring. |
| Containers | Non-root, read-only root filesystem, every capability dropped, no privilege escalation, seccomp RuntimeDefault. Only rw-brain gets a service-account token. |
| Historian | A StatefulSet with exactly one replica. |
| Secrets | Generated once into rw-secrets and rw-broker-creds, and kept across upgrades and uninstall. Services read them through secretKeyRef or as files. |
Before you install
Section titled “Before you install”-
Install gVisor on the nodes that run sandboxes. Install
runscand register it with containerd under the handlerrunsc, following the gVisor containerd guide. Then create thegvisorRuntimeClass, or let the chart create it:Terminal window kubectl get runtimeclass gvisor # provided by the platform (e.g. GKE Sandbox), or# set sandboxes.runtimeClass.create=true below -
Use a network plugin that enforces NetworkPolicy (Calico, Cilium, kindnet, …). Without one, the policies are accepted but nothing is blocked.
-
Build and push the images to the site’s registry. The image names are the Compose ones; set
images.registryto your registry andimages.tagto the tag you pushed:Terminal window docker build -f services/rw-brain/Dockerfile.sandbox -t rw-sandbox:dev .docker build -f services/rw-brain/Dockerfile.sandbox --target brain -t reducewaste/rw-brain:dev .docker build --build-arg PACKAGE=rw-ingest -f docker/python.Dockerfile -t reducewaste/rw-ingest:dev .docker build --build-arg PACKAGE=rw-detect -f docker/python.Dockerfile -t reducewaste/rw-detect:dev .docker build -f services/rw-docparse/Dockerfile -t rw-docparse:dev .docker build -f docker/node.Dockerfile -t reducewaste-rw-app:dev .Third-party images (PostgreSQL + pgvector, Mosquitto, Caddy, vLLM, SGLang) are pinned by digest in
values.yaml.
Install
Section titled “Install”mkdir -p .rw/infer-argsuv run rw infer args ornith-1.5-9b > .rw/infer-args/rw-infer.argsuv run rw infer args qwen3-embedding-0.6b > .rw/infer-args/rw-embed.argshelm install rw deploy/helm/reducewaste -n reducewaste --create-namespace \ --set images.registry=registry.example.internal/reducewaste \ --set-file infer.args=.rw/infer-args/rw-infer.args \ --set-file embed.args=.rw/infer-args/rw-embed.argsThen apply the database schema and set the document parser’s password. These are the same two steps as the Compose install:
kubectl -n reducewaste rollout status statefulset/rw-historiankubectl -n reducewaste port-forward svc/rw-historian 55432:5432 &PW=$(kubectl -n reducewaste get secret rw-secrets -o jsonpath='{.data.historian-db}' | base64 -d)uv run rw db migrate --dsn "postgresql://rw:$PW@127.0.0.1:55432/rw"
DP=$(kubectl -n reducewaste get secret rw-secrets -o jsonpath='{.data.docparse-db}' | base64 -d)kubectl -n reducewaste exec -i rw-historian-0 -- psql -U rw -d rw -v pw="$DP" \ <<< "ALTER ROLE rw_docparse PASSWORD :'pw'"Open the app through your own ingress or load balancer (app.service.type), or forward
it for a first look:
kubectl -n reducewaste port-forward svc/rw-app 4441:3000Model weights
Section titled “Model weights”The inference servers never download anything. Copy the weights into the
rw-infer-models volume (the layout rw models pull --dest writes). Pass each server’s
flags as infer.args, embed.args and inferVision.args; these are the outputs of
rw infer args <profile>. For a GPU server, set infer.backend to vllm or sglang and
request the GPU with infer.gpu:
--set infer.backend=vllm --set 'infer.gpu.nvidia\.com/gpu=1'Multi-box sites
Section titled “Multi-box sites”Label the nodes by role and point each role at its label:
kubectl label node box-a rw.role=corekubectl label node box-b rw.role=inferhelm upgrade rw deploy/helm/reducewaste -n reducewaste --reuse-values \ --set 'nodeSelectors.core.rw\.role=core' --set 'nodeSelectors.infer.rw\.role=infer'| Role | Services |
|---|---|
core | historian, broker, ingest, rollup, detect, app, brain, docparse |
infer | rw-infer, rw-embed, rw-infer-vision |
vision | rw-vision |
edge | rw-edge, the box in the DMZ that can reach the PLCs |
Sandbox pods follow their RuntimeClass (sandboxes.runtimeClass.nodeSelector). The
files, edgeConfig and visionMedia volumes are shared between services. They work as
ReadWriteOnce while the services that share them run on one node. A multi-node core
needs a ReadWriteMany storage class (persistence.files.accessModes).
Edge and cameras
Section titled “Edge and cameras”rw-edge and rw-vision are off by default. Each one may reach only the networks you
list:
--set edge.enabled=true --set-file edge.config=site/edge.yaml \--set 'edge.otCidrs={10.10.0.0/16}'--set vision.enabled=true --set-file vision.cameras=site/cameras.yaml \--set vision.cameraSecrets.cam-line-1='rtsp://user:pass@10.20.0.11/stream' \--set 'vision.cameraCidrs={10.20.0.0/24}'Development clusters without gVisor
Section titled “Development clusters without gVisor”On a laptop cluster (kind, minikube) without gVisor, the default install starts every
service except rw-brain. Its log says RuntimeClass gvisor not found. That is the
fail-closed behaviour. To run sandboxes on the node’s default runtime, opt out
explicitly. Never do this on a site:
helm upgrade rw deploy/helm/reducewaste -n reducewaste --reuse-values \ --set sandboxes.runtimeClass.name=runc --set sandboxes.requireRunsc=false \ --set-string docparse.runtimeClassName=Checking the chart
Section titled “Checking the chart”The chart is linted, rendered and validated against the Kubernetes schemas with pinned
tools. deploy/helm/tools.sh downloads kind and kubeconform at fixed versions and checks
their sha256:
bin=$(sh deploy/helm/tools.sh)helm lint deploy/helm/reducewastehelm template rw deploy/helm/reducewaste | "$bin/kubeconform" -strict -summary -kubernetes-version 1.37.0The full in-cluster test creates a kind cluster with the pinned node image. It installs
the chart and proves three things: the missing RuntimeClass fails closed; with the runc
override, two sandboxes reach only the relay; and the cluster is deleted at the end.
uv run pytest -q -p no:cacheprovider -m docker services/rw-brain/tests/sandbox/test_k8s_kind.py -s