Deployment

This is the operator reference for running RoboCo on a NAS or server. If you just want it up on your laptop, the install quickstart is faster — this page assumes you've done that once and now want the durable, server-side setup: the compose files, the host mounts agents need, where data lives, how to back it up, and how to harden it.

Trusted network only

RoboCo is built for a private LAN or homelab. Do not expose it directly to the public internet. nginx is the single entry point, but the orchestrator's WebSocket streams and (in header-trust mode) its API assume a trusted network. Put it behind your own VPN if you need remote access.

The three compose files

There are three tracked compose files, and they are not interchangeable:

FileWhat it doesNeeds a build toolchain?
docker-compose.ymlBuilds every image from the Dockerfiles in docker/.Yes
docker-compose.yamlByte-identical to docker-compose.yml.Yes
docker-compose.registry.ymlPulls and runs the pre-built published images.No

docker-compose.yml and docker-compose.yaml are the same file under two names — Docker Compose picks up either, and the NAS deployment runs the .yaml. If you fork RoboCo and change a service, keep all three in sync.

Which one to run

For a server you don't intend to hack on, run the registry file — it pulls finished images and needs no source tree or compiler on the host:

bash
1docker compose -f docker-compose.registry.yml pull2docker compose -f docker-compose.registry.yml up -d
Or let make quickstart do it

make quickstart runs exactly this path for you — an idempotent scripts/bootstrap.sh that scaffolds .env with freshly generated secrets (or validates an existing one, untouched), pulls, brings the registry compose up, and runs a doctor-style readiness sweep with pointed remedies at whichever stage doesn't check out. See the install quickstart for the one-command version.

Two variables choose what you pull (defaults shown):

bash
1ROBOCO_REGISTRY=ghcr.io/rennf93   # or docker.io/renzof932ROBOCO_VERSION=latest             # or a pinned release, e.g. 0.20.0

The orchestrator then spawns the matching pre-built agent images on demand (it reads ROBOCO_AGENT_IMAGE_REGISTRY / ROBOCO_AGENT_IMAGE_TAG, which the registry compose wires to the same registry and version). Pin ROBOCO_VERSION to a release tag in production so an upstream latest push can't silently change your fleet.

Build from source only when you're modifying RoboCo:

bash
1docker compose up -d   # builds on first run
Agent images are build/pull-only services

The agent-*-image services in every compose file are one-shot stubs — they exist so docker compose build/pull materializes each per-role agent image up front. They never run as long-lived containers. The orchestrator spawns the actual agent containers itself, on demand, over the mounted Docker socket, and tears them down when their work is done.

The single origin

nginx (docker/nginx.conf, rendered from an envsubst template) is the only externally-exposed service. It listens on localhost:3000 and routes by path:

PathUpstream
/api/, /ws/, /health, /readyroboco-orchestrator:8000
everything elseroboco-panel:3000

The browser only ever sees one origin (:3000), so there's no CORS to configure — the panel uses relative /api and /ws URLs and lets nginx dispatch. The panel container is never published directly; you reach it only through nginx. /ws/ also gets a long (86400s) read timeout so live sockets stay open.

The backing services do publish host ports for direct inspection — Postgres on 15432, Redis on 16379, Ollama on 11435, and the orchestrator on 8000. You don't route browser traffic at these; they're there for psql, redis-cli, and the like.

Two internal Docker networks, not one

Separate from nginx's browser-facing single origin, all three tracked compose files also split the internal Docker network in two: roboco_default (the agent mesh — panel, nginx, ollama, every spawned agent container, and their sandbox DB/Redis sidecars) and roboco_data (postgres + redis only). The orchestrator is the only multi-homed service, so an agent container cannot resolve or reach roboco-postgres:5432 / roboco-redis:6379 at all — network membership is the entire containment, which matters because Redis has no auth of its own. ROBOCO_DB_NETWORK_ISOLATED (config default false) is set true by these compose files and suppresses the legacy prod-creds gate-env injection accordingly; see DB network isolation.

Required host-path mounts

The orchestrator is Docker-in-Docker: it mounts /var/run/docker.sock and spawns agent containers itself. Because those agent bind-mounts resolve on the host daemon (not inside the orchestrator container), several paths must be given as absolute host paths — the orchestrator passes them straight through to docker run -v for each agent.

VariableWhat it points atCompose default
ROBOCO_HOST_PROJECT_DIRThe RoboCo project directory on the host./volume1/roboco
ROBOCO_HOST_CLAUDE_DIR / CLAUDE_AUTH_DIRThe host ~/.claude Claude Code auth dir, mounted into the orchestrator and each agent./home/renzof/.claude / ${HOME}/.claude
ROBOCO_HOST_DATA_DIRThe host data dir handed to agents for shared volumes (workspaces, logs, grok-usage)./volume1/roboco/data
ROBOCO_DATA_DIRHost root for all persistent volumes mounted into the backing services and orchestrator (see below)../data
ROBOCO_HOST_GROK_DIRHost ~/.grok SuperGrok auth — only needed if you run any agent on Grok./home/renzof/.grok
ROBOCO_HOST_CODEX_DIRHost ~/.codex ChatGPT-subscription auth — only needed if you run any agent on Codex.~/.codex
ROBOCO_HOST_GEMINI_DIRHost ~/.gemini OAuth login — only needed if you run any agent on Gemini.~/.gemini (registry) / /home/renzof/.gemini
ROBOCO_HOST_KIMI_DIRHost ~/.kimi-code Kimi-subscription auth, shared read-write across every Kimi agent plus the orchestrator — only needed if you run any agent on Kimi.~/.kimi-code
These must be real, absolute host paths

A relative path or a path that only exists inside the orchestrator container will make agent spawns fail, because the host Docker daemon resolves the bind. On a NAS the project and data dirs usually live on the RAID volume (e.g. /volume1/roboco and /volume1/roboco/data).

The host ~/.grok is mounted read-write into the orchestrator (it rewrites the short-lived token in place to keep agents from hanging on an expired login) and read-only into each Grok agent. Run grok login on the host once before enabling Grok. ~/.codex and ~/.gemini are each mounted read-only — Codex's orchestrator-side refresh loop and Gemini's per-container in-process refresh both write only to the image's own local copy, never back to your host credential. ~/.kimi-code is the odd one out: it's mounted read-write and shared into every Kimi agent and the orchestrator, because Moonshot's refresh token rotates with only a short reuse grace — independent per-container copies would eventually cross-invalidate each other, so every container instead redeems the same rotating chain, serialized by the CLI's own cross-process lock. Run codex login / the interactive gemini login / kimi login on the host once before enabling any of the three. Provider routing and the Grok/Codex/Gemini/Kimi runtimes are covered in the models section.

Data persistence and backup

Everything durable lives under ROBOCO_DATA_DIR (default ./data). On a server, point this at a RAID volume:

bash
1ROBOCO_DATA_DIR=/volume1/roboco/data
SubdirectoryHolds
postgres/The entire database — tasks, projects, work sessions, journals, encrypted git tokens, the pgvector store.
redis/Append-only cache, sessions, rate-limit + event-bus state.
ollama/The local model cache (embedding model + local LLM) — large, but re-pullable.
workspaces/Each agent's git clone of each project.
logs/Per-agent run logs.
mcp-configs/, prompts-generated/, agent-settings/, briefings/, manifests/Per-agent spawn artifacts the orchestrator writes.
grok-usage/Per-agent Grok cost/usage capture.
backups/Daily pg_dump snapshots of postgres/, written by the always-on backup sidecar (see below).
vault/The Obsidian vault projection, when ROBOCO_OBSIDIAN_VAULT_ENABLED is on — a rebuildable read surface on the database, not load-bearing itself.

The load-bearing directory is postgres/ (everything else here is either re-derivable or a derived projection of it). ollama/ and workspaces/ are reconstructible — Ollama re-pulls models, agents re-clone repos — so they're not worth backing up at all.

A backup sidecar (pgvector/pgvector:pg16, same image as postgres) ships in both tracked compose files and takes care of Postgres automatically: a pg_dump -Fc on container start and then every 24 hours, written to backups/roboco-<timestamp>.dump, rotated to the newest 14. No flag — it's always on. Setting ROBOCO_BACKUP_MIRROR_DIR to a host path on a different disk additionally mirrors every successful dump there — see Backups for the rotation detail, the off-disk mirror, and the restore procedure (including a quarterly restore-drill script). For a one-off manual dump instead of waiting for the sidecar's cycle, the same command works against the published port:

bash
1pg_dump -h localhost -p 15432 -U roboco -Fc roboco > roboco-backup.dump
Back up `ROBOCO_ENCRYPTION_KEY` with the database

Every per-project GitHub token in the database is Fernet-encrypted with ROBOCO_ENCRYPTION_KEY. A database backup is useless without the key. If you lose or change the key, every stored token becomes undecryptable and must be re-entered project by project. Store the key with your secrets, keep it stable across restarts, and never commit .env.

Object storage (MinIO) — default off

MinIO is the optional durable object store for rendered MP4s. Default is off — empty ROBOCO_MINIO_ENDPOINT leaves the existing FileResponse media-serve path byte-for-byte unchanged, so upgrading is a no-op until you opt in. The NAS compose arms it; the registry compose omits it (NAS default-on, registry default-off).

The minio service runs on the data network only (off the agent mesh) with a named minio-data volume; the orchestrator reaches it over its data NIC. Host ports 19000:9000 (API) and 19001:9001 (console) are published for debugging only.

Arming it

Set the five ROBOCO_MINIO_* vars on the orchestrator (see env reference). Once ROBOCO_MINIO_ENDPOINT is non-empty:

  • Write path — after each render's local write, the orchestrator PUTs the MP4 to ROBOCO_MINIO_BUCKET keyed by its filename. The PUT is non-fatal: if MinIO is down, the render still succeeds and stays on local disk (the source of truth); the failure is only logged.
  • Serve path — the panel preview's media route streams the object from MinIO via StreamingResponse, with _require_ceo auth kept end-to-end (no presigned URLs, no redirect — same URL/headers/body the panel's axios-blob flow already uses). If the object is missing (a render predating MinIO) or MinIO is down, the route falls back to FileResponse from the local render dir.

Backfilling existing renders

Renders created before MinIO was armed live only on the bind mount. Copy them in with mc:

bash
1mc alias set local http://localhost:19000 $ROBOCO_MINIO_ACCESS_KEY $ROBOCO_MINIO_SECRET_KEY2mc cp --recursive ./data/video-renders local/$ROBOCO_MINIO_BUCKET/

The serve route falls back to the local file for any key not yet in the bucket, so backfill is safe to run while the panel is up.

Pointing a dev env at MinIO

A dev (non-NAS) environment can target the NAS MinIO by setting the five ROBOCO_MINIO_* vars to the NAS endpoint (and ensuring the dev host can reach roboco-minio / the published port). For a fully local dev MinIO, run the minio image standalone and point ROBOCO_MINIO_ENDPOINT at it; minio-init creates the bucket idempotently.

Secure mode

On a trusted LAN RoboCo runs in header-trust mode by default (ROBOCO_AGENT_AUTH_REQUIRED=false): callers are identified by role headers, no token required. That's the intended homelab setup.

To harden it so one agent can't spoof another's role, turn on fail-closed auth:

bash
1ROBOCO_AGENT_AUTH_REQUIRED=true2ROBOCO_AGENT_AUTH_SECRET=<your HMAC secret>     # already required for docker compose3ROBOCO_PANEL_AGENT_TOKEN=<from make panel-token>

With auth required, every API call must carry a valid X-Agent-Token. The panel runs in your browser and can't hold the signing secret, so nginx injects the CEO's token for it: generate the token with make panel-token (it signs one using your ROBOCO_AGENT_AUTH_SECRET), put it in ROBOCO_PANEL_AGENT_TOKEN, and nginx adds it as X-Agent-Token on /api and /ws. The panel keeps working; the secret never reaches the browser.

ROBOCO_ENCRYPTION_KEY and ROBOCO_AGENT_AUTH_SECRET are both required for any docker compose run — the orchestrator service block guards them with compose :? so the stack refuses to start if either is unset. See Security for the full sandboxing model and the env reference for every knob.

Cloud auth — for exposing the panel beyond the LAN

Header-trust and ROBOCO_AGENT_AUTH_REQUIRED both assume the agent fleet's identity headers. If you want the CEO's own browser session to require a login instead of relying purely on network trust, arm ROBOCO_CLOUD_AUTH_ENABLED — a single seeded user, a sliding 30-day session cookie, and a startup guard that refuses to boot without ROBOCO_CLOUD_AUTH_SECRET set.

bash
1ROBOCO_CLOUD_AUTH_ENABLED=true2ROBOCO_CLOUD_AUTH_EMAIL=you@example.com3ROBOCO_CLOUD_AUTH_PASSWORD=<a strong password>4ROBOCO_CLOUD_AUTH_SECRET=<from: python -c 'import secrets; print(secrets.token_hex(32))'>
Requires TLS in front of RoboCo

The session cookie is secure-only — browsers won't send it over plain HTTP. You must terminate TLS somewhere in front of nginx (your own reverse proxy or tunnel) before this does anything useful; without it, login will look like it silently fails. See Cloud auth for the full model.

Startup sequence

depends_on conditions enforce a strict boot order; the effective sequence is:

  • postgres / redis / ollama must each pass their healthcheck (pg_isready, redis-cli ping, ollama list) before anything downstream starts.
  • ollama-init is a one-shot that best-effort pulls the embedding model and the local LLM, then gates success on the models being present — a degraded model registry can't take down a fully-cached deployment.
  • orchestrator waits for postgres + redis + ollama healthy, ollama-init completed, and agent-base-image built. On startup it runs the database migrations itself (idempotently, to head) and indexes its knowledge base — you do not run alembic upgrade head by hand for the compose path.
  • panel waits for the orchestrator; nginx waits for both.

First boot is the slow one: the model pulls (the LLM is a couple of minutes) plus knowledge-base indexing. Watch it come up:

bash
1docker compose logs -f orchestrator2curl http://localhost:8000/health3docker ps --filter name=roboco

When the orchestrator reports serving, open http://localhost:3000. A boot that hangs is almost always waiting on ollama-init (model pull) or a healthcheck — check docker compose ps to see which service is still starting. Migration and data details are in Data & migrations; recurring boot symptoms are in Common issues.

Operator-relevant Makefile targets

The Makefile drives the host developer workflow (uv-based, for hacking on RoboCo itself) — it is separate from the Docker stack and needs uv on the host. The handful that matter operationally:

TargetDoes
make panel-tokenPrints a signed CEO token for ROBOCO_PANEL_AGENT_TOKEN (secure mode).
make infraBrings up only postgres + redis (make infra-down stops them) — for host-side dev against the backing services.
make migrateRuns alembic upgrade head on the host (the compose stack self-migrates; this is the host-dev path).
make runRuns the API + orchestrator on the host (no --reload); make api is the reload dev server, make dev runs both.
make qualityThe full merge gate: ruff format-check + lint, mypy, pytest with 80% coverage floor, complexity, security, dependency, and migration checks.
make serve-docsServes this documentation locally with mkdocs serve.
make status / make logsOrchestrator status / recent logs against a running instance.

Run make help for the full list.

Makefile enforcement for agents

The agent runtime uses the same Makefile discipline as the host developer workflow. When an agent is working in a repository that has a Makefile, the bash guard routes quality commands through make targets and blocks the raw package-manager invocations that would bypass the project's guards.

  • Agents run make quality, make gate, make lint, or make test instead of calling uv run ..., pip install ..., conda ..., or poetry ... directly.
  • The bash guard rejects raw uv run, pip install, conda, and poetry invocations whenever a Makefile is present in the working directory.
  • This preserves the Makefile's UV_NO_SYNC=1 setting and its private UV_CACHE_DIR, preventing the concurrent-venv-corruption race that bare uv run would trigger.

This applies to the agent's runtime path inside a task branch, not to the NAS operator commands you run by hand. For the host operator workflow, the targets in the table above still behave the same way.

Next

llms.txt