Files
turnstone/turnstone/deploy/compose.yaml
T
Patrick Buckley 1728a4c0af feat(web-search): replace Tavily/DuckDuckGo backends with self-hosted SearxNG
Drop the Tavily and DuckDuckGo (ddgs) web_search backends for a single
self-hosted SearxNG service bundled into the docker-compose stacks.

Core:
- New SearXNGClient + _format_searxng; rewrite resolve_web_search_client to
  (backend, searxng_url, searxng_engines, ...). MCP backend + oauth_user guard
  unchanged. _resolve_search_client follows storage -> toml -> env -> default
  precedence (explicit "" disables, via ConfigStore.stored_keys()).
- Drop the Tavily-era topic=finance (no SearxNG category); topic is now
  general/news.

Settings/config:
- Remove tools.tavily_api_key, get_tavily_key, $TAVILY_API_KEY, [api].tavily_key.
- Add tools.searxng_url (default http://searxng:8080) + tools.searxng_engines,
  with get_searxng_url/get_searxng_engines.

Compose + bundled config:
- Internal-only searxng service (no published API port, :ro config, /healthz
  healthcheck, persistent searxng-cache volume) in both stacks; bundle
  turnstone/deploy/searxng/settings.yml (JSON output on, limiter off).
- Caddy serves the SearxNG web UI on :8444 (dev: localhost-only; prod: opt-in).
- bootstrap extractor + wheel packaging updated.

Deps: drop the ddg extra + ddgs mypy override (regenerates uv.lock, removing the
lxml/h2/brotli transitives).

Docs: tools/docker/architecture/openshell + diagrams + config example + CHANGELOG;
docs/docker.md carries the AGPL-3.0 §13 operator note.

BREAKING: tools.web_search_backend no longer accepts "tavily"/"ddg";
tools.tavily_api_key and the ddg extra are removed. Run the bundled SearxNG (ships
in the compose stacks) or set TURNSTONE_SEARXNG_URL to an external instance.

Closes #545
2026-05-31 19:03:16 -07:00

252 lines
9.5 KiB
YAML

# =============================================================================
# Turnstone — production stack (docker compose)
#
# Same shape as the dev stack at the repo root (Caddy-fronted console, shared
# Postgres, channel gateway) but it pulls released images from ghcr.io instead
# of building, runs a single server node, and requires real secrets. Bundled
# with the turnstone wheel and written by turnstone-bootstrap.
#
# docker compose -f turnstone/deploy/compose.yaml up
#
# Dashboard: https://localhost:8443 (Caddy's local CA — trust it once; for a
# real domain/cert, edit turnstone/deploy/Caddyfile — see docs/tls.md)
#
# Access is via Caddy only — the console's plain-HTTP port is intentionally not
# published (HTTP/2 from Caddy avoids the browser's 6-connection cap on the
# dashboard's SSE streams).
#
# No baked-in secrets: set TURNSTONE_JWT_SECRET and POSTGRES_PASSWORD in .env
# (turnstone-bootstrap generates them). Pin images with TURNSTONE_IMAGE_TAG
# (default: latest).
#
# Join a bare-metal host: Postgres is published on 127.0.0.1:5432, so a
# turnstone-server on this machine can join the cluster. Put its secrets in
# ~/.config/turnstone/config.toml (chmod 0600) — see "Join a bare-metal host"
# in docs/docker.md.
#
# Enable mTLS between services with the overlay:
# docker compose -f turnstone/deploy/compose.yaml -f deploy/docker-compose.tls.yml up
# =============================================================================
name: turnstone
networks:
turnstone-net:
driver: bridge
volumes:
turnstone-data:
workspace:
postgres-data:
caddy-data:
caddy-config:
searxng-cache:
# Defined once, referenced (*alias) by every service so values can't drift.
x-shared:
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
jwt-secret: &jwt-secret "${TURNSTONE_JWT_SECRET:?Set TURNSTONE_JWT_SECRET in .env}"
db-backend: &db-backend "${TURNSTONE_DB_BACKEND:-postgresql}"
# Every service shares one Postgres. Node discovery REQUIRES a shared DB: the
# server registers + heartbeats into a `services` table the console polls.
db-url: &db-url "${TURNSTONE_DB_URL:-postgresql+psycopg://${POSTGRES_USER:-turnstone}:${POSTGRES_PASSWORD}@postgres:5432/turnstone}"
image: &image "ghcr.io/turnstonelabs/turnstone:${TURNSTONE_IMAGE_TAG:-latest}"
services:
# -------------------------------------------------------------------
# PostgreSQL — the shared database that ties the deployment together.
# Published on localhost so a bare-metal turnstone-server on this host
# can join (set POSTGRES_BIND=0.0.0.0 to let another machine connect).
# -------------------------------------------------------------------
postgres:
image: pgautoupgrade/pgautoupgrade:18-alpine
command:
- postgres
- -c
- max_connections=${POSTGRES_MAX_CONNECTIONS:-300}
- -c
- shared_buffers=128MB
environment:
POSTGRES_DB: turnstone
POSTGRES_USER: ${POSTGRES_USER:-turnstone}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
PGDATA: /var/lib/postgresql/data
ports:
- "${POSTGRES_BIND:-127.0.0.1}:${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- turnstone-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-turnstone}"]
interval: 5s
timeout: 3s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: 1G
restart: unless-stopped
# -------------------------------------------------------------------
# turnstone-console — cluster dashboard. Reach it ONLY through Caddy at
# https://localhost:8443. The console port (8090) is not published; Caddy
# serves the browser over HTTP/2 and proxies to console:8090 internally.
# extra_hosts lets it reach a bare-metal server on host.docker.internal.
# -------------------------------------------------------------------
console:
image: *image
command:
- turnstone-console
- --host=0.0.0.0
- --port=8090
environment:
TURNSTONE_JWT_SECRET: *jwt-secret
TURNSTONE_DB_BACKEND: *db-backend
TURNSTONE_DB_URL: *db-url
TURNSTONE_CONSOLE_URL: http://console:8090
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- turnstone-net
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "/usr/local/bin/healthcheck.py", "http://127.0.0.1:8090/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
# -------------------------------------------------------------------
# caddy — browser TLS for the dashboard. Terminates HTTPS → console:8090.
# Uses the bundled Caddyfile (Caddy's local CA). For a real domain/cert,
# edit turnstone/deploy/Caddyfile (see docs/tls.md).
# -------------------------------------------------------------------
caddy:
image: caddy:2.11
depends_on:
- console
ports:
- "${CONSOLE_HTTPS_PORT:-8443}:443"
# SearxNG web UI — NOT exposed by default. SearxNG has no auth, so
# publishing it is an open search proxy and triggers the AGPL-3.0 §13
# source-offer obligation (see docs/docker.md). Uncomment to opt in:
# - "127.0.0.1:${SEARXNG_HTTPS_PORT:-8444}:8444"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data # persist Caddy's local CA across restarts
- caddy-config:/config
networks:
- turnstone-net
restart: unless-stopped
# -------------------------------------------------------------------
# searxng — self-hosted metasearch backing the web_search tool.
# Internal-network only (no published port): the server reaches it at
# http://searxng:8080. Config (JSON output on, limiter off) is the bundled
# ./searxng/settings.yml, mounted read-only. Commercial models use native
# provider search and never hit this; it serves local/vLLM models. Override
# the tag with SEARXNG_IMAGE_TAG in .env.
# -------------------------------------------------------------------
searxng:
image: searxng/searxng:${SEARXNG_IMAGE_TAG:-latest}
volumes:
- ./searxng:/etc/searxng:ro
- searxng-cache:/var/cache/searxng # favicon + internal SQLite cache (survives restarts)
networks:
- turnstone-net
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/healthz"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
restart: unless-stopped
# -------------------------------------------------------------------
# turnstone-server — Web UI + chat workstreams + LLM interaction.
# Registers itself in Postgres on boot so the console can see it. Boots
# even with no model configured yet — add model backends in the console UI.
# -------------------------------------------------------------------
server:
image: *image
command:
- sh
- -c
- >-
turnstone-server
--host 0.0.0.0
--port 8080
--base-url "$${LLM_BASE_URL}"
--api-key "$${OPENAI_API_KEY}"
$${MODEL:+--model $$MODEL}
$${SKIP_PERMISSIONS:+--skip-permissions}
$${MCP_CONFIG:+--mcp-config $$MCP_CONFIG}
volumes:
- turnstone-data:/data
- ${WORKSPACE_MOUNT:-workspace}:/workspace
environment:
TURNSTONE_JWT_SECRET: *jwt-secret
TURNSTONE_DB_BACKEND: *db-backend
TURNSTONE_DB_URL: *db-url
# Bootstrap LLM defaults — real backends are configured in the console UI.
LLM_BASE_URL: ${LLM_BASE_URL:-http://host.docker.internal:8000/v1}
OPENAI_API_KEY: ${OPENAI_API_KEY:-dummy}
# web_search backend. Defaults to the bundled searxng service; point at an
# external SearxNG by setting TURNSTONE_SEARXNG_URL in .env (empty disables).
TURNSTONE_SEARXNG_URL: ${TURNSTONE_SEARXNG_URL:-http://searxng:8080}
MODEL: ${MODEL:-}
MCP_CONFIG: ${MCP_CONFIG:-}
SKIP_PERMISSIONS: ${SKIP_PERMISSIONS:-}
TURNSTONE_NODE_ID: ${TURNSTONE_NODE_ID:-node-1}
TURNSTONE_ADVERTISE_URL: ${TURNSTONE_ADVERTISE_URL:-http://server:8080}
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- turnstone-net
depends_on:
postgres:
condition: service_healthy
searxng:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "/usr/local/bin/healthcheck.py", "http://127.0.0.1:8080/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
restart: unless-stopped
# -------------------------------------------------------------------
# turnstone-channel — channel gateway (Discord and/or Slack).
# Runs HTTP-only with no adapters until you set a token. See docs/channels.md.
# -------------------------------------------------------------------
channel:
image: *image
command:
- sh
- -c
- >-
turnstone-channel
--http-host=0.0.0.0
$${TURNSTONE_DISCORD_GUILD:+--discord-guild $$TURNSTONE_DISCORD_GUILD}
environment:
TURNSTONE_JWT_SECRET: *jwt-secret
TURNSTONE_DB_BACKEND: *db-backend
TURNSTONE_DB_URL: *db-url
TURNSTONE_DISCORD_TOKEN: ${TURNSTONE_DISCORD_TOKEN:-}
TURNSTONE_DISCORD_GUILD: ${TURNSTONE_DISCORD_GUILD:-0}
TURNSTONE_SLACK_TOKEN: ${TURNSTONE_SLACK_TOKEN:-}
TURNSTONE_SLACK_APP_TOKEN: ${TURNSTONE_SLACK_APP_TOKEN:-}
TURNSTONE_CHANNEL_ADVERTISE_URL: http://channel:8091
networks:
- turnstone-net
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped