Deployment

  1. Images
  2. Docker
  3. Single VPS, with shunt
    1. Behind nginx
  4. Kubernetes
  5. Limits

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 of docker inspect. Terrarium reads /run/secrets/<NAME> for TERRA_TOKEN, TERRA_KEK and ANTHROPIC_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, secrets and configmaps (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 with strategy: Recreate and one replica.
  • Secrets for TERRA_TOKEN and TERRA_KEK (and ANTHROPIC_API_KEY if 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_IMAGE pointing 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.