Database backup
Before every upgrade, take a custom-format PostgreSQL dump and encrypt it with a separately retained age recipient. Keep passwords, dumps and decryption keys outside Git. From the image bundle directory:pg_restore --exit-on-error --no-owner --no-acl. Compare table counts,
migration version, case/warning/receipt identifiers and event/outbox JSON object
types. Read cases from the restored instance. A dump is not a verified backup
until the restore rehearsal succeeds. Never attach the live volume to it.
When private-object storage is enabled
IfPUP_OBJECT_STORAGE_ENABLED=true, a database dump alone is incomplete.
PostgreSQL stores object records and encrypted staging data; retained attachment,
evidence, transcript, and export bytes can live in the separate private Azure
Blob store.
- Protect the private containers alongside PostgreSQL. Retain the object keys, bytes, content types, metadata, and provider versions needed by the database records. Keep backups private and preserve them until explicitly deleted. Continue to honor legal holds.
- Keep an encrypted recovery copy of the storage configuration and access needed
to read those containers, including any configured encryption scope. Preserve
the complete
PUP_OBJECT_STAGING_KEYSkeyring used by the API, bot, and worker, with its key IDs. Retained staging rows require their original key; generating a new key with the same ID does not decrypt them. Retain older keys while any retained database backup or staging row needs them, regardless of age. Completion does not remove a retained recovery copy; retire a key only after all copies that need it have been explicitly deleted. - Rehearse with a separate database and private storage copy, with live Discord delivery disabled. Check that an authorized case-evidence read retrieves the expected bytes and passes integrity checks. If the backup contains queued staging, verify that its matching key can decrypt it and that storage work can complete against the rehearsal store.
- Rehearse private-object recovery on a disposable test object using an installation operator account with the required server permission. Wait for the queued restore result and verify the object can be read; an accepted restore request alone does not establish recovery. Never restore explicitly deleted member data merely to make a backup test pass.
Upgrade
Retain the previous verified release manifest, all four deployed image digests, their source version, and configuration. Review schema compatibility and Discord application identity. Follow Self-host Pup to obtain the next version’s portable images: schema-2 releases supply three ARM64 runtime images and a Cloudflare dashboard artifact, so the Compose dashboard must be built from matching source. Other architectures require matching source builds for all four services. Do not combine a newer runtime with an older dashboard. For migration0142_remove_automatic_retention, take and verify a backup, then stop
the old API, bot and worker before running the migration. This prevents an already
claimed deletion job from completing during the upgrade. Start only the new binaries
afterward; older binaries are incompatible with the removed fields and workers.
The migration preserves stored content and cancels old automatic deletion work.
It cannot recover content that was already erased.
Update the four image digest values in config.env, run the one-shot migration
service, recreate the application containers, and verify Connection status,
Cases, and Discord delivery. Recheck private-object access when enabled.