PostgreSQL is gone. Host destroyed, data dir deleted, primary and replica both down. All you have is the repository and the keyring. This is the canonical "we lost the database" runbook and the most important one in this directory.
- The primary host is unreachable, refuses to boot, or the
pg_datadirectory is empty / corrupt. psqlto the production endpoint refuses connections or returns garbage.- You're at a fresh host (or the same host post-repaved) and need to reconstruct PG from the repo.
Before touching anything, verify the inputs you have:
-
Repository is reachable and intact.
pg_hardstorage repo check <repo-url>
The output must be clean — signatures verify, chunks present. If
repo checkreports findings, see R4-repo-corruption-at-rest first; do not restore from a corrupt repo. -
Keyring is present and matches the backups.
pg_hardstorage kms inspect
Note the public-key fingerprint. It must match the manifest's
attestation.public_keyfor the backup you intend to restore. If the fingerprint doesn't match, you have the wrong keyring; find the right one before continuing. A wrong keyring fails fast at restore-time (restore.kek_mismatch, exit 1) — but verify now, not after a 4-hour copy. -
Pick the restore target.
- Backup ID.
pg_hardstorage list <deployment>for choices. Pick the latest one that pre-dates the loss event by enough margin to avoid ingesting whatever caused the loss. - PITR target if you have one (e.g. just before a known-bad
deploy at
2026-04-29 10:42 UTC). - Target host with enough free disk for the restored data dir plus 20% margin. Pre-flight asserts 110%; aim for more.
- Backup ID.
-
Verify pg_verifybackup is on PATH.
which pg_verifybackup
The mandatory verify gate uses it. If it isn't there, install the matching
postgresql-client-<major>package or pass--verify=skipat restore-time (only after explicit acknowledgement — exit 9 is the contract).
-
Provision the target host. Install PG of the same major version the backup was taken against (the manifest's
pg_versionfield). Do NOT initialise the data dir —pg_hardstorage restoredoes that:pg_hardstorage show <deployment> <backup-id> | jq '.result.pg_version'
-
Preview before committing. This prints the WAL replay range, RTO estimate, target tablespace mapping, verification gate. No bytes move:
pg_hardstorage restore <deployment> <backup-id> \ --target /var/lib/postgresql/restored \ --to "<your-pitr-target>" \ --repo <repo-url> \ --preview
Read the output. Confirm the LSN range falls outside any known WAL gaps (the preview will refuse if not). Confirm the tablespace mapping matches what you expect on this host.
-
Run the restore. Same flags, drop
--preview. Pass--forceif the target directory is non-empty (the default refuses to overwrite):pg_hardstorage restore <deployment> <backup-id> \ --target /var/lib/postgresql/restored \ --to "<your-pitr-target>" \ --repo <repo-url>
The restore writes mode-0600 files, fsyncs per file, atomically commits recovery files (
recovery.signaland a managed block inpostgresql.auto.confwhoserestore_commandinvokespg_hardstorage wal fetch). The mandatorypg_verifybackupgate runs against the data dir before the command declares success. Exit 9 means the verifier said no; do not start PG. -
Start PG. systemd unit,
pg_ctl start, container, whatever your local convention is. PG enters recovery,wal fetchis invoked per segment until the recovery target is reached or no more WAL is available (exit 6 fromwal fetchis what tells PG to stop). -
Wait for recovery completion. Watch the PG log for
consistent recovery state reachedand thenrecovery has paused at WAL location ...(if you specified a target) orarchive recovery complete(if you restored to latest). -
Promote. When PG is paused at the recovery target:
SELECT pg_promote();PG exits recovery and accepts writes on a new timeline.
-
PG is up, accepting connections, on a new timeline ID greater than the manifest's
timeline. -
SELECT pg_is_in_recovery();returnsfalse. -
pg_amcheck --all --heapallindexed --rootdescendpasses (run this; it is the strongest check we have):pg_amcheck --all --heapallindexed --rootdescend -d <database>
-
A spot-check query against your most-recently-known data is consistent with the PITR target.
-
pg_hardstorage status <deployment>shows the recovered host as the new endpoint and starts a fresh backup chain.
- If the restore fails verification (exit 9): the data dir is in a
partially-populated state.
rm -rfthe target dir. Pick a different backup ID and restart from step 2. - If recovery on PG fails after start: stop PG,
rm -rfthe data dir, repeat the restore. Restores are idempotent on the target path with--force. - If you promoted prematurely: there is no rollback from
pg_promote(). Take a fresh backup of whatever state you ended up in and start a new run if needed.
-
Document the timeline: when did the loss happen, when did the restore start, when did PG accept writes again, what RPO and RTO were achieved.
-
Compare to SLO targets:
pg_hardstorage slo report <deployment>
-
Append an audit event:
restore.cold_start_completedwith source backup ID, PITR target, new timeline ID. -
Re-establish the streaming pipeline against the new endpoint:
pg_hardstorage init(idempotent on the deployment) reconfigures the slot and resumes WAL streaming under a new persistent slot on the new timeline. -
File a postmortem against the originating loss event. The cold start succeeded; the question is why it was needed.