Skip to content

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.

AreaBehaviour
Agent sandboxesOne pod per site, in the rw-sandboxes namespace, under the RuntimeClass gvisor. If that RuntimeClass is missing, rw-brain refuses to start.
Sandbox networkEach 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 SecurityThe sandbox namespace enforces the restricted Pod Security standard.
GatewayThe 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 rightsThe 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 servicesDefault-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.
ContainersNon-root, read-only root filesystem, every capability dropped, no privilege escalation, seccomp RuntimeDefault. Only rw-brain gets a service-account token.
HistorianA StatefulSet with exactly one replica.
SecretsGenerated once into rw-secrets and rw-broker-creds, and kept across upgrades and uninstall. Services read them through secretKeyRef or as files.
  1. Install gVisor on the nodes that run sandboxes. Install runsc and register it with containerd under the handler runsc, following the gVisor containerd guide. Then create the gvisor RuntimeClass, 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
  2. Use a network plugin that enforces NetworkPolicy (Calico, Cilium, kindnet, …). Without one, the policies are accepted but nothing is blocked.

  3. Build and push the images to the site’s registry. The image names are the Compose ones; set images.registry to your registry and images.tag to 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.

Terminal window
mkdir -p .rw/infer-args
uv run rw infer args ornith-1.5-9b > .rw/infer-args/rw-infer.args
uv run rw infer args qwen3-embedding-0.6b > .rw/infer-args/rw-embed.args
helm 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.args

Then apply the database schema and set the document parser’s password. These are the same two steps as the Compose install:

Terminal window
kubectl -n reducewaste rollout status statefulset/rw-historian
kubectl -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:

Terminal window
kubectl -n reducewaste port-forward svc/rw-app 4441:3000

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:

Terminal window
--set infer.backend=vllm --set 'infer.gpu.nvidia\.com/gpu=1'

Label the nodes by role and point each role at its label:

Terminal window
kubectl label node box-a rw.role=core
kubectl label node box-b rw.role=infer
helm upgrade rw deploy/helm/reducewaste -n reducewaste --reuse-values \
--set 'nodeSelectors.core.rw\.role=core' --set 'nodeSelectors.infer.rw\.role=infer'
RoleServices
corehistorian, broker, ingest, rollup, detect, app, brain, docparse
inferrw-infer, rw-embed, rw-infer-vision
visionrw-vision
edgerw-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).

rw-edge and rw-vision are off by default. Each one may reach only the networks you list:

Terminal window
--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}'

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:

Terminal window
helm upgrade rw deploy/helm/reducewaste -n reducewaste --reuse-values \
--set sandboxes.runtimeClass.name=runc --set sandboxes.requireRunsc=false \
--set-string docparse.runtimeClassName=

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:

Terminal window
bin=$(sh deploy/helm/tools.sh)
helm lint deploy/helm/reducewaste
helm template rw deploy/helm/reducewaste | "$bin/kubeconform" -strict -summary -kubernetes-version 1.37.0

The 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.

Terminal window
uv run pytest -q -p no:cacheprovider -m docker services/rw-brain/tests/sandbox/test_k8s_kind.py -s