Self-hosting
teaspill self-hosts as a small Docker Compose stack of infrastructure services plus two services you deploy yourself. This guide gets that stack running, explains what each piece does, and covers the networking and security decisions you need to get right before production.
Deployment model
There are two halves to a teaspill deployment: the infrastructure stack you run from Compose, and the two planes you deploy — the process that runs your agents, and the executor that hosts their workspaces.
your app / UI / CLI
│
│ API key (single entrypoint)
▼
┌──────────────────────────────────────────┐
│ compose stack (infrastructure) │
│ │
│ gateway ──► restate │
│ │ ├─► postgres ◄── electric │
│ │ └─► durable-streams │
└──────┼───────────────────────────────────┘
│ Restate dials your registered URLs directly
▼
┌──────────────────────────────┐
│ agent-loop service(s) │ ← you deploy these
│ executor + workspaces │ (they register through the gateway)
└──────────────────────────────┘
The gateway is the single front door: your app, your UIs, and the CLI reach teaspill only through it, and the internal services are never exposed to external callers directly. Your agent-loop service — the process running your defineAgent definitions — and your executor register themselves with the coordinator through the gateway, and from then on the coordinator dials them directly on every wake.
The two planes scale on different axes: agent-loop replicas scale with how many model conversations run at once; the executor fleet scales with workspace compute and disk demand.
The five services
The infrastructure stack is five containers, defined in docker-compose.yml. Every image tag is pinned.
| Service | Role |
|---|---|
gateway | The single entrypoint. Authenticates every request, then proxies to the internal services. Built from the teaspill gateway package. |
restate | The coordination core — the durable-execution engine (Restate) that runs each agent's wakes and holds its live working state. Single-node. |
postgres | The catalog store: the registry of every entity, plus the archive of record for archived agents. Also Electric's replication source; runs with logical replication enabled. |
electric | Streams catalog rows to your UIs as live-updating subscriptions, fed by Postgres logical replication. |
durable-streams | The history store — every timeline, delta stream, and workspace-output stream, append-only and resumable. Runs in a crash-safe, fsync-on-append mode. |
Persistent data lives in named Docker volumes (postgres_data, restate_data, durable_streams_data, electric_storage). Stopping the stack keeps them; docker compose down -v destroys them.
Configuring the stack
Every variable the Compose file reads has a working default baked in (${VAR:-default}), so an empty or missing .env still boots the whole stack. Copy the example and edit only what you need:
cp .env.example .env
The variables you are most likely to touch:
| Variable | Default | Meaning |
|---|---|---|
POSTGRES_PASSWORD | teaspill | Catalog database password. Change it before exposing Postgres beyond localhost. |
GATEWAY_PORT | 8787 | Host port the gateway is published on. This is the one port your clients use. |
ELECTRIC_INSECURE | true | Dev-only. Skips shape-API auth. Set false (and provide ELECTRIC_SECRET) before exposing Electric. |
POSTGRES_PORT, RESTATE_INGRESS_PORT, DURABLE_STREAMS_PORT, … | see .env.example | Host-published debug ports for the internal services. |
The gateway reads further environment directly for auth and CORS — those are covered in Auth & API keys. The complete per-service table is in the Configuration reference.
DATABASE_URL is not in .env.example. Compose synthesizes it for the in-network services from the Postgres credentials. Anything you run outside the Compose network — the CLI's keys command, local tests — must set it explicitly, e.g. postgresql://teaspill:teaspill@localhost:5432/teaspill?sslmode=disable.Running the stack
The primary way to run everything, including your own agent-loop and executor, is the teaspill dev command. It brings the stack up, waits for the gateway to be healthy, registers your local deployments with retry and backoff (avoiding the register-before-up race), then tails logs:
export COMPOSE_FILE=docker-compose.yml:docker-compose.overlay.yml
teaspill dev --deployment http://agent-loop:9080 --deployment http://executor:9081
The reference deployment ships as a Compose overlay — docker-compose.overlay.yml — that adds a working agent-loop and executor to the base stack. It is the canonical worked example: copy it to start your own deployment. To run it with plain Compose instead of the CLI:
# Build the service bundles first, then bring up base + overlay.
pnpm -r build
pnpm --filter @teaspill/gateway bundle
pnpm --filter @teaspill/reference-deployment bundle
docker compose -f docker-compose.yml -f docker-compose.overlay.yml up -d --build
teaspill dev --watch re-registers your deployment whenever its built output changes, so you can iterate on an agent without restarting the stack.
Networking rules
The coordinator dials the URL you register directly, from inside its own container, on every invocation — that traffic never passes back through the gateway. This makes the registration URL the single thing operators get wrong, so it has one rule:
localhost.- A service inside the Compose network registers its service name:
http://agent-loop:9080. - A service running on your host (the common case during local dev) registers
http://host.docker.internal:9080.
http://localhost:9080 will register successfully and then fail on the first wake, because localhost inside the coordinator's container is the container itself, not your host.The Compose file already adds the host.docker.internal mapping to the coordinator so this works on plain Docker Engine (Linux) as well as Docker Desktop. teaspill dev and the reference deployment default their registration URLs correctly for their context.
Registration and invocation are two different network paths. When a service registers, the coordinator only records the URL — it does not dial it, so any syntactically valid URL is accepted. The failure surfaces later: on the first wake, the coordinator opens a connection to that URL from inside its own container's network namespace. localhost there resolves to the coordinator container, which is not running your agent, so the call fails. teaspill deliberately does no URL rewriting — a "helpful" rewrite of loopback addresses is a well-known source of confusing, environment-dependent bugs, so the rule is explicit instead.
The internal services also publish their ports to your host (5432, 8080, 4437, and so on) so you can psql or curl them while debugging. Treat those as a localhost-only debugging surface — the production access path is the gateway, and nothing you build should depend on the internal ports being reachable from outside the host.
The executor and the Docker socket
The default executor adapter runs each workspace in its own Docker container, and it gets access to Docker by mounting the host's Docker socket into the executor.
The workspace containers themselves are hardened, independently of the socket:
- The default workspace image is digest-pinned (
alpine:3.20@sha256:…) so every executor host materializes a byte-identical base. Pin your own images by digest too. - Network isolation is per-workspace. Choose
none(loopback only),bridge(the default — egress for tool calls), or a named network. Setnonewhen running code that must not reach the network. - Containers run with dropped capabilities, no new privileges, and memory, CPU, and process-count limits.
None of this hardens the socket holder — it hardens the workspaces. The executor process itself must be treated as trusted-as-root.
Production checklist
Before you put a deployment in front of anyone:
- Change
POSTGRES_PASSWORDfrom the default. - Set
ELECTRIC_INSECURE=falseand provideELECTRIC_SECRET. - Mint real API keys and drop the dev bootstrap key — see Auth & API keys.
- Set
GATEWAY_JWT_SECRETif you want browsers to read streams directly with read tokens. - Terminate TLS in front of the gateway (a reverse proxy or load balancer). The gateway speaks plain HTTP; put encryption at the edge.
- Keep the internal service ports unpublished on any host reachable from outside.
Scaling
The two planes you deploy scale independently and horizontally: add agent-loop replicas for more concurrent model conversations, add executor hosts for more workspace capacity. The coordinator load-balances wakes across registered replicas of the same deployment.
The infrastructure services run single-node in this stack — that is the supported self-host shape, and it is enough for real workloads. In particular Restate runs as a single node, which is the deployment shape its own backup guidance is written for.
Next steps
Once you are running, plan for recovery: Backup & restore covers what each store holds and which restore combinations come back cleanly.
Frontend integration
Spawn agents, read and live-follow their timelines, subscribe to the catalog, and mint browser read tokens with the frontend SDK.
Auth & API keys
The server-side API key that authorizes everything, the optional read token that lets browsers read streams directly, and why authorization is yours to own.