Changelog
Operations

Self-hosting

Run the teaspill stack yourself — the five infrastructure services, your two deployed planes, and the one networking rule that bites everyone.

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.

ServiceRole
gatewayThe single entrypoint. Authenticates every request, then proxies to the internal services. Built from the teaspill gateway package.
restateThe coordination core — the durable-execution engine (Restate) that runs each agent's wakes and holds its live working state. Single-node.
postgresThe 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.
electricStreams catalog rows to your UIs as live-updating subscriptions, fed by Postgres logical replication.
durable-streamsThe 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:

VariableDefaultMeaning
POSTGRES_PASSWORDteaspillCatalog database password. Change it before exposing Postgres beyond localhost.
GATEWAY_PORT8787Host port the gateway is published on. This is the one port your clients use.
ELECTRIC_INSECUREtrueDev-only. Skips shape-API auth. Set false (and provide ELECTRIC_SECRET) before exposing Electric.
POSTGRES_PORT, RESTATE_INGRESS_PORT, DURABLE_STREAMS_PORT, …see .env.exampleHost-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:

Register a service by the address the coordinator can reach it at, from inside its container — never 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.

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.

Holding the Docker socket is root-equivalent access to the host machine — anything that can talk to the socket can start a container that mounts the whole host filesystem. Keep the executor internal, behind the gateway, running only your own agents. Do not expose it to untrusted callers, and do not use the socket-mount adapter to run hostile code. For multi-tenant or untrusted workloads, move to a boundary that does not hand out host root: rootless Docker-in-Docker, or a remote-VM adapter.

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. Set none when 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_PASSWORD from the default.
  • Set ELECTRIC_INSECURE=false and provide ELECTRIC_SECRET.
  • Mint real API keys and drop the dev bootstrap key — see Auth & API keys.
  • Set GATEWAY_JWT_SECRET if 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.