PostgreSQL backup tự động trên VPS qua pg_dump + cron + S3

Mục lục
TL;DR
  • pg_dump + cron + s3cmd/rclone là combo backup Postgres ổn định cho dev solo, không cần Patroni hay Barman.
  • Backup daily, retention 7-30 ngày, encrypt với GPG hoặc age, push lên S3-compatible (R2, B2, Wasabi, MinIO).
  • Restore một lệnh: download .sql.gz từ S3, gunzip pipe psql. Test restore hàng tháng để biết backup work.
  • Chi phí: 100GB backup retention 30 ngày trên Backblaze B2 ~5 USD/tháng. Trên R2 Cloudflare gần free 10GB.
  • Bài này hướng dẫn full: script bash, encryption, cron, monitor success/fail, restore demo, alert khi backup miss.

Backup database là việc dev hay quên cho đến lúc xảy ra disaster. DROP TABLE accident, ransomware encrypt VPS, người team cũ truy cập rồi xóa data, hardware fail. Khi đó còn lại duy nhất một câu hỏi: backup gần nhất là khi nào, restore được không. Bài này hướng dẫn setup pipeline backup Postgres tự động lên S3-compatible bucket, từ A đến Z, theo cách dev solo có thể duy trì 5 phút mỗi tháng.

Test trên Cloud VPS 50 (4GB RAM) chạy Postgres 16, backup mỗi đêm lên Backblaze B2 (R2 cũng tương tự). Sau bài này bạn có một script bash chạy cron, retention policy 30 ngày, encryption GPG, alert Telegram khi fail, và script restore one-liner sẵn dùng khi cần.

Chọn loại backup: pg_dump vs pg_basebackup vs WAL archive

  • pg_dump: logical backup, output SQL hoặc custom format. Dễ restore từng bảng, dễ migrate giữa version. Chậm khi DB >50GB.
  • pg_basebackup: physical backup, snapshot file data. Restore nhanh, nhưng restore phải cùng major version Postgres.
  • WAL archive: cho phép PITR (Point In Time Recovery). Phức tạp setup, cần pgBackRest hoặc Barman.

Cho dev solo, pg_dump là đủ và dễ nhất. Đến khi DB lớn 100GB+ hoặc cần RPO 5 phút mới cần WAL archive. Bài này tập trung pg_dump.

Bước 1: Tạo Backblaze B2 bucket (hoặc R2)

  1. Đăng ký Backblaze B2 (10GB free, 5 USD/100GB sau đó).
  2. Tạo bucket "my-pg-backups", private.
  3. Tạo Application Key với quyền read+write chỉ bucket này, copy keyID + applicationKey.

Tương tự cho Cloudflare R2 (10GB free, không egress fee), Wasabi (6 USD/TB), AWS S3 (23 USD/TB).

Bước 2: Cài rclone (universal S3 client)

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

Setup remote cho B2:

rclone config
# n -> new remote
# name: b2
# Storage: 6 (Backblaze B2)
# account: $YOUR_KEY_ID
# key: $YOUR_APP_KEY
# (rest defaults)

Test:

rclone lsd b2:
rclone touch b2:my-pg-backups/test.txt
rclone ls b2:my-pg-backups

Bước 3: Generate GPG key cho encryption

Backup encrypt phòng trường hợp bucket bị compromise:

gpg --batch --passphrase '' --quick-gen-key [email protected] default 0
gpg --list-keys [email protected]
# Lưu private key để restore sau:
gpg --export-secret-keys -a [email protected] > backup-key-secret.asc
# Backup file backup-key-secret.asc vào nơi khác (Google Drive, USB).

QUAN TRỌNG: lưu file secret key ở nơi khác VPS. Mất key = mất backup mãi mãi.

Bước 4: Script backup

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

DATE=$(date +%F-%H%M)
DB_LIST="myapp_prod analytics_db wordpress_db"
BACKUP_DIR=/tmp/pg-backup
mkdir -p $BACKUP_DIR
LOG=/var/log/pg-backup.log
exec >> $LOG 2>&1

echo "=== Backup start: $DATE ==="

for DB in $DB_LIST; do
    FILE=$BACKUP_DIR/$DB-$DATE.sql.gz.gpg
    sudo -u postgres pg_dump -Fc $DB 
        | gzip -9 
        | gpg --batch --yes --trust-model always 
              --encrypt -r [email protected] -o $FILE
    SIZE=$(du -h $FILE | cut -f1)
    echo "$DB: $SIZE"

    rclone copy $FILE b2:my-pg-backups/$(date +%Y/%m)/
    rm $FILE
done

# Cleanup remote: giữ 30 ngày
rclone delete --min-age 30d b2:my-pg-backups/

# Notify Telegram
curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" 
    -d "chat_id=$CHAT_ID" 
    -d "text=Postgres backup OK: $DATE, dbs: $DB_LIST"

echo "=== Backup end: $(date +%F-%H%M) ==="

Cấp quyền chạy:

sudo chmod +x /usr/local/bin/pg-backup.sh
sudo /usr/local/bin/pg-backup.sh

Lần đầu chạy xem có lỗi không. Kiểm tra bucket: rclone ls b2:my-pg-backups. Phải thấy file .sql.gz.gpg đã upload.

Bước 5: Cron schedule

# sudo crontab -e
0 3 * * * /usr/local/bin/pg-backup.sh
0 4 * * 0 /usr/local/bin/pg-backup-weekly.sh
0 5 1 * * /usr/local/bin/pg-backup-monthly.sh

Daily 3 giờ sáng. Weekly Chủ nhật 4 giờ với retention 90 ngày. Monthly ngày 1 với retention 1 năm. Đa tầng giúp restore tới bất kỳ điểm thời gian gần.

Bước 6: Alert khi backup miss

Notify khi backup OK chưa đủ. Cần biết khi backup không chạy (cron fail, server down). Setup dead-man switch:

  • Tạo monitor trên healthchecks.io (free 20 check).
  • Sửa cuối script backup: curl -fsS --retry 3 https://hc-ping.com/$YOUR_UUID.
  • Cấu hình healthchecks ping mỗi 26 giờ (cho phép trễ 2 giờ).
  • Nếu không nhận ping, healthchecks gửi email/Slack alert.

Cách này phát hiện cả khi VPS down hoàn toàn (cron không chạy được), khác với notification trong script (chỉ chạy khi script success).

Bước 7: Script restore

#!/bin/bash
# /usr/local/bin/pg-restore.sh
set -e

if [ $# -ne 2 ]; then
    echo "Usage: $0  "
    echo "Example: $0 myapp_prod myapp_prod-2026-05-22-0300.sql.gz.gpg"
    exit 1
fi

DB=$1
FILE=$2
TMPDIR=$(mktemp -d)
cd $TMPDIR

echo "Downloading from B2..."
rclone copy b2:my-pg-backups/2026/05/$FILE ./

echo "Decrypting..."
gpg --batch --yes --decrypt $FILE > backup.sql.gz

echo "Stopping app to prevent writes..."
pm2 stop all || true

echo "Restoring (using pg_restore for custom format)..."
sudo -u postgres dropdb --if-exists ${DB}_restore
sudo -u postgres createdb ${DB}_restore
gunzip -c backup.sql.gz | sudo -u postgres pg_restore -d ${DB}_restore

echo "Verifying row counts..."
sudo -u postgres psql -d ${DB}_restore -c "SELECT COUNT(*) FROM users LIMIT 1;"

echo "Done. Restored to ${DB}_restore. Rename when ready:"
echo "  ALTER DATABASE $DB RENAME TO ${DB}_old;"
echo "  ALTER DATABASE ${DB}_restore RENAME TO $DB;"

Restore vào DB mới trước, verify, rồi rename. Tránh ghi đè production DB ngay lập tức.

Bước 8: Test restore hàng tháng

Backup chỉ có giá trị khi restore được. Test định kỳ:

  1. Restore vào một staging server hoặc local docker postgres.
  2. Verify số dòng các bảng quan trọng (users, orders, payments).
  3. Chạy query sanity: SELECT MAX(id) FROM users so với production.
  4. Log lần test vào sheet, đánh dấu PASS/FAIL.

Trong 10 năm vận hành database, các đợt fail restore thường do: GPG key bị mất, format pg_dump không tương thích version, quên include schema special. Test thường xuyên là cách duy nhất để phát hiện sớm.

Tối ưu disk và bandwidth

  • Compress -9: tăng CPU nhưng giảm bandwidth + storage 30-50 phần trăm.
  • --exclude-table cho bảng log: bảng audit_log thường lớn nhất và không cần restore.
  • Stream qua pipe: không lưu file intermediate trên disk, save IO và disk space.
  • Incremental backup với pgBackRest: chỉ gửi WAL mới mỗi 15 phút, daily full restore base.

Multi-region backup

Một bucket có thể fail. Setup multi-region:

rclone copy $FILE b2:my-pg-backups/  # primary US
rclone copy $FILE r2:my-pg-backups/  # secondary EU
rclone copy $FILE local-nas:/backup/pg/  # local NAS thứ 3

3-2-1 rule: 3 copy, 2 storage type khác, 1 offsite. Backup là khoản đầu tư rẻ nhất để mua giấc ngủ ngon.

Chi phí thực tế

ProviderFreeSau freeEgress
Backblaze B210 GB6 USD/TB1 USD/TB
Cloudflare R210 GB15 USD/TBFree
WasabiNone6 USD/TBFree (under storage)
AWS S35 GB / 12 tháng23 USD/TB90 USD/TB
TND Cloud StorageTùy góiTùy góiTrong nước nhanh

Cho dev solo dưới 100GB backup: B2 hoặc R2 đều rẻ. R2 free egress nên ưu tiên khi cần restore thường xuyên (test).

Encrypted backup: GPG vs age vs rclone crypt

  • GPG: truyền thống, hỗ trợ rộng, khó quản key.
  • age: mới, đơn giản, key X25519 ngắn gọn. Khuyến nghị cho project mới.
  • rclone crypt: tích hợp trong rclone, encrypt khi upload, không cần command riêng.

Một trong ba đều đảm bảo bucket compromise không lộ data. Bắt buộc dùng ít nhất 1 nếu chứa PII hay credit card data.

FAQ

pg_dump có lock bảng không?

pg_dump dùng REPEATABLE READ isolation, không block write thông thường. App vẫn insert/update bình thường trong khi dump chạy. Chỉ block DDL (ALTER TABLE, DROP). Cho production dump nhỏ-vừa, không cần worry. DB lớn (>50GB) thì dump trên replica để tránh load.

Backup tốn bao nhiêu RAM và CPU?

pg_dump tốn ~100-300MB RAM cho DB cỡ trung. Gzip -9 tốn CPU rõ (50-80 phần trăm 1 core trong vài phút). Đặt cron 3 giờ sáng khi traffic thấp, ảnh hưởng user gần 0.

Khi VPS bị compromise, attacker có xóa được backup không?

Nếu rclone token có quyền delete: có. Workaround: tạo 2 set credential. Set 1 chỉ có write (cho cron upload). Set 2 có delete (chỉ admin biết, dùng để cleanup retention thủ công hoặc qua bucket lifecycle policy).

Có thể backup nhiều VPS vào cùng bucket không?

Có. Đặt prefix riêng cho mỗi VPS: my-pg-backups/vps-a/, my-pg-backups/vps-b/. Một bucket, nhiều origin. Quản lý cleanup retention dễ với rclone --include-from.

Backup DB lớn 200GB+ nên dùng gì?

pg_dump không phù hợp (chậm, single-threaded). Dùng pgBackRest: incremental backup, parallel processing, retention policy built-in, archive WAL liên tục. Setup phức tạp hơn nhưng đáng cho DB lớn.

Cloud VPS cho vibe coder

Cloud VPS có snapshot 1-click, kết hợp pg_dump + S3 backup cho an toàn tối đa

Cloud VPS TND sẵn AlmaLinux 9, Ubuntu 22/24, Debian 12/13. SSD CEPH, snapshot 1-click, backup hằng ngày, network 200Mbps trong nước. Snapshot bảo vệ khỏi corruption disk, pg_dump S3 bảo vệ khỏi accident DROP và ransomware.

Xem 8 cấu hình Cloud VPS →

Chia sẻ bài viết