Production Deployment

deploy/compose.yaml is the reference single-node deployment. It runs Postgres, Redis, Qdrant, MinIO, LiteLLM, and (optionally) local embedding/rerank model servers as one Docker Compose stack, with the Ragz API and Celery workers alongside it. This page is the checklist for taking that stack from a laptop demo to something you can point real users at.

Single-node, by design

This is a reference deployment for one host. It scales a long way — see Scaling — but if you need multi-node HA from day one, treat this page as the baseline configuration to adapt, not a drop-in cluster.

Before exposing anything beyond localhost

Every service in deploy/compose.yaml ships with a working dev credential so docker compose up works out of the box. None of them are safe to expose. Change all of the following before anyone outside your laptop can reach the stack:

CredentialWhere it livesNotes
LITELLM_MASTER_KEYlitellm service envAdmin key for the LiteLLM proxy
LITELLM_SALT_KEYlitellm service envEncrypts provider keys at rest in LiteLLM's own DB
Postgres passwordpostgres service env (POSTGRES_PASSWORD)Shared by Ragz and LiteLLM's database
MinIO root user/passwordminio service env (MINIO_ROOT_USER / MINIO_ROOT_PASSWORD)Object storage for documents and extracted blocks
RAGZ_BOOTSTRAP_PASSWORDbackend bootstrap envFirst superadmin's password — 12+ characters

None of these are read from .env files checked into the repo — set them as real environment variables (or secrets in your orchestrator) at deploy time.

Back up two things

Before you go live, make sure both of these are captured by your backup job:

  1. The pgdata volume — all tenant data: organizations, workspaces, users, documents metadata, chat history, and encrypted secrets.
  2. The KEK file (backend/data/ragz_kek, or wherever RAGZ_KEK_FILE points). This is the Key Encryption Key that every provider secret is wrapped under (AES-256-GCM envelope encryption). Losing it makes every stored provider key unrecoverable — not "hard to recover," mathematically unrecoverable. See Backups for the full procedure.

Put it behind a reverse proxy with TLS

Every service in deploy/compose.yaml binds to 127.0.0.1 on the host — Postgres on 55432, Redis on 56379, Qdrant on 56333, MinIO on 59000/ 59001, LiteLLM on 54000, and so on. That's deliberate: nothing in the stack is meant to be reachable directly. The only thing that should be public is a reverse proxy terminating TLS in front of the Ragz API and web app.

  • Run nginx, Caddy, or Traefik in front of the FastAPI backend and the frontend build.
  • Terminate TLS at the proxy (Let's Encrypt via Caddy or certbot is the simplest path).
  • Pass the real client IP. The backend's rate limiting (auth and chat endpoints) keys off the request IP — if your proxy doesn't forward X-Forwarded-For (or your proxy config doesn't trust/parse it), every request looks like it comes from the proxy itself and rate limiting degrades to one shared bucket for all your users.
# Minimal nginx example — adapt to your proxy of choice
location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $host;
}

If you're fronting the proxy itself with Cloudflare (Tunnel or DNS-proxied), see Cloudflare — the same "real client IP" concern applies at that layer too.

Scale as load grows

The two things that need more headroom as usage grows are ingestion and chat:

  • More Celery workers for ingestion throughput (parsing, chunking, embedding). Add workers rather than making one worker do more — see Scaling for the exact command.
  • More uvicorn processes for chat/API concurrency.

Move Postgres to managed

deploy/compose.yaml's postgres service is fine for evaluation and small deployments, but for real traffic move to a managed Postgres instance (RDS, Cloud SQL, or equivalent) with its own automated backups and point-in-time recovery. Point DATABASE_URL at it and drop the postgres service from your compose file. LiteLLM shares the same Postgres instance (a separate litellm database, seeded by postgres-init.sql) — migrate both together.

Checklist

  • Rotated LITELLM_MASTER_KEY, LITELLM_SALT_KEY, Postgres password, MinIO root user/password
  • Set a strong RAGZ_BOOTSTRAP_PASSWORD (12+ characters)
  • Backup job covers the pgdata volume
  • Backup job covers the KEK file, stored somewhere independent of the Postgres backup
  • Reverse proxy terminates TLS; only the proxy is publicly reachable
  • Proxy forwards X-Forwarded-For and the backend trusts it
  • A plan exists for adding Celery workers / uvicorn processes under load
  • Postgres is on a managed instance with its own backups (or you've accepted the operational cost of running it yourself)