Skip to content

Deployment: shipping a pre-processed cache

The analyzer's data pipeline (downloads from psp.cz, PDF text extraction, LLM classification and summarization, amendment parsing) takes hours to run from scratch. Production deployments therefore ship a pre-processed cache instead: build it once on a machine with the LLM backend configured, archive the cache directory, upload the archive, and extract it over the production cache dir. No re-download or re-processing happens on the server.

Production topology

  • Two containers from one image (docker compose up): the public frontend (port 8000) and the password-protected backend/admin (port 8001).
  • Both mount the same host directory as the cache: ${PSPCZ_CACHE_HOST_DIR:-./cache-data}:/data/cache (see Configuration).
  • LLM summaries are generated by the configured provider — production runs vLLM behind an OpenAI-compatible endpoint (LLM_PROVIDER=openai, OPENAI_BASE_URL, OPENAI_MODEL in .env).
  • The frontend hot-reloads when cache files change. Always stop the app before touching the cache dir — extracting under a running app triggers reloads from partially-written files.

Step 1 — Build the archive

On a machine with network access to psp.cz and a working LLM backend:

# Default: periods 10 9 8 7 6 5 → ./pspcz-cache-<timestamp>.tar.gz
uv run python scripts/build_and_backup_cache.py

# Subset / no archive / resume without re-downloading
uv run python scripts/build_and_backup_cache.py --periods 10 9
uv run python scripts/build_and_backup_cache.py --no-backup
uv run python scripts/build_and_backup_cache.py --skip-download

# Long build? Detach it:
nohup uv run python scripts/build_and_backup_cache.py > build.log 2>&1 &

See Building & shipping a pre-processed cache in the README section and the module docstring of scripts/build_and_backup_cache.py for details. The archive stores paths relative to the cache dir, so it extracts cleanly over any PSPCZ_CACHE_DIR layout.

Step 2 — Upload (scripts/upload_cache.sh)

A hardened rsync wrapper built for slow, flaky uplinks: it resumes interrupted transfers (--partial --append-verify), retries with backoff, and verifies the remote checksum before declaring success.

# Upload the newest ./pspcz-cache-*.tar.gz to SSH host "master"
./scripts/upload_cache.sh

# Explicit archive + host, custom staging dir
./scripts/upload_cache.sh pspcz-cache-20260722-051522.tar.gz prod-host --remote-dir ~/cache-upload

# Simulate: preflight checks + dry-run rsync, changes nothing
./scripts/upload_cache.sh --dry-run

# Upload AND extract into the production cache dir (app must be stopped)
./scripts/upload_cache.sh prod-host --extract --remote-cache-dir /data/psp-cache
Argument / option Default Meaning
TARBALL newest ./pspcz-cache-*.tar.gz Archive to upload
REMOTE_HOST $PSPCZ_REMOTE or master SSH host alias or user@host
--remote-dir DIR ~/cache-upload Remote staging directory
--extract off Extract after verified upload
--remote-cache-dir DIR — (required with --extract) Absolute remote PSPCZ_CACHE_DIR to extract into
--clobber-state off Also overwrite runtime_config.json / pipeline_history.json
--attempts N 10 Max transfer attempts before giving up
--dry-run off Preflight + simulated rsync only

Every run is tee'd to ./upload-cache-<timestamp>.log.

tmux new -s cache-upload
./scripts/upload_cache.sh          # e.g. 8.7 GB @ 3 MB/s ≈ 50 min of transfer
# Ctrl-b d — detach; the transfer continues without your terminal
tmux attach -t cache-upload        # reattach later; look for "VERIFIED"

If the connection drops, the retry loop reconnects automatically and resumes from the last complete block — a failed run never loses progress. Re-running the same command after any failure is always safe.

Step 3 — Extract and verify

If you did not pass --extract, extract manually on the production host (app stopped first):

docker compose stop
tar xzf ~/cache-upload/pspcz-cache-*.tar.gz -C "$PSPCZ_CACHE_HOST_DIR"
docker compose up -d

State files: runtime_config.json / pipeline_history.json

The archive contains the build machine's runtime_config.json (admin config-editor state, including LLM provider/model settings) and pipeline_history.json. Extracting them over production would silently replace production's configuration.

  • scripts/upload_cache.sh --extract excludes both files by default. Pass --clobber-state only when you deliberately want a full reset.
  • For manual tar extraction, exclude them explicitly if production already has config you want to keep: tar xzf archive.tar.gz --exclude=runtime_config.json --exclude=pipeline_history.json -C "$DIR"

Rollback

Before extracting, move the current cache aside rather than overwriting it:

mv "$PSPCZ_CACHE_HOST_DIR" "${PSPCZ_CACHE_HOST_DIR}.old"
mkdir -p "$PSPCZ_CACHE_HOST_DIR"
tar xzf pspcz-cache-*.tar.gz -C "$PSPCZ_CACHE_HOST_DIR"
# healthy? rm -rf "${PSPCZ_CACHE_HOST_DIR}.old"   # only after verification

Step 4 — Restart and health-check

docker compose up -d
curl -fsS http://localhost:8000/api/health          # frontend
curl -fsS http://localhost:8001/admin/api/health    # backend (IP-whitelisted)

Both containers use a 300 s healthcheck start period — a cold cache loads shared tables and per-period data on first boot, which takes a while.