Self-hosting
Run the whole platform inside your own boundary.
Rokkhe self-hosts on infrastructure you operate. The shape we run is the shape you can run — a single VM with Compose behind a reverse proxy. Nothing in the stack calls home, and the only outbound traffic is what you configure: your model provider, and whichever integrations you connect.
#What you need
- A Linux host with Docker and the Compose plugin. One VM is enough; the reference deployment runs every service on a single machine.
- A domain pointed at the host if you want Caddy to obtain certificates automatically.
- Credentials for a model provider. Rokkhe never ships a key; the deployment uses yours.
- Outbound access to your image registry, or a mirror you populate yourself.
#The stack
Nine services come up together. Only Caddy publishes a port — everything else is reachable only on the internal networks below.
| Service | Image | Role |
|---|---|---|
| caddy | caddy:2-alpine | TLS termination and the only published port. |
| web | rokkhe-web | The console and public site. |
| api | rokkhe-api | Application API. Depends on migrate and redis. |
| auth | rokkhe-auth | Sessions and OAuth. Depends on the database and redis. |
| worker | rokkhe-scan-worker | Runs engagements and creates each scan’s sandbox. |
| timescaledb | timescale/timescaledb:latest-pg16 | Findings, cases, evidence, and run history. |
| redis | redis:7-alpine | Queues and short-lived state. |
| migrate | timescale/timescaledb:latest-pg16 | Applies migrations once, then exits. |
| egress-gateway | built locally | Optional. Constrains what a sandbox may reach. |
#Network boundaries
The stack is segmented so that compromising one tier does not reach the next. This is the part worth reading if you are assessing Rokkhe rather than deploying it.
| Network | Members | Reachable from the internet |
|---|---|---|
| edge | caddy, web, api, auth | Through Caddy only. |
| data | timescaledb, redis, migrate, api, auth, worker | No. The database has no edge route at all. |
| sandbox | worker, and each scan’s container | No, and it carries no route to data. |
The worker sits on both data and sandbox because it owns the boundary between them: it reads work from the queue and creates the container that executes against a target. A scan’s container never joins the data network, so a compromised target cannot reach your findings.
#Required configuration
Configuration lives in infra/.env. The base Compose file carries development defaults so a local stack comes up unattended; the production overlay removes them. These four have no default in production and the stack refuses to render without them.
| Variable | Meaning |
|---|---|
| POSTGRES_PASSWORD | Database password. The development default is refused. |
| DATABASE_URL | Connection string for the TimescaleDB instance. |
| PUBLIC_ORIGIN | The HTTPS origin the deployment answers on. Also becomes the site URL the web app renders. |
| AUTH_SECRET | At least 32 characters. The development placeholder is refused. |
docker-compose.yml alone brings the stack up with AUTH_SECRET set to a known placeholder, POSTGRES_PASSWORD set to rokkhe, and ROKKHE_ENV left at development. That is deliberate, so a local stack needs no setup — but it means running the base file alone on a reachable host publishes a deployment with published credentials. Always pass both files.
#Commonly set
| Variable | Default | Meaning |
|---|---|---|
| SITE_ADDRESS | :8080 | Set to your domain for automatic HTTPS. |
| IMAGE_OWNER / IMAGE_TAG | main | Which published images to pull. |
| ROKKHE_IMAGE | rokkhe-sandbox:latest | The sandbox image each scan runs in. |
| SELF_SERVE_SCANS | 1 | Whether accounts may start their own scans. |
| CADDY_LOCAL_BIND | 127.0.0.1:8080 | Loopback publish for the base stack. |
| ROKKHE_CREDENTIAL_KEYRING | unset | Encryption keyring for stored integration credentials. |
#Bringing it up
Both files, every time. The overlay is what pins ROKKHE_ENV to production, requires the four variables above, and moves Caddy onto 80 and 443.
bashcd infra
cp .env.example .env # then fill in the four required values
docker compose \
-f docker-compose.yml \
-f docker-compose.prod.yml \
pull
docker compose \
-f docker-compose.yml \
-f docker-compose.prod.yml \
up -dmigrate runs to completion before api and worker start, so the first boot applies the schema before anything serves traffic.
#Health
- /health/live — the process is alive.
- /health/startup — the backend initialized at least once.
- /health/ready — required dependencies are healthy; the edge gates traffic on this.
- /api/health — public compatibility endpoint reporting only status and readiness.
The detailed probes are internal: the edge answers 404 for /health/* because the payload names versions, environment, and dependency state. Compose healthchecks cover timescaledb, redis, api, auth, and web, so a failed dependency holds back the services that need it rather than surfacing as a broken page.
#Upgrading
bashdocker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dAn upgrade replaces the worker container, and a scan in progress does not survive it — its working directory goes with the container. Drain running engagements before deploying, or expect to restart them.
#State and backups
| Volume | Holds | Back up |
|---|---|---|
| pgdata | Findings, cases, evidence, run history, accounts. | Yes — this is the deployment. |
| caddy_data | Issued certificates. | Optional; re-issues on loss. |
| caddy_config | Caddy runtime state. | No. |
| rokkhe_config | Engine configuration written at runtime. | Optional. |
pgdata is the only volume whose loss is unrecoverable. Snapshot it with your normal database tooling rather than copying the directory from under a running Postgres.