Skip to content

Latest commit

 

History

History
284 lines (219 loc) · 12.7 KB

File metadata and controls

284 lines (219 loc) · 12.7 KB

Docker Deployment

Turnstone ships two Docker Compose stacks:

Stack File Use it for
Dev cluster compose.yaml (repo root) Clone-and-run. Builds locally, zero config, full 10-node cluster.
Production turnstone/deploy/compose.yaml Pip/pipx installs. Pulls released images from ghcr.io, requires real secrets.

Quick start — local cluster

git clone https://github.com/turnstonelabs/turnstone
cd turnstone
docker compose up

That builds one image and brings up the whole stack: PostgreSQL, the console, Caddy, the channel gateway, and 10 server nodes (node-1node-10). No .env is required — it ships with insecure dev defaults so it just works.

Open the dashboard at https://localhost:8443. It's served by Caddy with its own local CA, so trust the root certificate once (or click through the browser warning):

docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt

Create your first admin user (any node works — they share one database):

docker compose exec node-1 turnstone-admin create-user --username admin --name "Admin"

Bring your own LLM

Nodes boot without an LLM and appear in the console immediately. Add real model backends (OpenAI, Anthropic, or a local/vLLM endpoint) from the console UI's Models tab. To set a node's bootstrap default instead, point LLM_BASE_URL / OPENAI_API_KEY at an OpenAI-compatible endpoint in .env.

Fewer nodes

Ten nodes is heavy on a laptop. Start a subset by naming the services (always include postgres, console, and caddy):

docker compose up postgres console caddy channel node-1 node-2 node-3

Why HTTPS-only?

The console's plain-HTTP port (8090) is not published to the host. A plain HTTP/1.1 origin caps the browser at 6 connections, which starves the dashboard's per-pane SSE streams. Caddy serves the browser over HTTP/2 (multiplexed) and proxies to console:8090 on the internal network, so the cap is gone. Everything goes through https://localhost:8443.

Join a bare-metal host

PostgreSQL, the console's ACME endpoint (:8090), and SearxNG (:8081) are published on 127.0.0.1, so a turnstone-server running directly on the same machine — for example to use a local GPU — can join the same cluster (enrolling its mTLS cert and running web_search) and show up in the console alongside the containerized nodes.

Put the secret and connection settings in ~/.config/turnstone/config.toml (secrets belong in this file, not the process environment — keep it 0600, the loader warns otherwise):

[auth]
jwt_secret = "dev-only-insecure-jwt-secret-change-me-for-real-deployments"

[database]
backend = "postgresql"
url = "postgresql+psycopg://turnstone:turnstone@localhost:5432/turnstone"

[api]
base_url = "http://localhost:8000/v1"   # your local model endpoint
api_key = "dummy"

Then start the server. The node identity isn't a secret, so it stays on the command line:

chmod 600 ~/.config/turnstone/config.toml
TURNSTONE_NODE_ID=host-1 \
  TURNSTONE_ADVERTISE_URL=http://host.docker.internal:8080 \
  TURNSTONE_CONSOLE_URL=http://localhost:8090 \
  TURNSTONE_SEARXNG_URL=http://localhost:8081 \
  turnstone-server --host 0.0.0.0 --port 8080

The host server registers itself in PostgreSQL; the console reaches it back via host.docker.internal. TURNSTONE_CONSOLE_URL points the node at the console's published ACME endpoint so it can enroll its mTLS certificate (needed only when the cluster runs mTLS; harmless otherwise), and TURNSTONE_SEARXNG_URL points web_search at the published SearxNG. The jwt_secret and DB credentials above are the dev-stack defaults — match whatever you set in .env if you changed them.

To let a server on a different machine join, start the stack with TURNSTONE_HOST_IP=<this host's LAN IP> — that binds PostgreSQL, the console ACME endpoint, and SearxNG to that interface. Then on the remote box set the three URLs above to that IP, and set TURNSTONE_ADVERTISE_URL to the remote box's own IP (the address the console dials back). Set a strong POSTGRES_PASSWORD firstTURNSTONE_HOST_IP exposes the database (and every user account + API-token hash in it), the console API, and the unauthenticated SearxNG to your network.

To run the bare-metal node as a hardened, persistent service instead of by hand, use the systemd units in deploy/systemd/.

Production stack

For a real deployment use the bundled stack, which pulls released images instead of building:

docker compose -f turnstone/deploy/compose.yaml up

It's the same shape as the dev stack — Caddy-fronted console, channel, and a PostgreSQL all share one database so the console discovers the node — but it pulls released images, runs a single server node, and has no baked-in secrets. Set these in .env first (generate with openssl rand -hex 32):

TURNSTONE_JWT_SECRET=<python -c "import secrets; print(secrets.token_hex(32))">
POSTGRES_PASSWORD=<a strong password>

The dashboard is at https://localhost:8443 (Caddy, same as the dev stack); the console's HTTP port isn't published. For a real domain and a publicly trusted cert, edit turnstone/deploy/Caddyfile to point Caddy at Let's Encrypt (see tls.md). Pin the image with TURNSTONE_IMAGE_TAG (default: latest).

mTLS

Layer the TLS overlay on the production stack to enable mutual TLS between services. A bootstrap container creates a CA and every service auto-provisions certs via the console's ACME endpoint:

docker compose -f turnstone/deploy/compose.yaml -f deploy/docker-compose.tls.yml up

See tls.md for details.

Configuration

Everything is configured with environment variables in .env (copy from .env.example). The dev stack needs none of them — they're overrides.

LLM backend

Variable Default Description
LLM_BASE_URL http://host.docker.internal:8000/v1 Bootstrap OpenAI-compatible API URL (real backends go in the UI)
OPENAI_API_KEY dummy API key (dummy for local servers)
TURNSTONE_SEARXNG_URL http://searxng:8080 SearxNG URL for the web_search tool (local/vLLM models only; Anthropic/OpenAI use native search). Defaults to the bundled searxng service; set to an external instance's URL. To turn web search off, clear tools.searxng_url in the admin Settings tab.
SEARXNG_IMAGE_TAG latest Tag for the bundled searxng/searxng image
MODEL Override the default model alias

Auth & database

Variable Default (dev / prod) Description
TURNSTONE_JWT_SECRET insecure default / required JWT signing secret. Every service must share one value.
TURNSTONE_DB_BACKEND postgresql sqlite or postgresql. Multi-node discovery requires postgresql.
TURNSTONE_DB_URL bundled Postgres SQLAlchemy URL. Override to use an external database.
POSTGRES_USER turnstone PostgreSQL username
POSTGRES_PASSWORD turnstone / required PostgreSQL password
POSTGRES_MAX_CONNECTIONS 300 max_connections for the bundled Postgres

Discovery needs a shared database. Each server registers and heartbeats into a services table that the console polls. All services in these stacks point at the same PostgreSQL by default; SQLite-per-container can't see other containers.

Large clusters: each process keeps a small pool (5 max). Beyond ~50 nodes, put PgBouncer (transaction pooling) between turnstone and PostgreSQL.

Ports

Both stacks publish Caddy (dashboard) and PostgreSQL; the dev stack additionally publishes the console's ACME endpoint and SearxNG on localhost so a bare-metal node can enroll its cert and run web_search. Everything else is reached through Caddy or proxied by the console:

Variable Default Description
CONSOLE_HTTPS_PORT 8443 Host port for Caddy (dashboard HTTPS)
SEARXNG_HTTPS_PORT 8444 Host port for the SearxNG UI via Caddy (dev: localhost-only; prod: opt-in)
POSTGRES_PORT 5432 Host port for PostgreSQL (for bare-metal joins)
SEARXNG_API_PORT 8081 Host port for the SearxNG API a bare-metal node's web_search dials (dev stack)
TURNSTONE_HOST_IP 127.0.0.1 Interface PostgreSQL, the console ACME endpoint, and SearxNG bind on (dev stack). Set to this host's LAN IP so a bare-metal node on another machine can reach them — set a strong POSTGRES_PASSWORD first (it also exposes the DB and the unauthenticated SearxNG to your network).
POSTGRES_BIND 127.0.0.1 Production stack (turnstone/deploy/compose.yaml) only: interface PostgreSQL binds on; set to the host's LAN IP for remote joins.

Channel gateway

Variable Default Description
TURNSTONE_DISCORD_TOKEN Discord bot token (enables the Discord adapter)
TURNSTONE_DISCORD_GUILD 0 Restrict to one guild (0 = all)
TURNSTONE_SLACK_TOKEN Slack Bot User OAuth token xoxb-…
TURNSTONE_SLACK_APP_TOKEN Slack App-Level token xapp-… (with the Slack token)

The channel runs HTTP-only with no adapters until a token is set, so it's safe to leave running. See Channel Integrations for app setup.

Web search (SearxNG)

The web_search tool for local/vLLM models is backed by a self-hosted SearxNG metasearch service, bundled into both stacks as the searxng service. The Turnstone nodes reach it over the internal docker network at http://searxng:8080 — its API port is not published. Its config — turnstone/deploy/searxng/settings.yml, mounted read-only — enables the JSON API and leaves the rate limiter off (the limiter would need a separate Valkey/Redis instance). A searxng-cache volume persists its favicon + internal cache across restarts. Commercial providers (Anthropic, OpenAI) use their own native search and never touch this service.

Point at an existing SearxNG instead of the bundled one with TURNSTONE_SEARXNG_URL, or narrow the engines via tools.searxng_engines in the admin Settings tab (e.g. duckduckgo,wikipedia).

SearxNG web UI. Caddy can also serve SearxNG's own search/Preferences UI on a dedicated port. The dev stack publishes it at https://localhost:8444 bound to localhost only; the production stack does not publish it by default (uncomment the 8444 port on the caddy service to opt in). Change the port with SEARXNG_HTTPS_PORT. SearxNG has no authentication — never bind this to a public interface, or anyone who can reach it can search through your instance.

AGPL note for operators. SearxNG is licensed AGPL-3.0. Kept on the internal network (or bound to localhost), no external user interacts with it — so the AGPL §13 (remote network interaction) source-offer obligation does not attach. If you publish SearxNG to remote users (bind its port to a public interface, or front it with your own reverse proxy) you become the operator of a network-reachable AGPL service and must offer its corresponding source; that is trivially satisfied by linking to upstream https://github.com/searxng/searxng. Turnstone's own license is unaffected: it talks to SearxNG over HTTP as a separate process (mere aggregation), not by linking.

Other

Variable Default Description
WORKSPACE_MOUNT empty volume Host directory bind-mounted at /workspace for the model to read/write
SKIP_PERMISSIONS Set to any value to auto-approve all tool calls (dev only)
MCP_CONFIG Path to an MCP server config file
TURNSTONE_IMAGE_TAG latest ghcr.io image tag — production stack

Building

Both stacks install all entry points into a single image (turnstone, turnstone-server, turnstone-console, turnstone-channel, turnstone-admin, turnstone-eval, turnstone-optimizer, turnstone-doctor):

docker compose build            # build the dev image
docker compose build --no-cache # rebuild from scratch

Volumes

Volume Purpose
postgres-data PostgreSQL data directory
turnstone-data /data per node (SQLite fallback, local state)
workspace /workspace (unless WORKSPACE_MOUNT is set)
caddy-data / caddy-config Caddy's local CA and config (dev stack)

Cleanup

docker compose down       # stop and remove containers
docker compose down -v    # also remove volumes (database, certs)