docs(deploy): make deploy/ describe the deployment that actually exists (#8)

Co-authored-by: claude <[email protected]>
This commit was merged in pull request #8.
This commit is contained in:
2026-09-10 21:45:01 -04:00
committed by claude
parent 254c4df71d
commit 053884f9fb
4 changed files with 135 additions and 250 deletions
+106 -93
View File
@@ -1,106 +1,119 @@
# Hosting stack for a docs MCP server.
# crop-chem-docs service block to MERGE into Drawbar's parent compose
# file at /home/justin/drawbar/drawbar-backend/docker-compose.yml on
# trashpanda (10.10.1.65).
#
# Replace <product> below with your product name on first deploy.
# Volumes: usage logs are mounted to a host path so they survive
# Watchtower-driven container recreates.
# This is NOT a standalone stack — do not `docker compose up` this file
# on its own. The MCP is one service inside the Drawbar backend stack,
# where it is reached over the internal docker network as
# `chem-mcp:8080` by drawbar-backend-api (CHEM_MCP_BASE_URL). Its tools
# land in the advisor's catalog under the `chem:` prefix via the
# mcp_client multiplex. Sibling seed-mcp sits alongside it.
#
# This template assumes a reverse proxy / Cloudflare Tunnel terminates
# TLS in front of port 8000. Adjust if your infra differs.
# Keep this block in sync with the parent compose. It is a copy of what
# actually runs, so that this repo's deploy/ documents reality rather
# than an aspiration.
services:
# The MCP server. Watchtower auto-pulls on :latest changes.
<product>-docs-mcp:
image: <registry>/<owner>/<product>-docs-mcp:latest
container_name: <product>-docs-mcp
restart: unless-stopped
ports:
- "8000:8000"
# crop-chem-docs — ~4.1K US row-crop pesticide / herbicide labels
# (EPA PPLS + Bayer), ~219K chunks. The advisor consults it for label
# rates, REI/PHI and rotation restrictions. Chroma + BM25 indexes are
# baked into the image, so there's no cold-start corpus build, no DB
# and no auth.
chem-mcp:
# :latest, NOT a corpus- tag. Watchtower only re-pulls the tag the
# container is already running, and corpus-YYYY.MM.DD tags are minted
# once and never re-pushed — so pinning one while keeping the
# watchtower label makes the opt-in silently inert. That is how prod
# sat on the May 2026 corpus through two refreshes until 2026-09-10
# (Drawbar/drawbar-backend#339). To freeze a snapshot deliberately,
# pin the corpus tag AND drop the watchtower label.
image: git.jpaul.io/justin/crop-chem-docs:latest
environment:
PRODUCT_NAME: "<product>"
PRODUCT_DOCS_URL: "https://docs.example.com"
# Streamable-HTTP transport. Stateless mode is required for
# production: clients don't lose sessions when Watchtower
# recreates the container.
MCP_TRANSPORT: streamable-http
MCP_HOST: 0.0.0.0
MCP_PORT: "8000"
# If you run MetaMCP or another gateway in front and reach
# this container via its compose DNS name (e.g. <product>-docs-mcp:8000),
# add that hostname here. "*" disables the rebind check entirely.
MCP_ALLOWED_HOSTS: "<product>-docs-mcp,localhost,127.0.0.1"
# Phase 6 — reranker sidecar (jina-reranker-v2-base via llama.cpp).
RERANK_URL: http://<product>-rerank:8080
RERANK_POOL: "200"
RERANK_TIMEOUT: "30"
# Phase 8 — hybrid retrieval (BM25 + dense + RRF). Set true
# only after the eval harness shows the dense-only path
# missing technical-term queries that BM25 catches.
HYBRID_SEARCH: "true"
# Phase 10 — usage telemetry.
USAGE_LOG_DIR: /app/var/logs
USAGE_LOG_KEEP_DAYS: "90"
volumes:
# Usage logs persist across container recreates.
- ./<product>-docs-mcp-logs:/app/var/logs
depends_on:
- <product>-rerank
MCP_PORT: "8080"
# DNS-rebinding protection rejects any Host header that isn't in
# its (empty by default) allowlist, with a 421. On an internal
# docker network the caller's Host is `chem-mcp:8080` — exactly
# what gets rejected. Safe to disable here: the container is
# `expose`d only, never published to a host port, so it is only
# reachable from inside the compose network.
#
# NOTE: this is the only knob this server has for it. There is no
# MCP_ALLOWED_HOSTS — the code does not read such a variable.
MCP_DISABLE_DNS_REBINDING_PROTECTION: "1"
# Query-time embeddings hit Ollama. Drawbar's own `ollama` compose
# service is commented out, so the image default
# (OLLAMA_URL=http://ollama:11434) does NOT resolve in this stack —
# this override is load-bearing, not cosmetic. Without it every
# search_docs call fails to embed its query.
OLLAMA_URL: ${CHEM_OLLAMA_URL:-http://host.docker.internal:11434}
EMBED_MODEL: ${CHEM_EMBED_MODEL:-nomic-embed-text}
# Not set here on purpose — these come from the image's ENV
# defaults (see Dockerfile) and are correct for this stack:
# PRODUCT_NAME=crop_chem
# HYBRID_SEARCH=true
# RERANK_URL=http://llama-rerank:8080
# Override any of them here if the stack's service names differ.
# Hybrid + rerank is the eval-validated config (MRR 0.672 vs 0.544
# for BM25 alone; see eval/results/with_rerank.md). Hybrid WITHOUT
# rerank is worse than BM25 alone — don't ship that combination.
extra_hosts:
- "host.docker.internal:host-gateway"
expose:
- "8080"
restart: unless-stopped
labels:
# Watchtower polls *only* containers with this label set true.
# Watchtower auto-pulls :latest on push from CI. The label is
# required because the Drawbar stack's watchtower runs in
# label-mode (WATCHTOWER_LABEL_ENABLE=true); it polls every 60s.
com.centurylinklabs.watchtower.enable: "true"
networks:
- mcp
# Reranker sidecar — llama.cpp serving jina-reranker-v2-base.
# Requires GPU access; adjust runtime/devices for your hardware.
<product>-rerank:
image: ghcr.io/ggml-org/llama.cpp:server-cuda
container_name: <product>-rerank
restart: unless-stopped
# Mount the GGUF model from the host. Download from huggingface
# (gguf-org/jina-reranker-v2-base-multilingual-GGUF) first.
volumes:
- /path/to/models:/models:ro
command: >
--model /models/jina-reranker-v2-base.Q8_0.gguf
--reranking
--host 0.0.0.0
--port 8080
--n-gpu-layers 99
--ctx-size 4096
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
networks:
- mcp
# Watchtower — auto-pulls :latest on push.
# Only watches containers labeled `com.centurylinklabs.watchtower.enable=true`.
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
WATCHTOWER_POLL_INTERVAL: "300" # 5 min
WATCHTOWER_LABEL_ENABLE: "true"
WATCHTOWER_CLEANUP: "true" # remove old images after pull
# If your registry requires auth, mount a docker config:
# volumes:
# - ./registry-auth.json:/config.json:ro
networks:
- mcp
# ─── llama-rerank ────────────────────────────────────────────────────
#
# The reranker is a SHARED sidecar (chem-mcp and seed-mcp both use it),
# and it is not declared in the parent compose — it runs as a standalone
# container. See deploy/rerank-docker.md for how to stand it up.
#
# The gotcha: it must be attached to the `drawbar-backend_default`
# network or `RERANK_URL=http://llama-rerank:8080` resolves via public
# DNS to an unrelated IP and connection-refuses. The MCP then falls back
# to dense+BM25 SILENTLY — retrieval quality craters with no error in
# the log. This bit chem-mcp through 2026-05-25. To fix or re-fix:
#
# docker network connect drawbar-backend_default llama-rerank
#
# It is idempotent, but it does NOT survive the container being
# recreated. Better: bring llama-rerank into the parent compose so the
# attachment is declarative.
#
# Confirm it is actually engaged — the header says which mode ran:
#
# docker exec drawbar-backend-chem-mcp-1 python -c \
# "from docs_mcp.server import search_docs; \
# print(search_docs('soybean herbicide for waterhemp', k=2))"
#
# Expect `mode=hybrid-rrf+rerank`. If it reads `mode=hybrid-rrf`, the
# sidecar is unreachable and you are serving degraded results.
networks:
mcp:
driver: bridge
# ─── verifying a deploy ──────────────────────────────────────────────
#
# docker exec drawbar-backend-chem-mcp-1 python -c \
# "from docs_mcp.server import corpus_status; print(corpus_status())"
#
# Expect the label/chunk counts and the active feature flags. To confirm
# the transport itself, from the api container:
#
# docker exec drawbar-backend-api-1 python -c \
# "import urllib.request, json; \
# req=urllib.request.Request('http://chem-mcp:8080/mcp', \
# data=json.dumps({'jsonrpc':'2.0','id':1,'method':'initialize', \
# 'params':{'protocolVersion':'2025-06-18','capabilities':{}, \
# 'clientInfo':{'name':'smoke','version':'0'}}}).encode(), \
# headers={'Content-Type':'application/json', \
# 'Accept':'application/json, text/event-stream'}); \
# print(urllib.request.urlopen(req, timeout=15).status)"
#
# Expect 200.