#!/usr/bin/env bash # # Back up Relata: MongoDB, the uploaded originals, and optionally the Qdrant # vectors. # # MongoDB and the upload directory must be captured together — an item row that # refers to a file missing from the backup is a broken record. Qdrant is derived # data and can be rebuilt by re-embedding, so its snapshot is a convenience. # # ./scripts/backup.sh [destination-directory] # # Stop Relata first for a consistent snapshot. The workers shut down gracefully: # in-flight jobs drain, and any that do not finish keep their leases and return # to the queue on restart. set -euo pipefail # Backups contain the complete knowledge store and may include personal data. # Do not let a permissive caller umask create group- or world-readable archives. umask 077 STAMP="$(date -u +%Y%m%dT%H%M%SZ)" DEST="${1:-backups}/${STAMP}" COMPOSE="${COMPOSE:-docker compose}" UPLOAD_DIR="${RELATA_UPLOAD_DIR:-./data/uploads}" MONGO_DB="${RELATA_MONGO_DATABASE:-relata}" QDRANT_URL="${RELATA_QDRANT_URL:-http://127.0.0.1:6333}" QDRANT_COLLECTION="${RELATA_QDRANT_COLLECTION:-relata_chunks}" mkdir -p "$DEST" echo "==> Writing backup to $DEST" # --- MongoDB: the irreplaceable part --------------------------------------- echo "==> MongoDB ($MONGO_DB)" $COMPOSE exec -T mongodb mongodump --db="$MONGO_DB" --archive --gzip \ > "$DEST/relata-mongo.archive.gz" echo " $(du -h "$DEST/relata-mongo.archive.gz" | cut -f1)" # --- Uploaded originals ------------------------------------------------------ if [ -d "$UPLOAD_DIR" ]; then echo "==> Uploaded originals ($UPLOAD_DIR)" tar -czf "$DEST/uploads.tar.gz" -C "$(dirname "$UPLOAD_DIR")" "$(basename "$UPLOAD_DIR")" echo " $(du -h "$DEST/uploads.tar.gz" | cut -f1)" else echo "==> No upload directory at $UPLOAD_DIR; skipping" fi # --- Qdrant: derived, so failure here is not fatal --------------------------- echo "==> Qdrant snapshot (optional — vectors are rebuildable)" if curl -fsS -X POST "$QDRANT_URL/collections/$QDRANT_COLLECTION/snapshots" \ > "$DEST/qdrant-snapshot.json" 2>/dev/null; then echo " snapshot requested; retrieve it from the Qdrant storage volume" else echo " Qdrant unreachable or snapshots disabled — re-embed after restoring instead" rm -f "$DEST/qdrant-snapshot.json" fi # --- Configuration, minus secrets ------------------------------------------- if [ -f .env ]; then echo "==> Configuration reference (secret values redacted)" # Redact by assignment rather than dropping matching lines: keeping the key # names is useful when restoring, and a comment that merely mentions # "password" is not a secret. sed -E 's/^([A-Za-z_][A-Za-z0-9_]*(PASSWORD|API_KEY|SECRET|TOKEN|URI))=.*/\1=[redacted]/' \ .env > "$DEST/config-reference.env" fi cat > "$DEST/RESTORE.md" <<'EOF' # Restoring this backup docker compose stop app docker compose exec -T mongodb mongorestore --archive --gzip --drop \ < relata-mongo.archive.gz rm -rf ./data/uploads tar -xzf uploads.tar.gz -C ./data docker compose start app # or: make dev Then check the settings page. If "vectors stored" does not match "chunks in MongoDB", use **Settings → Re-embed workspace** to rebuild them. Full procedure: docs/BACKUP_AND_RESTORE.md EOF echo echo "==> Backup complete: $DEST" ls -lh "$DEST"