Deployment
Images
Published to Docker Hub on every release:
| Image | Role |
|---|---|
k3scat/terrarium-orchestrator |
The control plane (FastAPI, :8900). |
k3scat/terrarium-console |
The Next.js console (:3737). |
k3scat/terrarium-sandbox |
The hardened agent sandbox, including Warden. Not run directly — the orchestrator launches it per session. |
Tags: the exact version (0.1.0), the minor line (0.1), and latest.
Docker
The orchestrator needs the Docker socket to spawn sandboxes, and both processes need a shared runtime volume so the console’s proxy can reach the API.
# compose.yaml
services:
orchestrator:
image: k3scat/terrarium-orchestrator:0.1
environment:
TERRA_RUNNER: docker
TERRA_HOST: 0.0.0.0
TERRA_TOKEN: ${TERRA_TOKEN:?set a token}
TERRA_KEK: ${TERRA_KEK:?set a key-encryption key}
TERRA_IMAGE: k3scat/terrarium-sandbox:0.1
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- terrarium-data:/data
ports: ["8900:8900"]
console:
image: k3scat/terrarium-console:0.1
environment:
TERRA_URL: http://orchestrator:8900
TERRA_TOKEN: ${TERRA_TOKEN}
ports: ["3737:3737"]
depends_on: [orchestrator]
volumes:
terrarium-data:
Mounting the Docker socket gives the orchestrator container root-equivalent control of the host. That is inherent to spawning sibling containers, and it is why the orchestrator — not the sandbox — is the thing holding it. Treat the orchestrator as a trusted component and keep its API behind a token.
Single VPS, with shunt
shunt deploys Docker services to one host over ssh. A ready
manifest lives at shunt.toml: it
pulls the released images from Docker Hub, so you deploy exactly what CI built. Set host,
fill in secrets/prod.env, then:
shunt validate && shunt audit && shunt plan
shunt up
Three things about it are worth reading rather than copying — the manifest explains each in place:
- Sandboxes are not services. The manifest declares the orchestrator and the console only;
per-session sandboxes are created by the orchestrator itself. shunt leaves them alone and
reports them as orphaned, which is correct. It also means shunt never pulls the sandbox
image — do that once per release:
docker pull docker.io/k3scat/terrarium-sandbox:latest. - The orchestrator needs the host’s Docker socket, and uid 1000 cannot read it as shipped:
setfacl -m u:1000:rw /var/run/docker.sock(persist via systemd or udev). That mount is the deployment’s trust boundary — see the warning under Docker above. - Secrets are mounted as files (
mode = "file"), so the admin token stays out ofdocker inspect. Terrarium reads/run/secrets/<NAME>forTERRA_TOKEN,TERRA_KEKandANTHROPIC_API_KEY, which is the same convention Docker Compose and Swarm use — nothing about it is shunt-specific.
Behind nginx
A ready config is at
deploy/nginx/terrarium.conf.
Only the console is proxied — the orchestrator is the admin API and has no business being
routable from the internet.
Three of its settings are load-bearing, and each breaks a specific feature if dropped:
| Setting | Without it |
|---|---|
| TLS (not optional) | The auth cookie is Secure in the published image, and browsers discard a Secure cookie sent over plain http:// on anything but localhost. Login appears to succeed and leaves you logged out, with nothing in any log to explain it. |
client_max_body_size 26m |
Workspace uploads fail at nginx with a 413 the app never sees. The orchestrator accepts 25 MiB; nginx defaults to 1 MiB. |
proxy_buffering off |
nginx holds SSE events until a buffer fills, so the live transcript arrives in bursts or not at all. Terrarium sends X-Accel-Buffering: no, which nginx honours, but the config sets it explicitly rather than depending on a header someone might remove. |
If you are the only operator, skip nginx entirely. Bind the console to loopback (
publish = ["127.0.0.1:3737:3737"]) and reach it over an ssh tunnel:ssh -L 3737:127.0.0.1:3737 you@your-vps. localhost is exempt from the Secure-cookie rule, so login works, and the admin UI is never exposed at all.
Kubernetes
Set TERRA_RUNNER=k8s and the orchestrator spawns sandbox Pods through the Kubernetes API
— no Docker daemon in-cluster. Warden runs as a native sidecar (an init-container with
restartPolicy: Always), so it is up and has written its CA before the worker starts.
No reference manifests ship with this release. Terrarium was extracted from a deployment whose chart was specific to its own cluster, and publishing that would have been a template for one person’s storage class and secrets operator. Bring your own manifests; what follows is the contract they have to satisfy.
The orchestrator needs:
- A ServiceAccount with RBAC in its own namespace for
pods,pods/attach,pods/exec,persistentvolumeclaims,secretsandconfigmaps(get,list,create,patch,delete). - A ReadWriteOnce PVC mounted at
/data— event logs, the session registry, the sealed credential store, and the drained egress audit all live there. Single-writer: run the Deployment withstrategy: Recreateand one replica. - Secrets for
TERRA_TOKENandTERRA_KEK(andANTHROPIC_API_KEYif you use the key path), from whatever secrets tooling you already run. An operator that supports a reload annotation will roll the orchestrator on change. TERRA_IMAGEpointing at the sandbox image, and a pull secret if your registry is private.
Storage sizing is per-session and tunable —
TERRA_K8S_MEMORY_SIZE, TERRA_K8S_WORKSPACE_SIZE, TERRA_K8S_MEMORY_EMPTYDIR_SIZE,
TERRA_K8S_AUDIT_SIZE.
Encryption at rest: Kubernetes Secrets are base64-encoded API objects, not encrypted storage. Enable encryption at rest for the cluster datastore and keep RBAC access to sandbox Secrets limited to the Terrarium service account.
Memory mode is the biggest lever on launch latency here: mounting the per-agent RWO PVC
costs roughly 11s of volume attach per launch. The default synced mode skips the mount and
snapshots memory in and out instead. See Configuration.
Limits
Session admission defaults to 32 live sessions globally and 4 per agent. Event and egress logs default to 256 MiB per session; crossing the limit terminates the producer rather than truncating evidence. All tunable — see Configuration.