# llama-cag-n8n v2 — fully containerized CAG stack. # All ports bind to 127.0.0.1. Configure via .env (python llamacag.py setup). # GPU: add -f docker-compose.gpu.yml (or: python llamacag.py start --gpu). services: llama-server: image: ${LLAMA_IMAGE:-ghcr.io/ggml-org/llama.cpp:server} container_name: llamacag-llama restart: unless-stopped # The "local-llama" profile keeps this service in the default all-in-Docker # setup. Native mode (Apple Silicon / Metal, where Docker has no GPU # passthrough) runs llama-server on the host instead: set COMPOSE_PROFILES= # empty in .env and point LLAMA_SERVER_URL at host.docker.internal. # See `python llamacag.py start --native-llama` and docs/HARDWARE.md. profiles: ["local-llama"] # -hf downloads the GGUF from Hugging Face into the llama_models volume on # first boot (~6.5 GB for the default model), then runs offline. # q8_0 KV cache quantization halves context memory at negligible quality # cost (flash attention is auto-enabled on supported backends). # Cache-persistence guards (keep all four, in all three compose files): # --swa-full is REQUIRED for saved slot states to restore correctly on # sliding-window-attention models (all Gemma) and needs a server built on/ # after 2026-04-24 — it sizes KV by the full context on every layer (no SWA # discount), which is the RAM price of trustworthy restores. --no-mmproj, # --cache-ram 0 and --ctx-checkpoints 0 keep the multimodal projector and # the newer host-RAM cache/checkpoint paths (where the open SWA cache bugs # live) away from slot persistence. Evidence: docs/ROADMAP.md F16. command: > -hf ${LLAMA_MODEL:-google/gemma-4-12B-it-qat-q4_0-gguf} --ctx-size ${LLAMA_CTX_SIZE:-65536} --parallel ${CAG_SLOTS:-1} --cache-type-k ${LLAMA_CACHE_TYPE_KV:-q8_0} --cache-type-v ${LLAMA_CACHE_TYPE_KV:-q8_0} --slot-save-path /caches --swa-full --no-mmproj --cache-ram 0 --ctx-checkpoints 0 --host 0.0.0.0 --port 8080 --threads ${LLAMA_THREADS:--1} ${LLAMA_EXTRA_ARGS:-} environment: - LLAMA_CACHE=/models volumes: - llama_models:/models - kv_caches:/caches ports: - "127.0.0.1:${LLAMA_PORT:-8080}:8080" cag-api: build: ./api container_name: llamacag-api restart: unless-stopped environment: # Defaults to the in-Docker service; override in .env with # LLAMA_SERVER_URL=http://host.docker.internal:8080 for native mode. - LLAMA_SERVER_URL=${LLAMA_SERVER_URL:-http://llama-server:8080} - LLAMA_CTX_SIZE=${LLAMA_CTX_SIZE:-65536} - CAG_SLOTS=${CAG_SLOTS:-1} # Stack-wide answer defaults (per-request values still win). Chatbot # deployments typically pin DEFAULT_TEMPERATURE=0.0 for deterministic, # extractive answers — see "Running it as a dedicated chatbot" in README. - DEFAULT_TEMPERATURE=${DEFAULT_TEMPERATURE:-0.2} - DEFAULT_MAX_ANSWER_TOKENS=${DEFAULT_MAX_ANSWER_TOKENS:-1024} # Paraphrase tolerance for POST /verify's quote grounding (higher = stricter). - QUOTE_MATCH_THRESHOLD=${QUOTE_MATCH_THRESHOLD:-0.9} # Cloud per-1k input-token price for GET /stats' savings estimate (0.0 hides it). - CLOUD_PRICE_PER_1K_INPUT=${CLOUD_PRICE_PER_1K_INPUT:-0.0} # POST /documents/{id}/calibrate: battery cap + non-strict scoring pass line. - CALIBRATE_MAX_ITEMS=${CALIBRATE_MAX_ITEMS:-100} - CALIBRATE_MATCH_THRESHOLD=${CALIBRATE_MATCH_THRESHOLD:-0.85} # Serve the zero-install web UI at /ui (loopback-only; unauthenticated stack). - WEBUI_ENABLED=${WEBUI_ENABLED:-true} - MAX_UPLOAD_MB=${MAX_UPLOAD_MB:-50} # llama call timeouts (seconds) — raise QUERY_TIMEOUT_S on slow CPUs if # first-after-restart queries on big documents hit the 502 wall. - QUERY_TIMEOUT_S=${QUERY_TIMEOUT_S:-600} - WARM_TIMEOUT_S=${WARM_TIMEOUT_S:-3600} - CACHE_DIR=/caches - DB_HOST=db - DB_PORT=5432 - DB_NAME=${DB_NAME:-llamacag} - DB_USER=${DB_USER:-llamacag} - DB_PASSWORD=${DB_PASSWORD:?set DB_PASSWORD in .env (python llamacag.py setup)} volumes: - kv_caches:/caches ports: - "127.0.0.1:${CAG_API_PORT:-8000}:8000" depends_on: db: condition: service_healthy # required: false so cag-api still starts in native mode, when the # llama-server service is disabled (its "local-llama" profile is off) and # inference runs on the host. cag-api reaches it via LLAMA_SERVER_URL. llama-server: condition: service_started required: false n8n: image: docker.n8n.io/n8nio/n8n:${N8N_VERSION:-latest} container_name: llamacag-n8n restart: unless-stopped environment: - N8N_PORT=5678 - N8N_PROTOCOL=http - N8N_HOST=${N8N_HOST:-localhost} - N8N_SECURE_COOKIE=false - N8N_DIAGNOSTICS_ENABLED=false - N8N_PERSONALIZATION_ENABLED=false - N8N_RUNNERS_ENABLED=true - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY:?set N8N_ENCRYPTION_KEY in .env (python llamacag.py setup)} - N8N_USER_MANAGEMENT_JWT_SECRET=${N8N_USER_MANAGEMENT_JWT_SECRET:?set N8N_USER_MANAGEMENT_JWT_SECRET in .env (python llamacag.py setup)} - GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-UTC} - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=db - DB_POSTGRESDB_PORT=5432 - DB_POSTGRESDB_DATABASE=n8n - DB_POSTGRESDB_USER=${DB_USER:-llamacag} - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD} volumes: - n8n_data:/home/node/.n8n # Watched by the document-ingestion workflow (Local File Trigger). - ${DOCUMENTS_FOLDER:-./documents}:/data/documents ports: - "127.0.0.1:${N8N_PORT:-5678}:5678" depends_on: db: condition: service_healthy db: image: postgres:17-alpine container_name: llamacag-db restart: unless-stopped environment: - POSTGRES_USER=${DB_USER:-llamacag} - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=${DB_NAME:-llamacag} volumes: - postgres_data:/var/lib/postgresql/data # 01: creates the separate `n8n` database; 02: llamacag app schema. - ./database/init-n8n-db.sh:/docker-entrypoint-initdb.d/01-init-n8n-db.sh:ro - ./database/schema.sql:/docker-entrypoint-initdb.d/02-schema.sql:ro ports: - "127.0.0.1:${DB_PORT:-5432}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-llamacag} -d ${DB_NAME:-llamacag}"] interval: 5s timeout: 5s retries: 10 volumes: n8n_data: postgres_data: llama_models: kv_caches: