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_MODELin.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.
Running overnight on a slow uplink¶
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 --extractexcludes both files by default. Pass--clobber-stateonly when you deliberately want a full reset.- For manual
tarextraction, 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.