Skip to content

How to restore PostgreSQL from a self-managed-postgres backup

How-to · Updated Sep 2026

Coming from another cloud?

▸AWS·RDS Point IN Time Recovery

This Quake AI feature maps to AWS’s RDS Point IN Time Recovery.

▸Google Cloud·Cloud SQL Backups

This Quake AI feature maps to Google Cloud’s Cloud SQL Backups.

How to restore PostgreSQL from a self-managed-postgres backup

Copy a scheduled pg_dumpall from a running self-managed-postgres instance onto a second, disposable instance, restore it with psql, and confirm databases, roles, and row counts match. The source instance stays untouched.

The template allocates no floating IP. Reach both private addresses through a bastion, VPN, or jump instance on the same network. The commands below use ssh -J / scp -J; see SSH bastion access into a private subnet for ProxyJump setup.

Prerequisites

Windows: CLI examples use bash. Set up a Linux CLI environment on Windows before proceeding.

  • A running self-managed-postgres deployment with at least one completed backup (run /usr/local/bin/pg-backup.sh on the source if the cron has not fired yet)
  • A jump host that routes to the private subnet (bastion with a floating IP, VPN, or another instance on the same network). See SSH bastion access into a private subnet
  • Enough quota for one additional scratch instance during the drill

Replace JUMP_HOST with the jump host's reachable address (floating IP or VPN hostname), SOURCE_PRIVATE_IP and SCRATCH_PRIVATE_IP with the two instance private addresses, and YOUR_PRIVATE_KEY_PATH with the key that matches key_name on both templates. The default image is Ubuntu 24.04, so the SSH user is ubuntu.

What the template already gives you#

self-managed-postgres cloud-init (cloud-init/postgres.yaml) installs two recovery mechanisms on first boot, both scoped to the local data volume:

  • Scheduled logical backups. /usr/local/bin/pg-backup.sh runs on the backup_schedule cron (0 2 * * * by default) from /etc/cron.d/pg-backup. It writes a gzip-compressed pg_dumpall (roles and databases on that instance) to /var/lib/postgresql/backups/pgdumpall-TIMESTAMP.sql.gz.
  • Local WAL archiving. PostgreSQL sets archive_mode = on and copies each completed WAL segment to /var/lib/postgresql/wal_archive on the same data volume.

Both mechanisms write to the same Cinder volume the running database uses. They cover logical mistakes (a bad DELETE, a dropped table, an application bug) while that volume is still attached. Copy backups off the instance before you rely on them for anything beyond same-volume recovery.

Step 1: Identify the backup to restore#

SSH to the source instance through the jump host, list the dumps, and pick one:

bash
ssh -i YOUR_PRIVATE_KEY_PATH -J ubuntu@JUMP_HOST ubuntu@SOURCE_PRIVATE_IP
ls -lh /var/lib/postgresql/backups/

If the directory is empty, the nightly cron has not run yet. Trigger one dump, then list again:

bash
sudo /usr/local/bin/pg-backup.sh
ls -lh /var/lib/postgresql/backups/

Copy the target file to your workstation through the same jump host:

bash
scp -i YOUR_PRIVATE_KEY_PATH -J ubuntu@JUMP_HOST \
  ubuntu@SOURCE_PRIVATE_IP:/var/lib/postgresql/backups/pgdumpall-TIMESTAMP.sql.gz .

Step 2: Provision a scratch restore target#

Deploy a second, disposable instance for the drill. Reuse the self-managed-postgres template in a separate working directory with a different instance_name and private_cidr, following Deploy the self-managed PostgreSQL template with OpenTofu through the tofu apply step. Set db_password to a throwaway value; you discard this instance when the drill finishes.

Confirm the scratch instance runs the same PostgreSQL major version as the source (pg_version in both terraform.tfvars files) so the dump format matches.

Step 3: Restore the dump#

Copy the backup file onto the scratch instance if you have not already, then SSH in:

bash
scp -i YOUR_PRIVATE_KEY_PATH -J ubuntu@JUMP_HOST \
  pgdumpall-TIMESTAMP.sql.gz ubuntu@SCRATCH_PRIVATE_IP:/tmp/
ssh -i YOUR_PRIVATE_KEY_PATH -J ubuntu@JUMP_HOST ubuntu@SCRATCH_PRIVATE_IP

On the scratch instance:

bash
gunzip -c /tmp/pgdumpall-TIMESTAMP.sql.gz | sudo -u postgres psql

pg_dumpall output is plain SQL: psql replays CREATE ROLE, CREATE DATABASE, and per-database statements from the dump against the running server. Statements that collide with objects cloud-init already created (the default appuser role and appdb database) print errors. Expect those collisions; they do not mean the restore failed.

Step 4: Verify the restore#

Confirm the databases and roles from the source exist on the scratch instance:

bash
sudo -u postgres psql -c "\l"
sudo -u postgres psql -c "\du"

Compare row counts for a known table against the source instance:

bash
sudo -u postgres psql -d appdb -c "SELECT count(*) FROM YOUR_TABLE;"

Run the same query on the source instance and confirm the counts match (accounting for any writes that happened between the backup and the drill). Confirm the application role can authenticate and read data with the credentials the source uses:

bash
PGPASSWORD='YOUR_APP_PASSWORD' psql -h 127.0.0.1 -U appuser -d appdb -c "SELECT 1;"

Point-in-time recovery: what local WAL archiving covers#

self-managed-postgres declares a wal_archive_bucket variable. The current cloud-init does not wire that variable into archive_command: WAL segments land only in /var/lib/postgresql/wal_archive on the same Cinder volume as the database. Replaying those segments with pg_basebackup and recovery.signal lets you roll forward to a point in time after a logical mistake, as long as the volume that holds both the data directory and the WAL archive survives.

That local archive cannot recover you after you lose the volume, because the base data and the WAL segments live in the same place. Offsite point-in-time recovery requires shipping WAL segments to a separate location (Object Storage, for example) by changing archive_command in the template's cloud-init. Treat the current WAL archiving as a same-instance recovery aid.

Make the drill repeatable#

Re-run this drill:

  • On a monthly cadence
  • After any pg_version upgrade, since a dump taken on one major version may need pg_upgrade-style handling to restore cleanly on another
  • After any change to pg-backup.sh, backup_schedule, or the cloud-init script that produces the backups

Step 5: Tear down the scratch instance#

Destroy the scratch deployment from its working directory once you confirm the restore:

bash
tofu destroy

Type yes to confirm. Tear down the scratch instance after you verify the restore: it holds a copy of the source databases and consumes quota once verification finishes.

See also#

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

For the full policy, see Usage Guidelines.

Last validated: 01.09.2026

Was this page helpful?