mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
1728a4c0af
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
328 lines
13 KiB
YAML
328 lines
13 KiB
YAML
# =============================================================================
|
|
# Turnstone — local cluster stack (docker compose)
|
|
#
|
|
# Clone the repo and run:
|
|
#
|
|
# docker compose up
|
|
#
|
|
# That builds one image and brings up a complete, console-visible cluster:
|
|
# PostgreSQL + console + Caddy + channel gateway + 10 server nodes (node-1…10).
|
|
#
|
|
# Dashboard: https://localhost:8443 (Caddy's local CA — trust it once)
|
|
#
|
|
# 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). Trust Caddy's root once:
|
|
# docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt
|
|
#
|
|
# It works out of the box with INSECURE dev defaults (see the secret/password
|
|
# values below) so there's nothing to configure first. A .env file still
|
|
# overrides any value. For a real deployment use the bundled production stack
|
|
# at turnstone/deploy/compose.yaml — it pulls released images from ghcr.io and
|
|
# requires you to set real secrets.
|
|
#
|
|
# Bring your own LLM: nodes boot without one and show up in the console
|
|
# immediately. Add model backends (OpenAI / Anthropic / local vLLM) from the
|
|
# console UI's Models tab, or point LLM_BASE_URL / OPENAI_API_KEY (below) at an
|
|
# OpenAI-compatible endpoint.
|
|
#
|
|
# Fewer nodes (lighter machines):
|
|
# docker compose up postgres console caddy channel node-1 node-2 node-3
|
|
#
|
|
# Join a bare-metal host: Postgres is published on 127.0.0.1:5432, so a
|
|
# turnstone-server running directly on this machine (e.g. to use a local GPU)
|
|
# can join the same cluster. Keep the secret + connection settings in
|
|
# ~/.config/turnstone/config.toml (chmod 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"
|
|
# api_key = "dummy"
|
|
# then run (node identity isn't a secret, so it stays on the command line):
|
|
# TURNSTONE_NODE_ID=host-1 TURNSTONE_ADVERTISE_URL=http://host.docker.internal:8080 \
|
|
# turnstone-server --host 0.0.0.0 --port 8080
|
|
# It registers in Postgres and the console reaches it back via host.docker.internal.
|
|
# =============================================================================
|
|
|
|
name: turnstone
|
|
|
|
networks:
|
|
turnstone-net:
|
|
driver: bridge
|
|
|
|
volumes:
|
|
turnstone-data:
|
|
workspace:
|
|
postgres-data:
|
|
caddy-data:
|
|
caddy-config:
|
|
searxng-cache:
|
|
|
|
# -- Shared values (scalar anchors) -------------------------------------------
|
|
# Defined once here, referenced (*alias) by every service so the dev defaults
|
|
# can't drift. All `${VAR:-default}` values are still overridable via .env.
|
|
x-shared:
|
|
# INSECURE dev default. Every service MUST share ONE secret — the console
|
|
# mints its own service token (signed with this) to reach the nodes. Override
|
|
# TURNSTONE_JWT_SECRET in .env for anything that isn't a local sandbox.
|
|
jwt-secret: &jwt-secret "${TURNSTONE_JWT_SECRET:-dev-only-insecure-jwt-secret-change-me-for-real-deployments}"
|
|
db-backend: &db-backend "${TURNSTONE_DB_BACKEND:-postgresql}"
|
|
# All services point at the same Postgres. Node discovery REQUIRES a shared
|
|
# DB: each server registers + heartbeats into a `services` table that the
|
|
# console polls. (SQLite-per-container can't see other containers.)
|
|
db-url: &db-url "${TURNSTONE_DB_URL:-postgresql+psycopg://${POSTGRES_USER:-turnstone}:${POSTGRES_PASSWORD:-turnstone}@postgres:5432/turnstone}"
|
|
|
|
services:
|
|
# -------------------------------------------------------------------
|
|
# PostgreSQL — the shared database that ties the cluster together.
|
|
# -------------------------------------------------------------------
|
|
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}
|
|
# INSECURE dev default — override POSTGRES_PASSWORD in .env for real use.
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-turnstone}
|
|
PGDATA: /var/lib/postgresql/data
|
|
# Published on localhost so a bare-metal turnstone-server running on THIS
|
|
# host can join the cluster (see "Join a bare-metal host" in the header).
|
|
# Bound to 127.0.0.1 by default; set POSTGRES_BIND=0.0.0.0 to let another
|
|
# machine connect — but set a real POSTGRES_PASSWORD first, or you'll expose
|
|
# a database with the insecure default password to your network.
|
|
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: 2G
|
|
restart: unless-stopped
|
|
|
|
# -------------------------------------------------------------------
|
|
# turnstone-console — cluster dashboard. Reach it ONLY through Caddy at
|
|
# https://localhost:8443 (see the caddy service below).
|
|
#
|
|
# The console port (8090) is deliberately 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 internally, so the cap is gone.
|
|
#
|
|
# The single `build:` here produces the turnstone:local image every other
|
|
# service reuses. extra_hosts lets the console reach a bare-metal server
|
|
# advertising http://host.docker.internal:8080 (see "Join a host" below).
|
|
# -------------------------------------------------------------------
|
|
console:
|
|
image: turnstone:local
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
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 console dashboard.
|
|
# Terminates HTTPS (Caddy's own local CA, see turnstone/deploy/Caddyfile) → console:8090.
|
|
# Dashboard over TLS: https://localhost:${CONSOLE_HTTPS_PORT:-8443}
|
|
# -------------------------------------------------------------------
|
|
caddy:
|
|
image: caddy:2.11
|
|
depends_on:
|
|
- console
|
|
ports:
|
|
- "${CONSOLE_HTTPS_PORT:-8443}:443"
|
|
# SearxNG web UI — localhost-only (it has no auth). Browse https://localhost:8444.
|
|
- "127.0.0.1:${SEARXNG_HTTPS_PORT:-8444}:8444"
|
|
volumes:
|
|
- ./turnstone/deploy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- caddy-data:/data # persist Caddy's local CA across restarts
|
|
- caddy-config:/config
|
|
networks:
|
|
- turnstone-net
|
|
restart: unless-stopped
|
|
|
|
# -------------------------------------------------------------------
|
|
# turnstone-channel — channel gateway (Discord and/or Slack).
|
|
# Runs HTTP-only with no adapters until you set a token, so it's safe
|
|
# to leave running. See docs/channels.md.
|
|
# -------------------------------------------------------------------
|
|
channel:
|
|
image: turnstone:local
|
|
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
|
|
|
|
# -------------------------------------------------------------------
|
|
# searxng — self-hosted metasearch backing the web_search tool.
|
|
# Internal-network only (no published port): nodes reach it at
|
|
# http://searxng:8080. Config (JSON output on, limiter off) lives in
|
|
# turnstone/deploy/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:
|
|
- ./turnstone/deploy/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
|
|
|
|
# ===================================================================
|
|
# Server fleet — node-1 … node-10
|
|
#
|
|
# Each node registers itself in Postgres on boot (unique
|
|
# TURNSTONE_NODE_ID + TURNSTONE_ADVERTISE_URL) and the console
|
|
# discovers it automatically — no static node list anywhere.
|
|
#
|
|
# node-1 carries the shared definition (&node / &node-env); node-2…10
|
|
# inherit it and override only their identity.
|
|
# ===================================================================
|
|
node-1: &node
|
|
image: turnstone:local
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
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: &node-env
|
|
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: node-1
|
|
TURNSTONE_ADVERTISE_URL: http://node-1: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
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 4G
|
|
restart: unless-stopped
|
|
|
|
node-2:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-2, TURNSTONE_ADVERTISE_URL: "http://node-2:8080" }
|
|
node-3:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-3, TURNSTONE_ADVERTISE_URL: "http://node-3:8080" }
|
|
node-4:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-4, TURNSTONE_ADVERTISE_URL: "http://node-4:8080" }
|
|
node-5:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-5, TURNSTONE_ADVERTISE_URL: "http://node-5:8080" }
|
|
node-6:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-6, TURNSTONE_ADVERTISE_URL: "http://node-6:8080" }
|
|
node-7:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-7, TURNSTONE_ADVERTISE_URL: "http://node-7:8080" }
|
|
node-8:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-8, TURNSTONE_ADVERTISE_URL: "http://node-8:8080" }
|
|
node-9:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-9, TURNSTONE_ADVERTISE_URL: "http://node-9:8080" }
|
|
node-10:
|
|
<<: *node
|
|
environment: { <<: *node-env, TURNSTONE_NODE_ID: node-10, TURNSTONE_ADVERTISE_URL: "http://node-10:8080" }
|