Backups
Ragz's state is split across four stores. Two of them are irreplaceable and must be backed up; two are re-derivable from source data, at a cost.
| Store | Contains | Re-derivable? |
|---|---|---|
Postgres (pgdata) | Orgs, workspaces, users, memberships, document metadata, chat history, encrypted secrets, audit log | No — back up |
| KEK file | The key that unwraps every encrypted secret in Postgres | No — back up |
| MinIO (object storage) | Original document files + extracted blocks/chunks | Yes, if you still have the source files — but re-ingestion costs time and compute |
| Qdrant (vectors) | Document embeddings | Yes — rebuildable by re-parsing + re-embedding from MinIO/Postgres, at real compute cost |
KEK loss is unrecoverable
The KEK (Key Encryption Key) is the only secret that lives outside Postgres. Every provider API key, and other secret Ragz stores, is wrapped with AES-256-GCM envelope encryption under this key (see Security Model). If you back up Postgres but lose the KEK file, every encrypted row in Postgres becomes permanently undecryptable — there is no recovery path, no "reset password" equivalent. Treat the KEK file with at least as much care as the Postgres backup, and store it separately from the Postgres backup so a single lost or corrupted backup target can't take out both.
What to back up
1. Postgres — the pgdata volume
This is the system of record for everything except file bytes and vectors: tenancy (organizations, workspaces, memberships, groups, role templates), document metadata (versions, ACL groups, approval state), chat history, usage records, and the encrypted secrets table itself.
Two approaches, either is fine:
# Logical backup — portable, works across Postgres versions
docker compose -f deploy/compose.yaml exec postgres \
pg_dump -U ragz -d ragz -Fc -f /tmp/ragz.dump
docker compose -f deploy/compose.yaml cp postgres:/tmp/ragz.dump ./backups/
# Volume snapshot — faster, ties you to the same Postgres major version on restore
docker run --rm -v ragz_pgdata:/data -v "$(pwd)/backups":/backup \
alpine tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .Don't forget the litellm database on the same Postgres instance
(postgres-init.sql seeds it) — it holds LiteLLM's own model routing state.
Back it up alongside ragz.
2. The KEK file
By default this is backend/data/ragz_kek (override with RAGZ_KEK_FILE).
It's a small file (base64-encoded 32 random bytes) with 0600 permissions —
back it up as-is:
cp backend/data/ragz_kek ./backups/ragz_kek.$(date +%F)Store it somewhere independent of your Postgres backup target — a secrets manager, a separate encrypted volume, or offline cold storage. If your Postgres backups and KEK backups live in the same place and that place is lost, you've lost both halves at once, which is exactly the failure mode this separation is meant to prevent.
What's re-derivable (and what that costs)
MinIO holds the original uploaded files and the extracted blocks/chunks
produced during parsing. If you lose it but still have the source documents
elsewhere, you can re-upload and re-ingest — but you lose nothing by backing
it up too (miniodata volume), since re-ingestion means re-running parsing,
chunking, and embedding for every document.
Qdrant holds the vector embeddings. These are fully re-derivable from
MinIO + Postgres by re-parsing and re-embedding — but that's real compute
time and, if you use a hosted embedding provider, real API cost, proportional
to your corpus size. For any non-trivial corpus, back up the qdrantdata
volume rather than planning to rebuild it from scratch.
docker run --rm -v ragz_miniodata:/data -v "$(pwd)/backups":/backup \
alpine tar czf /backup/miniodata-$(date +%F).tar.gz -C /data .
docker run --rm -v ragz_qdrantdata:/data -v "$(pwd)/backups":/backup \
alpine tar czf /backup/qdrantdata-$(date +%F).tar.gz -C /data .Restore order
Restore in this order so nothing comes up referencing state that isn't there yet:
- KEK file first — put it back at
RAGZ_KEK_FILEbefore starting anything that touches secrets. - Postgres (
pgdataorpg_dumprestore) — the system of record. - MinIO (
miniodata) — document files the metadata in Postgres refers to. - Qdrant (
qdrantdata) — vectors the retrieval path queries. If this restore is stale or missing, ingestion status in Postgres will disagree with what's actually searchable; a full re-index of affected documents resolves that. - Start the stack, and verify: sign in as an existing user, open a workspace with documents, and confirm chat returns citations against a known document.
Test your restores
A backup you haven't restored is a hypothesis, not a backup. Periodically restore into a scratch environment and confirm you can sign in, see existing documents, and get a cited chat answer — that exercises all four stores together.