mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
1f61350545
A turnstone-server running outside the compose network ("bare-metal", e.g. a
local-GPU box) couldn't fully join: it can't resolve the in-cluster console
(console:8090) to enroll its mTLS cert, and SearxNG was unreachable for
web_search. Only Postgres was published.
Publish the console's plain-HTTP ACME endpoint (:8090) and SearxNG (:8081)
alongside Postgres, all bound via one knob TURNSTONE_HOST_IP (default 127.0.0.1
-- nothing new on the LAN; set it to the host's LAN IP for a node on another
machine). Postgres keeps honoring the legacy POSTGRES_BIND as a fallback, so
existing .env files don't break.
The node's TLS client now honors TURNSTONE_CONSOLE_URL so a bare-metal node can
point at the published ACME endpoint instead of the unreachable in-cluster name
(empty = in-cluster service discovery, unchanged).
Docs (docker.md, tls.md), the run.sh-generated .env, and the bootstrap wizard
updated to match. The advertised host is the cert's primary SAN and the console
collector dials it back, so mTLS hostname verification holds both ways.
282 lines
12 KiB
Markdown
282 lines
12 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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-1`…`node-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):
|
|
|
|
```bash
|
|
docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt
|
|
```
|
|
|
|
Create your first admin user (any node works — they share one database):
|
|
|
|
```bash
|
|
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`):
|
|
|
|
```bash
|
|
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):
|
|
|
|
```toml
|
|
[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:
|
|
|
|
```bash
|
|
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` first** — `TURNSTONE_HOST_IP` exposes the database (and every
|
|
user account + API-token hash in it), the console API, and the unauthenticated
|
|
SearxNG to your network.
|
|
|
|
## Production stack
|
|
|
|
For a real deployment use the bundled stack, which pulls released images
|
|
instead of building:
|
|
|
|
```bash
|
|
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 (`turnstone-bootstrap` generates them):
|
|
|
|
```bash
|
|
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](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:
|
|
|
|
```bash
|
|
docker compose -f turnstone/deploy/compose.yaml -f deploy/docker-compose.tls.yml up
|
|
```
|
|
|
|
See [tls.md](tls.md) for details.
|
|
|
|
## Configuration
|
|
|
|
Everything is configured with environment variables in `.env` (copy from
|
|
[`.env.example`](../.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](pgbouncer.md) (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](channels.md) for app setup.
|
|
|
|
### Web search (SearxNG)
|
|
|
|
The `web_search` tool for local/vLLM models is backed by a self-hosted
|
|
[SearxNG](https://searxng.org) 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`](../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-bootstrap`):
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
docker compose down # stop and remove containers
|
|
docker compose down -v # also remove volumes (database, certs)
|
|
```
|