* feat(console): reactive node discovery via PG LISTEN/NOTIFY dispatcher Add a console-side `NotifyDispatcher` that holds a dedicated PostgreSQL `LISTEN` connection and fans wake-ups out to per-channel handlers on a separate dispatch thread. Cluster collector subscribes to a new `services` channel and runs node discovery reactively — new-node / graceful-deregister visibility drops from up-to-60 s to ~500 ms on Postgres, with the 60 s discovery loop retained as the backstop for crash-shaped node loss (NOTIFY only fires on real writes). Storage layer gains a uniform `notify` / `listen` API: - PostgreSQL: real `pg_notify` / `LISTEN` on a dedicated session-mode connection that bypasses pgbouncer (mandatory: pgbouncer is required in transaction-pool mode per docs, which is incompatible with LISTEN). - SQLite: in-process fan-out + synthetic-sweep fallback so consumer code is identical across backends. `TURNSTONE_DB_LISTEN_URL` (or `[database] listen_url` in config.toml) points the dispatcher's connection direct-to-Postgres. Defaults to the main DB URL when unset. Migration 053 installs the `services_notify` trigger; it filters heartbeat-only UPDATEs in-trigger so the 30 s × N-nodes heartbeat tick stays quiet, while INSERT, DELETE, and url/metadata-changing UPDATE still fire. Dispatcher detail: - Two threads: listener (drains stream → bounded queue) and dispatch (invokes handlers under exception suppression). Same-channel notifies coalesce per dispatch batch so an N-node deploy burst is one `_discover_nodes` per channel. - Reconnect uses exponential backoff (1 s → 30 s cap). After any successful reopen — whether the prior failure was a stream-poll error or a connect / initial-LISTEN error — one synthetic Notify with payload="reconcile" is enqueued per channel so handlers re-read on the same code path they use for real events. Future consumers (ConfigStore live reload, scheduler immediate dispatch, audit live-tail) plug in by adding their channel to the dispatcher's construction list. Tests: 22 dispatcher tests (incl. reconnect + coalescing under stub storage), 7 SQLite notify-stream tests, 4 PG-gated trigger-filter tests, 4 collector wire-in tests. All pass; ruff + mypy clean. * fix(notify): address Copilot review on #505 - _sqlite.py: SQLiteBackend.listen() now de-dupes channel names via dict.fromkeys before constructing the stream — duplicates would otherwise register the queue twice and double-deliver each notify. - _sqlite.py: SQLiteBackend.listen() gains a keyword-only sweep_interval parameter (defaults to _SQLITE_NOTIFY_SWEEP_INTERVAL) — matches what the comment at the constant already promised, and lets future consumers without their own polling timer pick a tighter cadence without reaching into private stream attributes. - _sqlite.py: documented the `except queue.Empty: pass` end-of-drain termination so it's not mistaken for swallowing an unexpected error. - _postgresql.py: docstring referenced :func:`_pg_listen_url` which was renamed to _resolve_pg_listen_url during PR development. - notify_dispatcher.py: module docstring referenced a non-existent _bootstrap_console_subsystem; wire-in is at console/server.py::main. Refuted (no change, false positives from github-code-quality bot): - 4× "Statement has no effect" on Protocol-method `...` ellipsis bodies (idiomatic Python Protocol declaration, not dead code). - 2× "Mixed import style" in tests — `import ... as nd_mod` is intentional to allow attribute assignment for monkey-patching the module's `_RECONNECT_BACKOFF_INITIAL` constant inside try/finally.
6.6 KiB
Docker Deployment
Docker Compose stack for running the full turnstone platform.
Quick Start
# Copy and edit environment config
cp .env.example .env
# Full stack (needs an LLM API on the host)
docker compose up
Console dashboard: http://localhost:8090
See also: Deployment diagram
Services
| Service | Port | Profile | Description |
|---|---|---|---|
server |
8080 | default | Web UI + chat workstreams + LLM |
console |
8090 | default | Cluster dashboard |
channel |
— | production | Channel gateway (Discord and/or Slack adapters) |
server-1…server-10 |
— | cluster | 10-node server fleet (PostgreSQL required) |
Profiles
Default (no flag) — starts server and console. Requires an OpenAI-compatible LLM API running on the host (default: http://localhost:8000/v1).
docker compose up
Production — adds PostgreSQL and the channel gateway. Requires POSTGRES_PASSWORD and (for Discord) TURNSTONE_DISCORD_TOKEN:
docker compose --profile production up
Cluster — 10-node server fleet sharing PostgreSQL. Access all nodes via the console at :8090. Requires POSTGRES_PASSWORD:
docker compose --profile cluster up
Configuration
All configuration is via environment variables in .env (copy from .env.example):
LLM Backend
| Variable | Default | Description |
|---|---|---|
LLM_BASE_URL |
http://host.docker.internal:8000/v1 |
OpenAI-compatible API URL |
OPENAI_API_KEY |
dummy |
API key (dummy for local servers) |
TAVILY_API_KEY |
— | Web search API key (only needed for local/vLLM models; Anthropic and OpenAI search models use native search) |
Server
| Variable | Default | Description |
|---|---|---|
SERVER_PORT |
8080 |
Host port mapping |
SKIP_PERMISSIONS |
— | Set to any value to auto-approve all tools |
Console
| Variable | Default | Description |
|---|---|---|
CONSOLE_PORT |
8090 |
Host port mapping |
Auth
Auth is always enabled. TURNSTONE_JWT_SECRET is required.
| Variable | Default | Description |
|---|---|---|
TURNSTONE_JWT_SECRET |
— | Secret key for signing JWTs (required) |
Database
| Variable | Default | Description |
|---|---|---|
TURNSTONE_DB_BACKEND |
sqlite |
Storage backend: sqlite or postgresql |
TURNSTONE_DB_URL |
— | Database URL (e.g. postgresql+psycopg://user:pass@postgres:5432/turnstone). For SQLite, defaults to /data/.turnstone.db |
TURNSTONE_DB_LISTEN_URL |
(falls back to TURNSTONE_DB_URL) |
Direct-to-PostgreSQL URL for the console's dedicated LISTEN connection. Set this when TURNSTONE_DB_URL points at PgBouncer in transaction pooling mode — LISTEN is session state and the transaction-pooled connection can't hold it. See pgbouncer.md. |
TURNSTONE_DB_POOL_SIZE |
2 |
PostgreSQL connection pool size per process (default: 2 base + 3 overflow = 5 max) |
POSTGRES_USER |
turnstone |
PostgreSQL container username (used in default TURNSTONE_DB_URL for cluster/channel) |
POSTGRES_PASSWORD |
— | PostgreSQL container password (required for production and cluster profiles) |
The database stores workstream history, user accounts, and API tokens. When using JWT auth, a database backend is required for user storage.
Upgrading from <1.3.0a4: Earlier versions used
DB_BACKENDandDATABASE_URLin.env, whichcompose.yamlmapped to theTURNSTONE_-prefixed names internally. These short aliases have been removed. RenameDB_BACKEND→TURNSTONE_DB_BACKENDandDATABASE_URL→TURNSTONE_DB_URLin your.envfile.
Large clusters: Each turnstone process maintains a small connection pool (5 max). At hundreds of nodes this adds up — use PgBouncer in transaction pooling mode between turnstone and PostgreSQL.
First-time setup: After deploying with auth enabled, create an initial admin user by running
turnstone-admin create-userinside the container:docker compose exec server turnstone-admin create-user --username admin --name "Admin"You will be prompted to set a password. Use it to log in via the UI or SDK, then create additional users through the admin API. Pass
--token --scopes read,write,approveto also generate an initial API token.
Channel Gateway
| Variable | Default | Description |
|---|---|---|
TURNSTONE_DISCORD_TOKEN |
— | Discord bot token (required to enable Discord adapter) |
TURNSTONE_DISCORD_GUILD |
0 |
Restrict to a single Discord guild (0 = all guilds) |
TURNSTONE_SLACK_TOKEN |
— | Slack Bot User OAuth token xoxb-… (required to enable Slack adapter) |
TURNSTONE_SLACK_APP_TOKEN |
— | Slack App-Level token xapp-… (required with TURNSTONE_SLACK_TOKEN) |
TURNSTONE_SLACK_CHANNELS |
— | Comma-separated Slack channel IDs to allow (empty = all) |
TURNSTONE_SLACK_SLASH_COMMAND |
/turnstone |
Slash command registered in the Slack app |
The channel service runs in the production profile. When
TURNSTONE_DISCORD_TOKEN or the Slack pair is set the gateway starts the
corresponding adapter; both can run in one process. See
Channel Integrations for platform app setup and user
account linking.
Scaling
For multi-node testing, use the cluster profile which provides 10 server instances with unique node IDs (node-1 through node-10), resource limits, and shared PostgreSQL:
POSTGRES_PASSWORD=secret docker compose --profile cluster up
The default server also runs alongside the cluster nodes (11 total). All nodes are accessible via the console dashboard at :8090.
For production clusters beyond ~50 nodes, add PgBouncer between turnstone services and PostgreSQL. See PgBouncer Connection Pooling for Docker Compose and Helm configuration.
Volumes
| Volume | Mount | Purpose |
|---|---|---|
turnstone-data |
/data |
SQLite database (.turnstone.db) |
Building
The image uses a multi-stage Dockerfile:
# Build all services
docker compose build
# Rebuild without cache
docker compose build --no-cache
All entry points are installed in a single image: turnstone,
turnstone-server, turnstone-console, turnstone-channel,
turnstone-admin, turnstone-eval, and turnstone-bootstrap.
Cleanup
# Stop and remove containers
docker compose down
# Stop, remove containers and volumes
docker compose down -v