caddy
Caddy 2.11, Let's Encrypt
Terminates HTTPS in front of web, the only container reachable from the internet. It gets and renews its certificate on its own, redirects HTTP to HTTPS, and passes the Server-Sent Events streams through unbuffered. It runs only in production (compose.prod.yaml).
web
Next.js 16, React 19, Tailwind 4, Zod
Serves the UI and a backend-for-frontend: the browser only ever calls web's own /api/* route handlers, which call FastAPI inside the stack and validate every response against Zod schemas generated from the API's OpenAPI schema. It also proxies the API itself under /fastapi/, docs included.
api
FastAPI, Uvicorn, psycopg 3, Pydantic 2
Answers REST requests and streams job progress as Server-Sent Events. It holds no files and no state of its own: it reads Postgres, publishes tasks to RabbitMQ by name, and reads progress from Redis. A correlation id follows each request from the BFF through the API and into the task's message headers.
postgres
PostgreSQL 18, raw SQL
The source of truth: the model registry and vocabularies, concept tables, corpora, search hits, and every job's status and attempts. Plain-SQL migrations run once by a one-shot migrate container before anything else starts, and each service logs in as its own role with only the privileges it needs.
rabbitmq
RabbitMQ 4.4, durable direct exchanges
The Celery broker. Each queue (cpu for concept walks, search for corpus scans) is durable and bound to a direct exchange of its own name. The consumer timeout is raised to about a year, since a walk or scan can run for hours while its message stays unacknowledged.
redis
Redis 8.8 Streams
Holds only progress: one stream per job, which the worker appends to and the API reads. A page that reconnects sends its Last-Event-ID and replays from there. Streams are capped and expire an hour after their job ends.
workers
Celery 5.6, NumPy, SciPy
One task at a time per container; scale by running more containers. Concept workers mount the models read-only and walk on threads; search workers mount the corpora read-only and never load a model. Both write their results to disk and Postgres, then mark the job done in the same transaction.
builder
the msr CLI, spaCy 3.8
A run-and-exit container for the heavy, offline steps: downloading and parsing a dataset, counting co-occurrences, building the transition matrix and the comparison walks, and registering the model. Builds are staged and checksummed, so an interrupted one resumes where it stopped.