app/mcp_server.py: FastMCP (mcp SDK), streamable-http auf /mcp, statischer Bearer-Token (constant-time ASGI-Middleware), Fail-Fast ohne RAG_MCP_TOKEN. Tools rag_search (mit semester/fach/typ-Filter) + get_file_chunks. Läuft aus demselben Image wie der Ingestor und reused den Embed-Pfad → Vektoren sind garantiert kompatibel zum Ingest (der offizielle qdrant-MCP-Server kann nur fastembed → Dimension-/Schema-Mismatch). app/qdrant_store.py: search_chunks (query_points + optionaler Payload-Filter) und get_chunks_by_path (scroll, nach chunk_index sortiert). app/bulk.py: Amplification-Guard — /bulk-import lehnt mit 409 ab solange ein vorheriger Bulk noch BackgroundTasks abarbeitet. docker-compose.coolify.yml: rag-mcp-Service (nicht public, externes metamcp-net statt Stack-Coupling) + Traefik-Rate-Limit-Middleware am ingestor. tests/conftest.py: Settings-env_file in Tests neutralisieren (Dev-.env darf die Suite nicht kontaminieren). 68 passed, ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
5.9 KiB
YAML
156 lines
5.9 KiB
YAML
# Coolify production stack — self-contained: qdrant + ollama + ingestor + rag-mcp.
|
|
#
|
|
# Services kommunizieren nur über das compose-interne Netz. Öffentlich ist
|
|
# allein der ingestor (Coolify-Domain → Port 8000) für den Nextcloud-Webhook.
|
|
# qdrant + ollama bleiben intern. rag-mcp ist NICHT public (keine Domain) —
|
|
# nur MetaMCP erreicht ihn über das dedizierte externe Netz "metamcp-net".
|
|
#
|
|
# Coolify rendert container_name/labels/COOLIFY_*-env/das Stack-Netz selbst.
|
|
# Hier stehen nur Extra-Labels und das, was zusätzlich nötig ist.
|
|
#
|
|
# Voraussetzungen in Coolify:
|
|
# - Pull-Credentials für gitea.jeanlucmakiola.de Registry hinterlegen
|
|
# - Externes Netz einmalig anlegen: docker network create metamcp-net
|
|
# und es in der MetaMCP-Compose ergänzen (networks: [metamcp-net])
|
|
# - Env-Vars setzen (Secrets wo markiert):
|
|
# NEXTCLOUD_WEBDAV_URL
|
|
# NEXTCLOUD_USER
|
|
# NEXTCLOUD_APP_PASSWORD (Secret)
|
|
# WEBHOOK_SECRET (Secret)
|
|
# RAG_MCP_TOKEN (Secret — Bearer-Token für MetaMCP)
|
|
# QDRANT_COLLECTION (optional, Default rag_thb_studium)
|
|
# INGEST_ROOT (optional, Default Documents/THB)
|
|
# LOG_LEVEL (optional, Default INFO)
|
|
# - Traefik-Rate-Limit am ingestor-Router aktivieren: siehe README
|
|
# ("MetaMCP & Härtung") — das Router-Binding-Label trägt die
|
|
# env-spezifische Coolify-UUID und gehört daher in die Coolify-UI,
|
|
# nicht ins versionierte Compose.
|
|
|
|
services:
|
|
qdrant:
|
|
image: qdrant/qdrant:latest
|
|
restart: unless-stopped
|
|
volumes:
|
|
- qdrant_data:/qdrant/storage
|
|
|
|
ollama:
|
|
image: ollama/ollama:latest
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ollama_data:/root/.ollama
|
|
# Konstante ~2 Cores statt Peaks über alle Host-Cores. Bewusster
|
|
# Trade-off: langsamerer Ingest, dafür predictable Last.
|
|
cpus: "2.0"
|
|
environment:
|
|
OLLAMA_NUM_PARALLEL: "1"
|
|
healthcheck:
|
|
test: ["CMD", "ollama", "list"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
|
|
# One-shot: zieht das Embed-Modell in das ollama-Volume und beendet sich.
|
|
# Idempotent — bei Redeploy mit warmem Volume nur Digest-Verifikation.
|
|
# Verhindert den Startup-Crash des Ingestors bei fehlendem Modell.
|
|
ollama-pull:
|
|
image: ollama/ollama:latest
|
|
restart: "no"
|
|
depends_on:
|
|
ollama:
|
|
condition: service_healthy
|
|
environment:
|
|
OLLAMA_HOST: "http://ollama:11434"
|
|
command: ["pull", "qwen3-embedding:0.6b"]
|
|
|
|
ingestor:
|
|
image: gitea.jeanlucmakiola.de/makiolaj/rag-ingestor:latest
|
|
restart: unless-stopped
|
|
pull_policy: always
|
|
depends_on:
|
|
qdrant:
|
|
condition: service_started
|
|
ollama:
|
|
condition: service_healthy
|
|
ollama-pull:
|
|
condition: service_completed_successfully
|
|
environment:
|
|
# Coolify-Magie: macht den Ingestor öffentlich über den Coolify-Proxy
|
|
# (inkl. Let's-Encrypt-TLS). Coolify generiert eine Domain; in der UI
|
|
# auf die echte (z.B. ingest.jeanlucmakiola.de) überschreiben. Nextcloud
|
|
# ruft nur diese öffentliche URL an — kein Coolify-Netz-Zugriff nötig.
|
|
SERVICE_FQDN_INGESTOR_8000: /
|
|
NEXTCLOUD_WEBDAV_URL: ${NEXTCLOUD_WEBDAV_URL}
|
|
NEXTCLOUD_USER: ${NEXTCLOUD_USER}
|
|
NEXTCLOUD_APP_PASSWORD: ${NEXTCLOUD_APP_PASSWORD}
|
|
OLLAMA_URL: http://ollama:11434
|
|
OLLAMA_EMBED_MODEL: qwen3-embedding:0.6b
|
|
QDRANT_URL: http://qdrant:6333
|
|
QDRANT_COLLECTION: ${QDRANT_COLLECTION:-rag_thb_studium}
|
|
WEBHOOK_SECRET: ${WEBHOOK_SECRET}
|
|
INGEST_ROOT: ${INGEST_ROOT:-Documents/THB}
|
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
# Extra-Label: definiert eine Traefik-Rate-Limit-Middleware (30 req/min,
|
|
# Burst 15) gegen Spam des öffentlichen Webhooks. Das Binding an den
|
|
# Coolify-Router (env-spezifische UUID) erfolgt in der Coolify-UI, s. README.
|
|
labels:
|
|
- "traefik.http.middlewares.rag-ratelimit.ratelimit.average=30"
|
|
- "traefik.http.middlewares.rag-ratelimit.ratelimit.period=1m"
|
|
- "traefik.http.middlewares.rag-ratelimit.ratelimit.burst=15"
|
|
expose:
|
|
- "8000"
|
|
healthcheck:
|
|
test:
|
|
- CMD
|
|
- python
|
|
- -c
|
|
- "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# MCP-Server für MetaMCP. Gleiches Image wie der ingestor (teilt den
|
|
# Embed-Pfad → identische Vektoren wie beim Ingest), nur anderer command.
|
|
# KEIN SERVICE_FQDN / kein expose → nicht public. Erreichbar ausschließlich
|
|
# von MetaMCP über das externe Netz metamcp-net. Bearer-Token als zweite
|
|
# Kontrolle hinter der Netz-Isolation.
|
|
rag-mcp:
|
|
image: gitea.jeanlucmakiola.de/makiolaj/rag-ingestor:latest
|
|
restart: unless-stopped
|
|
pull_policy: always
|
|
command: ["python", "-m", "app.mcp_server"]
|
|
depends_on:
|
|
qdrant:
|
|
condition: service_started
|
|
ollama:
|
|
condition: service_healthy
|
|
ollama-pull:
|
|
condition: service_completed_successfully
|
|
environment:
|
|
OLLAMA_URL: http://ollama:11434
|
|
OLLAMA_EMBED_MODEL: qwen3-embedding:0.6b
|
|
QDRANT_URL: http://qdrant:6333
|
|
QDRANT_COLLECTION: ${QDRANT_COLLECTION:-rag_thb_studium}
|
|
RAG_MCP_TOKEN: ${RAG_MCP_TOKEN}
|
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
# Vom MCP-Server ungenutzt, aber das geteilte Settings-Modell verlangt
|
|
# diese Felder — harmlose Platzhalter, damit die Validierung durchläuft.
|
|
NEXTCLOUD_WEBDAV_URL: http://unused
|
|
NEXTCLOUD_USER: unused
|
|
NEXTCLOUD_APP_PASSWORD: unused
|
|
WEBHOOK_SECRET: unused
|
|
networks:
|
|
- default
|
|
- metamcp-net
|
|
|
|
volumes:
|
|
qdrant_data:
|
|
ollama_data:
|
|
|
|
networks:
|
|
# Coolify injiziert sein Stack-Netz als "default" automatisch. metamcp-net
|
|
# ist ein eigens angelegtes externes Netz, dem nur rag-mcp und MetaMCP
|
|
# beitreten — qdrant/ollama/ingestor bleiben außen vor.
|
|
metamcp-net:
|
|
external: true
|
|
name: metamcp-net
|