Files
mcpctl/stack/docker-compose.yml

97 lines
3.1 KiB
YAML
Raw Permalink Normal View History

services:
postgres:
image: postgres:16-alpine
container_name: mcpctl-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- mcpctl-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- mcpctl
mcpd:
image: mysources.co.uk/michal/mcpd:latest
container_name: mcpctl-mcpd
restart: unless-stopped
ports:
- "${MCPD_PORT:-3100}:3100"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
MCPD_PORT: "3100"
MCPD_HOST: "0.0.0.0"
MCPD_LOG_LEVEL: ${MCPD_LOG_LEVEL:-info}
MCPD_NODE_RUNNER_IMAGE: mysources.co.uk/michal/mcpctl-node-runner:latest
MCPD_PYTHON_RUNNER_IMAGE: mysources.co.uk/michal/mcpctl-python-runner:latest
MCPD_RATE_LIMIT_MAX: "2000"
MCPD_MCP_NETWORK: mcp-servers
depends_on:
postgres:
condition: service_healthy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- mcpctl-backup:/data/backup
networks:
- mcpctl
- mcp-servers
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:3100/healthz || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
feat(servers): persistent volumes + self-hosted web search and docs templates Instances are immutable and get recreated on any server edit, so anything an MCP server wrote to its container filesystem was lost at exactly that point. That ruled out every stateful MCP server, docs-mcp among them: its index is a SQLite file (better-sqlite3 + sqlite-vec) and it has no external-database mode, so no amount of Postgres helps. A server or template can now declare volumes. The backing store is keyed on the server, not the instance — `mcpctl-<server>-<name>` — which is the whole point: an instance-scoped claim would be destroyed precisely when the data needs to survive. On Kubernetes that is a PVC ensured in the servers namespace before the pod is created and never deleted with it; on Docker, a named volume (named, not anonymous, so `removeContainer`'s `v: true` leaves it alone). Claims are ReadWriteOnce, so volumes and replicas > 1 are mutually exclusive; validation rejects that pair instead of leaving the extra replicas unschedulable. storageClassName is omitted rather than sent empty when no class is configured — to Kubernetes those mean different things. Also fixes a pre-existing bug in the same path: seedTemplates dropped `runtime`, so every PyPI-backed template seeded from YAML silently defaulted to node and would run `npx` against a package that only exists on PyPI. `unifi-network` declares `runtime: python` and had been seeding with runtime unset. Templates added, all self-hosted and none needing an API key: - duckduckgo — no backing service at all - searxng — needs a SearXNG engine (compose profile in stack/) - docs-mcp — open-source Context7/Ref alternative, uses the new volume Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
2026-08-09 18:17:32 +01:00
# --- websearch profile -------------------------------------------------
# Opt-in: `docker compose --profile websearch up -d`.
#
# Only the SearXNG *engine* lives here — it is plain infrastructure, not an
# MCP server, so mcpctl has nothing to manage it with. The MCP servers that
# sit in front of it (`searxng`, `duckduckgo`, `docs-mcp`) are mcpctl
# resources created from templates. Needs no API key.
searxng:
image: docker.io/searxng/searxng:latest
container_name: mcpctl-searxng
profiles: ["websearch"]
restart: unless-stopped
environment:
SEARXNG_BASE_URL: http://mcpctl-searxng:8080/
SEARXNG_SECRET: ${SEARXNG_SECRET:-mcpctl-searxng-internal-only}
# The limiter guards public instances from bots and needs Valkey to work.
# This one binds no host port and is reachable only from mcp-servers.
SEARXNG_LIMITER: "false"
volumes:
- ./searxng/settings.yml:/etc/searxng/settings.yml:ro
- mcpctl-searxng-cache:/var/cache/searxng
networks:
- mcp-servers
healthcheck:
# Probes the JSON API specifically — a healthy HTML UI with `json` missing
# from search.formats is exactly the failure mode worth catching here.
test: ["CMD-SHELL", "wget -q -O /dev/null 'http://localhost:8080/search?q=ping&format=json' || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
networks:
mcpctl:
driver: bridge
mcp-servers:
name: mcp-servers
driver: bridge
# Not internal — MCP servers need outbound access for external APIs.
# Isolation enforced by not binding host ports on MCP containers.
volumes:
mcpctl-pgdata:
mcpctl-backup:
feat(servers): persistent volumes + self-hosted web search and docs templates Instances are immutable and get recreated on any server edit, so anything an MCP server wrote to its container filesystem was lost at exactly that point. That ruled out every stateful MCP server, docs-mcp among them: its index is a SQLite file (better-sqlite3 + sqlite-vec) and it has no external-database mode, so no amount of Postgres helps. A server or template can now declare volumes. The backing store is keyed on the server, not the instance — `mcpctl-<server>-<name>` — which is the whole point: an instance-scoped claim would be destroyed precisely when the data needs to survive. On Kubernetes that is a PVC ensured in the servers namespace before the pod is created and never deleted with it; on Docker, a named volume (named, not anonymous, so `removeContainer`'s `v: true` leaves it alone). Claims are ReadWriteOnce, so volumes and replicas > 1 are mutually exclusive; validation rejects that pair instead of leaving the extra replicas unschedulable. storageClassName is omitted rather than sent empty when no class is configured — to Kubernetes those mean different things. Also fixes a pre-existing bug in the same path: seedTemplates dropped `runtime`, so every PyPI-backed template seeded from YAML silently defaulted to node and would run `npx` against a package that only exists on PyPI. `unifi-network` declares `runtime: python` and had been seeding with runtime unset. Templates added, all self-hosted and none needing an API key: - duckduckgo — no backing service at all - searxng — needs a SearXNG engine (compose profile in stack/) - docs-mcp — open-source Context7/Ref alternative, uses the new volume Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
2026-08-09 18:17:32 +01:00
mcpctl-searxng-cache: