PostgreSQL Backup Strategies for VPS: pg_dump, WAL Archiving, pgBackRest, and S3

PostgreSQL Backup Strategies for VPS: pg_dump, WAL Archiving, pgBackRest, and S3

A PostgreSQL database without a tested backup is a liability. A VPS with a single-disk PostgreSQL installation and no off-server backup copy has a single point of failure that disk failure, provider incident, or human error can trigger. This guide covers three backup strategies in order of complexity: pg_dump automation (suitable for most cases), WAL archiving for point-in-time recovery, and pgBackRest for incremental backups at scale.

Backup Strategy Comparison

Strategy RPO (data loss) RTO (recovery time) Complexity Best for
pg_dump daily Up to 24 hours Minutes–hours Low Small databases, low write volume
pg_dump hourly Up to 1 hour Minutes–hours Low Medium databases, moderate writes
WAL archiving Minutes (per WAL segment) Minutes–hours Medium Point-in-time recovery needed
pgBackRest incremental Minutes Minutes High Large databases, fast recovery SLA

Strategy 1: Automated pg_dump to S3

Install and Configure Rclone

curl https://rclone.org/install.sh | sudo bash

# Configure S3 remote (Backblaze B2, Cloudflare R2, or AWS S3)
rclone config
# Follow prompts: new remote → s3 → your provider → access keys → save

# Test connection
rclone ls s3:your-backup-bucket

Create Backup Script

sudo nano /usr/local/bin/pg-backup.sh
#!/bin/bash
set -euo pipefail

BACKUP_DIR="/opt/backups/postgres"
S3_BUCKET="s3:your-backup-bucket/postgres"
KEEP_LOCAL_DAYS=3
KEEP_S3_DAYS=30
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOGFILE="/var/log/pg-backup.log"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOGFILE"; }

mkdir -p "$BACKUP_DIR"
log "Starting PostgreSQL backup..."

# Backup all databases (recommended for full recovery capability)
BACKUP_FILE="$BACKUP_DIR/pg_all_${TIMESTAMP}.sql.gz"
sudo -u postgres pg_dumpall | gzip > "$BACKUP_FILE"
log "Backed up all databases → $BACKUP_FILE ($(du -sh "$BACKUP_FILE" | cut -f1))"

# Upload to S3
log "Uploading to $S3_BUCKET..."
rclone copy "$BACKUP_FILE" "$S3_BUCKET"
log "Upload complete"

# Clean local backups older than KEEP_LOCAL_DAYS
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$KEEP_LOCAL_DAYS -delete
log "Removed local backups older than $KEEP_LOCAL_DAYS days"

# Clean S3 backups older than KEEP_S3_DAYS
rclone delete "$S3_BUCKET" --min-age "${KEEP_S3_DAYS}d"
log "Backup complete"
sudo chmod +x /usr/local/bin/pg-backup.sh
sudo mkdir -p /opt/backups/postgres

# Test it works
sudo /usr/local/bin/pg-backup.sh

Schedule Daily Backup with Health Check Monitoring

sudo crontab -e
# Daily backup at 2 AM with Healthchecks.io monitoring
0 2 * * * curl -fsS https://hc-ping.com/YOUR-UUID/start > /dev/null; \
          /usr/local/bin/pg-backup.sh \
          && curl -fsS https://hc-ping.com/YOUR-UUID > /dev/null \
          || curl -fsS https://hc-ping.com/YOUR-UUID/fail > /dev/null

Strategy 2: WAL Archiving for Point-in-Time Recovery

WAL (Write-Ahead Log) is PostgreSQL’s transaction log. Archiving WAL segments allows recovery to any exact moment in time — not just daily backup snapshots.

Enable WAL Archiving

sudo nano /etc/postgresql/16/main/postgresql.conf
wal_level = replica               # Required for archiving (must be 'replica' or 'logical')
archive_mode = on                 # Enable WAL archiving
archive_command = 'rclone copy %p s3:your-backup-bucket/wal/%f'
archive_timeout = 300             # Force WAL segment switch every 5 minutes (5-min max RPO)
max_wal_senders = 3               # Needed if you also use replication
sudo systemctl restart postgresql

Take a Base Backup

sudo -u postgres pg_basebackup \
    --pgdata=/opt/backups/postgres/basebackup_$(date +%Y%m%d) \
    --format=tar \
    --gzip \
    --checkpoint=fast \
    --progress

# Upload base backup to S3
rclone copy /opt/backups/postgres/basebackup_$(date +%Y%m%d) \
    s3:your-backup-bucket/basebackups/$(date +%Y%m%d)/

Point-in-Time Recovery

# In postgresql.conf (after restoring base backup):
restore_command = 'rclone copy s3:your-backup-bucket/wal/%f %p'
recovery_target_time = '2026-06-01 14:30:00 UTC'   # Recover to this exact moment
recovery_target_action = 'promote'                  # Make server primary after recovery

Strategy 3: pgBackRest for Incremental Backups

pgBackRest is the production-grade PostgreSQL backup tool — supports incremental/differential backups, parallel processing, compression, encryption, and native S3 storage.

sudo apt install -y pgbackrest

sudo nano /etc/pgbackrest/pgbackrest.conf
[global]
# S3 storage backend
repo1-type=s3
repo1-s3-bucket=your-backup-bucket
repo1-s3-endpoint=s3.amazonaws.com
repo1-s3-region=us-east-1
repo1-s3-key=YOUR_AWS_ACCESS_KEY
repo1-s3-key-secret=YOUR_AWS_SECRET_KEY
repo1-path=/pgbackrest

# Retention
repo1-retention-full=2       # Keep 2 full backups
repo1-retention-diff=7       # Keep 7 differential backups

# Security
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=YourStrongEncryptionPassphrase

# Performance
start-fast=y
compress-type=lz4

[mydb]
pg1-path=/var/lib/postgresql/16/main
# Initialize repository and take first full backup
sudo -u postgres pgbackrest --stanza=mydb stanza-create
sudo -u postgres pgbackrest --stanza=mydb --type=full backup

# Schedule in crontab:
# Full backup weekly (Sunday 1 AM)
0 1 * * 0 sudo -u postgres pgbackrest --stanza=mydb --type=full backup

# Differential backup daily Mon–Sat 1 AM
0 1 * * 1-6 sudo -u postgres pgbackrest --stanza=mydb --type=diff backup

# View available backups
sudo -u postgres pgbackrest --stanza=mydb info

Verify Backups — The Non-Negotiable Step

# Verify a pg_dump backup is readable
gunzip -c /opt/backups/postgres/pg_all_latest.sql.gz | \
    psql -U postgres -d test_restore_db --quiet
echo "Restore exit code: $?"

# Verify pgBackRest backup integrity
sudo -u postgres pgbackrest --stanza=mydb check

# Test actual restore to a different directory (non-destructive)
sudo -u postgres pgbackrest --stanza=mydb restore \
    --delta \
    --target-action=promote \
    --pg1-path=/tmp/pg_restore_test

Getting Started

Start with the pg_dump + S3 strategy — it covers most VPS use cases with minimal setup. Add WAL archiving when losing more than 1 hour of data is unacceptable. Use pgBackRest with incremental backups for databases over 50 GB. All strategies work on Ubuntu VPS at VPS.DO. Backblaze B2 is a cost-effective S3-compatible backup target at $6/TB/month.

Conclusion

Choose your PostgreSQL backup strategy based on recovery objectives: pg_dump is sufficient for small databases where 24-hour RPO is acceptable; WAL archiving enables point-in-time recovery for transactional systems; pgBackRest provides incremental backups for large databases with fast recovery requirements. The non-negotiable requirement across all strategies: verify that backups actually restore. An untested backup is not a backup you can rely on during a real incident.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!