This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This repo is a single Docker Compose stack (docker-compose.yml) for a self-hosted, GPU-accelerated AI chatbot with retrieval-augmented generation (RAG). There is no application source code, build system, or test suite — all behavior comes from the compose file wiring together off-the-shelf container images. "Working in this codebase" means editing docker-compose.yml (service config, env vars, ports, volumes) and operating the stack.
cp .env.example .env # REQUIRED before first up: set WEBUI_SECRET_KEY (openssl rand -hex 32)
docker compose up -d # start the whole stack (detached)
docker compose down # stop and remove containers (data persists in bind mounts)
docker compose pull && docker compose up -d # update images and recreate changed services
docker compose ps # list running services
docker compose logs -f open-webui # tail logs for one service
docker compose restart open-webui # restart after changing its env
docker compose exec ollama ollama pull <model> # download a model (e.g. gemma3:12b)
docker compose exec ollama ollama list # list installed models
# Optional bulk PDF import (profile-gated; drop PDFs/zip in ./import first):
docker compose --profile import run --rm pdf-import
# Optional: create/update the grounded model bound to the imported collection:
docker compose --profile setup run --rm setup-grounded
# Optional: provision API access for external consumers (prints an API key):
docker compose --profile setup run --rm setup-apiThere is no lint/test/build step — validate changes with docker compose config (renders and checks the compose file) and by bringing the stack up.
Four always-on services on one Compose network, plus optional profile-gated extras (one-shot import/setup jobs and a consumer demo). Only ollama and open-webui are published to the host (plus the demo's :8090 when enabled); the rest are reachable only by service name from inside the network. The Open WebUI image tag is shared via the x-owui-image YAML anchor (&owui_image) so open-webui and the helper jobs pull a single image.
- ollama (host
11500→11434) — LLM inference engine. Requires an NVIDIA GPU (reserved viadeploy.resourceswithcount: all); the stack will not run models without it. Default context window is raised viaOLLAMA_CONTEXT_LENGTH=16384(Ollama's 4096 default silently truncates RAG prompts; the oldOLLAMA_NUM_CTXon open-webui was a no-op). Thedocs-groundedmodel also pinsnum_ctx: 16384in its params viasetup_grounded.py. Models persist in~/ollama_data. - open-webui (host
3000→8080) — the chat frontend and the only user-facing UI. Uses the:v0.10.2-cudaimage and reserves the GPU (deploy.resources) so embeddings + the reranker run on GPU, not CPU (query latency is otherwise dominated by CPU reranking, independent of corpus size). Talks to Ollama viaOLLAMA_BASE_URL=http://ollama:11434. Owns the RAG configuration (see below). RequiresWEBUI_SECRET_KEY(from.env). App state (users, chats, vector DB) persists in~/openwebui_data. - docling-serve (internal
:5001) — document extraction backend. open-webui offloads file parsing to it viaCONTENT_EXTRACTION_ENGINE=docling+DOCLING_SERVER_URL. - pipelines — Open WebUI Pipelines runtime (OpenAI-compatible plugin server, default
:9099) for custom filters/functions. Config persists in~/pipelines_data. - setup-api (optional,
profiles: ["setup"]) — one-shot; runsscripts/setup_api_consumer.py. Provisions external API access: enables API keys endpoint-restricted to/api/chat/completions+/api/models, grants thefeatures.api_keysdefault-user permission (required for non-admins to hold a key; harmless here since signup is off), creates the non-admin[email protected]user, mints/fetches its API key, smoke-tests it againstdocs-grounded, and prints the key. Idempotent. Field-name gotcha: this build's admin auth config uses pluralENABLE_API_KEYS/API_KEYS_ALLOWED_ENDPOINTS(unknown fields are silently ignored). Model visibility for non-admins requires an access grant —setup_grounded.pysets a public-read grant (principal_id: "*"); without it non-admin API calls get 400 "Model not found". The knowledge collection needs its own public-read grant (set byimport_pdfs.pyvia/api/v1/knowledge/{id}/access/update) — model access alone is not enough: retrieval is access-filtered per requesting user, so a non-readable collection makes the grounded model refuse every question with "no info in the documents" (empty context), with no error anywhere. Consumer docs:docs/API.md. - pdf-import (optional,
profiles: ["import"]— not started byup) — one-shot bulk PDF importer. Reuses the open-webui image; runsscripts/import_pdfs.py(pure stdlib). Reads PDFs/zip from./import, mounts~/openwebui_dataread-only to read the admin id, signs a JWT with the sharedWEBUI_SECRET_KEY, and loads files into a knowledge collection (IMPORT_COLLECTION, default "Imported PDFs") via the API. Idempotent. Run:docker compose --profile import run --rm pdf-import. - setup-grounded (optional,
profiles: ["setup"]) — one-shot; runsscripts/setup_grounded.py. Creates/updates thedocs-groundedworkspace model (basegemma3:12b) bound to the imported collection, with a system prompt that answers only from the docs (no guessing), replies in the user's language, and cites sources. Idempotent (create-or-update). The strict RAG template,RAG_RELEVANCE_THRESHOLD, andDEFAULT_MODELS=docs-groundedare seeded from open-webui env on first boot; this job only owns the model (it references a runtime collection, so it cannot be an env var). Run after importing:docker compose --profile setup run --rm setup-grounded.
Data flow for a RAG query: open-webui extracts uploaded docs via docling → chunks (CHUNK_SIZE=3000, CHUNK_OVERLAP=500) and embeds them → on a question, runs hybrid retrieval (ENABLE_RAG_HYBRID_SEARCH, BM25 weight 0.5) pulling RAG_TOP_K=20, then reranks with BAAI/bge-reranker-v2-m3 down to RAG_TOP_K_RERANKER=10 → passes context to the Ollama model for generation.
WEBUI_SECRET_KEYis required (referenced as${WEBUI_SECRET_KEY:?...}indocker-compose.yml). Copy.env.example→.envand set it (openssl rand -hex 32) ordocker compose upfails. Pinning it keeps logins alive across restarts and lets thepdf-importservice authenticate..envis gitignored; the droppedimport/files are too.- Open WebUI config is write-once (PersistentConfig). Its env vars (web search, tool calling, docling, RAG, etc.) seed the SQLite DB (
~/openwebui_data/webui.db,configtable) only on the first boot with an empty data dir. After that the DB wins and env edits are ignored. To change a setting on an existing install: use the UI, or patch theconfigtable directly anddocker compose restart open-webui(it caches config in memory at boot), or wipe~/openwebui_datato re-seed. This bit us: added web-search/tool env didn't apply until patched in the DB. - Web search is auto-wired (via env, on first boot):
ENABLE_WEB_SEARCH=true,WEB_SEARCH_ENGINE=duckduckgo(no key),WEB_LOADER_ENGINE=safe_web(open-webui's built-in page fetcher — no JS rendering; SPA-heavy sites extract poorly). It runs only when a UI user toggles the globe icon; the grounded model and API consumers never trigger it. A crawl4ai + proxy scraping pair was removed (the unmaintained proxy 502'd on real multi-URL batches — see git history if resurrecting). pipelines is standalone — register it manually as an OpenAI connection tohttp://pipelines:9099if you want it (optional; the chatbot works without it). function_callingmust belegacy, notdefault, for tool-less models (gemma3). Set viaDEFAULT_MODEL_PARAMS={"function_calling":"legacy"}. In open-webui v0.10.2middleware.pytreats ONLY"legacy"as prompt-based; every other value (including"default") is native → it attaches atoolsarray → gemma3 errors"does not support tools"with web search on. The UI label "Default" maps to native here; "Legacy" is the safe one.- Never touch
webui.dbfrom the host. It runs in WAL mode; a host (Windows) SQLite connection creates-wal/-shmsidecar files the container's Linux SQLite then cannot open →OperationalError: unable to open database fileon every query, and any uncommitted WAL writes can be lost. Edit the DB only viadocker compose exec -T open-webui python3 - <<'PY' … PY(container-side), the HTTP API, or with the container stopped. - Grounded model + registry refresh. The
docs-groundedmodel answers only from the docs. A model created/updated via the API is invisible to/api/chat/completions(400 "Model not found") until/api/modelsis fetched once or open-webui restarts. Bound-collection knowledge is injected only whenfunction_callingislegacy. To go grounded end-to-end on a fresh box:up -d(seeds strictRAG_TEMPLATE,RAG_RELEVANCE_THRESHOLD=0.25,DEFAULT_MODELS=docs-grounded) → import PDFs → runsetup-grounded. - Citation format must be spelled out in
RAG_TEMPLATE. Chunks reach the model as<source id="N">tags; a vague "cite as [id]" instruction makes gemma3 copy the attribute syntax and emit literal[id="1"]in answers (breaks the UI's citation chips, reads as noise over the API). The template now dictates the exact format (bracketed number only).RAG_TEMPLATEis PersistentConfig — changing it on an existing install means patching therag.templatekey in theconfigtable (container-side) + restart, not just editing the env. - Citation semantics for API consumers.
/api/chat/completionsresponses include a top-levelsourcesarray (retrieved chunks + metadata). Marker[n]= nth chunk across all sources, flattened, 1-based. Source names are intentionally NOT exposed as download links: the API key is blocked from/api/v1/files, and public widget visitors shouldn't get the raw (possibly restricted) PDFs. Seedocs/API.md. - Scanned-PDF OCR.
DOCLING_PARAMSforces OCR (force_ocr) withocr_preset=tesseract—ocr_engineis deprecated in docling-serve and silently ignored, and the default RapidOCR mis-reads Latin/Malay (loads a Chinese model). Re-OCRs digital PDFs too (slower). Changing it needs a re-import to take effect. - Bind mounts, not named volumes. All persistent data lives under the invoking user's home dir (
~/ollama_data,~/openwebui_data,~/pipelines_data). Deleting these wipes models/chats. Do not switch to named volumes without migrating this data. - All images are pinned (see
docker-compose.yml).pipelinespublishes no semver tags, so it is pinned bygit-<sha>.docker compose pullwill not move versions; bump deliberately by editing the tags. ollamawas on a stale release candidate. It is now0.31.1(stable). A version jump can change the on-disk model store format — after bumping, verify existing models in~/ollama_datastill load.