Hardened profile
The hardened profile is the way to run ReduceWaste on a real site. It is an override file,
deploy/compose/hardened.yaml, on top of the same compose.yaml the
Compose install uses, plus the hardened profile:
docker compose -f compose.yaml -f deploy/compose/hardened.yaml --profile hardened up -d --waitWith the override file but without --profile hardened, nothing starts. That is on
purpose: a half-hardened stack cannot come up by accident. Never combine it with
--profile dev; the dev port forwarders are switched off in this profile anyway.
What changes
Section titled “What changes”| Area | Hardened behaviour |
|---|---|
| Agent sandboxes | gVisor (runsc) only: RW_SANDBOX_RUNTIME=runsc and RW_REQUIRE_RUNSC=1. Without runsc the control plane refuses to start. runc is a dev-only runtime. |
| Document parser | rw-docparse runs under runsc too. |
| Filesystem | Every container has a read-only root filesystem. Only tmpfs paths and named volumes are writable. |
| Privileges | cap_drop: ALL and no-new-privileges everywhere. The one-shot init jobs get back only the capability they need to hand a fresh volume to its service user. |
| Kernel filters | Docker’s builtin seccomp profile on every service, and AppArmor docker-default where the host runs AppArmor. |
| Users | Services run as non-root users (uid 10001; the historian as postgres, the broker as mosquitto). |
| Limits | Memory, CPU and process-count limits on every service, plus rotated logs (10 MB × 5). |
| Health | A healthcheck on every long-running service that can carry one, and a restart policy. |
| Images | Every third-party image is pinned by its multi-arch digest (amd64 and arm64). |
| Secrets | No password or token in any container environment. They come from compose secrets. |
| Ports | Only the app port (4441, bound to 127.0.0.1 unless you set RW_APP_BIND) is published. |
Before the first start
Section titled “Before the first start”-
Install gVisor on the host and register it with Docker, following the gVisor install guide (
runsc install, then restart Docker). Check it:Terminal window docker info --format '{{json .Runtimes}}' | grep -q runsc && echo runsc-ok -
Write the secrets (once per site):
Terminal window bash deploy/compose/hardened-secrets.shThis creates
./.rw/secrets/(or$RW_SECRETS_DIR) with the historian password, the matching database URL, the app-to-control-plane token, the sandbox signing key and the document parser’s database password. Existing files are kept. Keep this directory out of backups that leave the site. -
Build the images offline from vendored wheels (see Air-gapped install for how the wheels reach the box):
Terminal window bash scripts/vendor_wheels.sh x86_64 # aarch64 on a GB10-class boxdocker compose -f compose.yaml -f deploy/compose/hardened.yaml --profile hardened build -
Start the broker first, so its init job writes the broker credentials the other services mount:
Terminal window H="docker compose -f compose.yaml -f deploy/compose/hardened.yaml --profile hardened"$H up -d --wait rw-broker rw-historian -
Migrate the historian and set the document parser’s password. The historian has no host port in this profile, so run the migration on its network:
Terminal window docker run --rm --network rw-core \-v "$PWD/.rw/secrets/historian-dsn:/run/secrets/env.RW_DSN:ro" -v "$PWD/db:/db:ro" \--entrypoint rw-env reducewaste/rw-brain:dev \rw db migrate --dir /db/migrations --rls-file /db/rls/agent-rls.sqldocker exec -i rw-historian psql -U rw -d rw -v pw="$(cat .rw/secrets/docparse-db)" \<<< "ALTER ROLE rw_docparse PASSWORD :'pw'" -
Start everything:
Terminal window RW_DOCKER_GID=$(stat -c %g /var/run/docker.sock) $H up -d --wait
Add --profile demo to run the simulated plant on a hardened box. rw-edge is in the
edge profile, as in the plain Compose install.
How secrets reach the services
Section titled “How secrets reach the services”The historian reads POSTGRES_PASSWORD_FILE. Services that read a database URL or token
from their environment (RW_DSN, DATABASE_URL, BRAIN_TOKEN, AGENT_SCOPE_SECRET) get
it from a compose secret mounted as /run/secrets/env.<NAME>. A small entrypoint wrapper in
every image, rw-env, exports each such file as that variable and then starts the service.
The value never appears in docker inspect or docker compose config.
Tuning the limits
Section titled “Tuning the limits”The limits are sized for one GB10-class box. Set these before up to change the largest
ones, or add your own override file for the rest:
| Variable | Default | Service |
|---|---|---|
RW_HISTORIAN_MEM_LIMIT | 8g | rw-historian |
RW_INFER_MEM_LIMIT | 96g | rw-infer, rw-infer-vision, rw-infer-vllm, rw-infer-sglang |
RW_INFER_CPUS | 0 (no cap) | the same inference servers |
RW_VISION_MEM_LIMIT | 8g | rw-vision, rw-vision-gpu |
Things to know
Section titled “Things to know”- The control plane still mounts the Docker socket to create one network and one container per sandbox. That is root-equivalent on the host, so the host itself must be locked down. The Helm chart uses scoped Kubernetes permissions instead.
- An existing historian volume keeps the password it was created with. Moving a dev
install to this profile means
ALTER ROLE rw PASSWORDto the value in.rw/secrets/historian-dbfirst. mediamtx(demo camera feed) has no healthcheck: its image has no shell or client to probe with.
Checking a stack
Section titled “Checking a stack”H="docker compose -f compose.yaml -f deploy/compose/hardened.yaml --profile hardened"$H config -q && echo config-oktest "$($H config | grep -c cap_drop)" -eq "$($H config --services | wc -l)" && echo all-droppeduv run pytest -q -p no:cacheprovider -m "docker or not docker" tests/deploy/test_hardened.py