Deployment¶
The supported deployment is Docker Compose: a custom Elasticsearch image (with the IK analyzer) plus the FastAPI app image. This page documents the compose stack, the app image, production hardening, data persistence/backup, and the docs auto-publish hook.
Quick start¶
cp .env.example .env # fill in KB_LLM__API_KEY / KB_EMBEDDING__API_KEY (optional)
docker compose up -d --build # builds ES+IK and the app; app on :8000
curl localhost:8000/readyz # 200 once ES is reachable
docker compose up -d --build elasticsearch brings up only ES (useful when running
the app on the host via uv run python -m kb). Missing API keys merely disable the
matching features (no LLM → chat/extract/ingest return 503; no embedding key → BM25-only).
The compose stack¶
docker-compose.yml defines two services.
elasticsearch (container kb-es)¶
- Built from
elasticsearch/Dockerfile— the officialelasticsearch:8.15.3image with theanalysis-ikplugin installed at build time. discovery.type=single-node,xpack.security.enabled=false, heap pinned to-Xms1g -Xmx1g.- Port
9200published; data on the named volumees-data(/usr/share/elasticsearch/data) so it survivesdocker compose down/recreation. - Healthcheck polls
_cluster/healthforgreen/yellow(5s interval, 30 retries).
app (container kb-app)¶
- Built from the root
Dockerfilewith build argINSTALL_OCR(default"false"). depends_on: elasticsearchwithcondition: service_healthy— the app waits for ES.- Reads
.env(optional) for API keys, and overridesKB_ES__URL=http://elasticsearch:9200so it reaches ES over the compose network regardless of anyKB_ES__URLin.env. - Port
8000published;restart: unless-stopped. - Bind mounts (host → container):
./config→/app/config— so seed-CSV edits and the startup taxonomy auto-sync (which rewritesconfig/taxonomy.yaml) persist to the host. This dir must stay writable../data/uploads→/app/data/uploads— so uploaded/imported files survive container recreation.- Healthcheck:
curl -fs http://localhost:8000/readyz(15s interval, 40s start period).
Runtime assets are read relative to the working dir
The image sets WORKDIR /app and the app reads config/, the seed CSVs,
data/uploads, and Knowledge Base Search.html relative to it. Keep those paths in
place (compose bind-mounts the first two).
The app image¶
Dockerfile is a multi-stage uv build:
- Builder (
python:3.12-slim) — installs deps reproducibly fromuv.lockwith theingestextra baked in (PDF/XLSX/PPTX/DOCX import works out of the box), then installs the project as a non-editable wheel into/app/.venv. - Optional OCR —
--build-arg INSTALL_OCR=trueaddspaddleocr+paddlepaddle(~1.5–2 GB) and the extra runtime libslibgl1/libglib2.0-0. Off by default to keep the image lean. - Runtime (
python:3.12-slim) — copies the venv,config/, and the frontend HTML; usestinias PID 1 for clean signal handling;HEALTHCHECKhits/readyz; entrypointpython -m kb --host 0.0.0.0 --port 8000.
docker build -t kb-app . # slim (no OCR)
docker build -t kb-app --build-arg INSTALL_OCR=true . # with PaddleOCR
To enable OCR in compose, set args.INSTALL_OCR: "true" under the app service and
rebuild.
Production hardening¶
The default compose stack is a single-node, security-off dev setup. For anything beyond local use:
| Concern | What to do | Setting / mechanism |
|---|---|---|
| ES over TLS | Point the app at an HTTPS ES and verify certs | KB_ES__URL=https://…, KB_ES__VERIFY_CERTS=true; for a self-signed node set KB_ES__SSL_FINGERPRINT (SHA-256 from ES startup output) |
| ES auth | Enable xpack.security, create a least-privilege user |
KB_ES__USERNAME / KB_ES__PASSWORD |
| Secrets | Never bake keys into the image | Mount .env or inject KB_LLM__API_KEY / KB_EMBEDDING__API_KEY via your orchestrator's secret store |
| AuthN/Z | The app has none — put it behind a gateway | Reverse proxy / API gateway enforcing auth (see Security) |
| Heap / resources | 1 GB heap is dev-sized | Raise ES_JAVA_OPTS; size ES to your corpus |
| Scaling | App is stateless except in-memory import sessions | Run multiple app replicas behind a load balancer; note import-session preview state is per-process (see below) |
Import sessions are in-process
Staged import sessions live in memory in the app process (TTL-evicted). Behind a load balancer, pin an import review flow to one replica (sticky sessions) or scale the app to one replica for the ingest UI — committed documents and the file tracker are in ES and are shared, only the preview state is local.
Data persistence & backup¶
Three kinds of state, all recoverable:
- Search documents in ES — the source of truth for imported docs is the
kb_import_filestracker index plus the seed CSVs. On every startup the app clears and reseeds the main indices from the CSVs, thenrestore_imports()replays committed imports from the tracker (see Build from Scratch → Startup). - Seed CSVs +
config/— version-control these; they fully define the seeded corpus. - The
es-datavolume — back it up if you want point-in-time recovery without a reseed:
# Snapshot the ES data volume to a tarball
docker run --rm -v knowledgebase_es-data:/data -v "$PWD":/backup alpine \
tar czf /backup/es-data-$(date +%F).tgz -C /data .
# Restore into a fresh volume
docker run --rm -v knowledgebase_es-data:/data -v "$PWD":/backup alpine \
sh -c "cd /data && tar xzf /backup/es-data-2026-06-05.tgz"
For larger deployments prefer the Elasticsearch snapshot API to a shared repository over a raw volume copy.
Docs auto-publish¶
The documentation site (this site) publishes to GitHub Pages via a local pre-push git
hook — deliberately not GitHub Actions, to avoid CI cost.
- Hooks are version-controlled in
scripts/git-hooks/and activated by./scripts/install-hooks.sh(which setscore.hooksPath, and is why that dir also carries the repo's Git LFS passthrough hooks:post-checkout,post-commit,post-merge). - On a push to
mainthat touchesdocs/ormkdocs.yml, thepre-pushhook runsscripts/deploy-docs.sh, which doesmkdocs gh-deploy --strict(build + push togh-pages). A failed strict build aborts the push. - Run
scripts/deploy-docs.shdirectly to publish on demand; bypass the hook once withgit push --no-verify.
Build/preview locally first: