backup-recovery

v2026.09.24

Production backup strategy and disaster recovery planning for Linux servers, covering database backups, file backups, encryption, cloud storage, retention policies, and restore runbooks. USE WHEN: - Designing or implementing a backup strategy for a production server - Writing pg_dump / mongodump / mysqldump backup scripts - Configuring rclone to sync backups to S3, Backblaze B2, or SFTP - Encrypting backup archives with GPG before uploading offsite - Setting up retention schedules (daily/weekly/monthly rotation) - Writing or testing a disaster recovery runbook - Monitoring backup success/failure with healthchecks.io DO NOT USE FOR: - Kubernetes PersistentVolume snapshots (use cloud-provider CSI snapshot tools) - Application-level data export (e.g., Stripe data export, SaaS data portability) - Code repository backups (use git push to multiple remotes) - Windows Server backup (this skill covers Linux only)

GitHub
安装命令
npx skhub add claude-dev-suite/backup-recovery
Markdown
SKILL.md

Backup & Recovery — Production Strategy

The 3-2-1 Rule

Every production backup strategy must satisfy:

  • 3 copies of data (1 primary + 2 backups)
  • 2 different storage media or locations (e.g., local disk + cloud)
  • 1 offsite copy (different geographic region from primary)
Backup TypeDescriptionUse Case
FullComplete snapshot of all dataWeekly baseline, before major upgrades
IncrementalChanges since last backup (any type)Daily; fast but requires chain for restore
DifferentialChanges since last full backupBalances restore speed vs. storage cost

Recommended schedule: Full weekly + Incremental daily with 30-day retention.


Database Backups

PostgreSQL

# Plain SQL dump (human-readable, compatible with any Postgres version)
pg_dump -U postgres -d mydb > mydb_$(date +%Y%m%d_%H%M%S).sql

# Custom format (-Fc) — compressed, supports parallel restore, preferred for large DBs
pg_dump -U postgres -Fc -d mydb -f mydb_$(date +%Y%m%d_%H%M%S).dump

# Directory format (-Fd) — parallel dump, one file per table
pg_dump -U postgres -Fd -j 4 -d mydb -f mydb_$(date +%Y%m%d_%H%M%S).dir/

# Dump globals (roles, tablespaces) — run separately
pg_dumpall -U postgres --globals-only > globals_$(date +%Y%m%d).sql

# Dump all databases
pg_dumpall -U postgres > all_databases_$(date +%Y%m%d).sql

PostgreSQL restore:

# From plain SQL
psql -U postgres -d mydb < mydb_20241201_030000.sql

# From custom format (parallel restore with -j)
pg_restore -U postgres -d mydb -j 4 --clean --if-exists mydb_20241201_030000.dump

# Create DB first if it doesn't exist
createdb -U postgres mydb
pg_restore -U postgres -d mydb mydb_20241201_030000.dump

# Restore globals first
psql -U postgres < globals_20241201.sql

MongoDB

# Full dump (BSON format)
mongodump --uri="mongodb://user:pass@localhost:27017/mydb" \
  --out=/var/backups/mongo/$(date +%Y%m%d_%H%M%S)/

# Compressed archive
mongodump --uri="mongodb://user:pass@localhost:27017/mydb" \
  --archive=/var/backups/mongo/mydb_$(date +%Y%m%d).gz \
  --gzip

# Restore
mongorestore --uri="mongodb://user:pass@localhost:27017/mydb" \
  --drop \
  --archive=/var/backups/mongo/mydb_20241201.gz \
  --gzip

MySQL / MariaDB

# Single database
mysqldump -u root -p mydb \
  --single-transaction \
  --routines \
  --triggers \
  --events \
  > mydb_$(date +%Y%m%d_%H%M%S).sql

# All databases
mysqldump -u root -p \
  --all-databases \
  --single-transaction \
  > all_databases_$(date +%Y%m%d).sql

# Restore
mysql -u root -p mydb < mydb_20241201_030000.sql

--single-transaction acquires a consistent snapshot for InnoDB without locking tables.


File Backups with rsync

# Basic archive sync (preserves permissions, timestamps, symlinks)
rsync -av /var/www/html/ /mnt/backup/html/

# With deletion (mirror: remove files deleted from source)
rsync -av --delete /var/www/html/ /mnt/backup/html/

# Exclude paths
rsync -av --delete \
  --exclude='*.log' \
  --exclude='node_modules/' \
  --exclude='.cache/' \
  /var/www/ /mnt/backup/www/

# Incremental backup using --link-dest (hardlinks unchanged files — space efficient)
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
LATEST=/mnt/backup/latest
rsync -av --delete \
  --link-dest="$LATEST" \
  /var/www/html/ \
  /mnt/backup/snapshots/$BACKUP_DATE/
# Update the "latest" symlink
ln -sfn /mnt/backup/snapshots/$BACKUP_DATE /mnt/backup/latest

# Remote sync over SSH (bandwidth-limited to 10 MB/s)
rsync -av --delete \
  --bwlimit=10000 \
  --partial \
  -e "ssh -i /root/.ssh/backup_key -p 22" \
  /var/www/html/ \
  backup-user@backup-server.example.com:/backups/html/

rclone for Cloud Storage

Installation and Configuration

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

# Interactive config
rclone config
# Creates ~/.config/rclone/rclone.conf

~/.config/rclone/rclone.conf:

[b2]
type = b2
account = your-b2-account-id
key = your-b2-application-key

[s3]
type = s3
provider = AWS
access_key_id = AKIAIOSFODNN7EXAMPLE
secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region = us-east-1

[sftp-backup]
type = sftp
host = backup-server.example.com
user = backup
key_file = /root/.ssh/backup_key
port = 22

rclone Commands

# Copy (source → dest, skip existing identical files)
rclone copy /var/backups/ b2:my-bucket/server-backups/ --progress

# Sync (make destination identical to source — DELETES files not in source)
rclone sync /var/backups/ b2:my-bucket/server-backups/ \
  --progress \
  --bwlimit 50M

# Copy with bandwidth limit and filtering
rclone copy /var/backups/ s3:my-bucket/backups/ \
  --bwlimit 20M \
  --include "*.gpg" \
  --progress

# List remote contents
rclone ls b2:my-bucket/server-backups/

# Check (verify checksums between source and destination)
rclone check /var/backups/ b2:my-bucket/server-backups/

Encryption with GPG

Never upload unencrypted backups containing sensitive data to cloud storage.

# Symmetric encryption (single passphrase) — simpler, good for personal use
gpg --symmetric \
    --cipher-algo AES256 \
    --compress-algo none \
    mydb_backup.dump
# Produces: mydb_backup.dump.gpg

# Asymmetric encryption (public key) — better for automation (no passphrase at encrypt time)
# Generate key pair once: gpg --full-gen-key
gpg --encrypt \
    --recipient backup-key@example.com \
    mydb_backup.dump

# Decrypt symmetric
gpg --decrypt mydb_backup.dump.gpg > mydb_backup.dump

# Decrypt asymmetric (requires private key)
gpg --decrypt mydb_backup.dump.gpg > mydb_backup.dump

# Pipe backup directly through encryption (no plaintext file on disk)
pg_dump -U postgres -Fc mydb | \
  gpg --symmetric --cipher-algo AES256 --compress-algo none \
  > /var/backups/mydb_$(date +%Y%m%d).dump.gpg

Complete Production Backup Script

/opt/scripts/backup-postgres.sh:

#!/bin/bash
# Production PostgreSQL backup: dump → encrypt → upload to S3 → cleanup → ping
set -euo pipefail

# ── Configuration ────────────────────────────────────────────────────────────
DB_NAME="mydb"
DB_USER="postgres"
BACKUP_DIR="/var/backups/postgres"
RCLONE_DEST="s3:my-backup-bucket/postgres"
GPG_PASSPHRASE_FILE="/etc/backup/gpg-passphrase"   # chmod 600, owned by backup user
HEALTHCHECK_URL="https://hc-ping.com/YOUR-CHECK-UUID"
RETAIN_LOCAL_DAYS=7
RETAIN_REMOTE_DAYS=30
LOG_FILE="/var/log/backup-postgres.log"

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

cleanup() {
  local exit_code=$?
  if [[ $exit_code -ne 0 ]]; then
    log "BACKUP FAILED (exit $exit_code) — sending failure ping"
    curl -fsS --retry 3 "${HEALTHCHECK_URL}/fail" > /dev/null || true
  fi
}
trap cleanup EXIT

# ── Ping start ────────────────────────────────────────────────────────────────
curl -fsS --retry 3 "${HEALTHCHECK_URL}/start" > /dev/null || true

# ── Backup ───────────────────────────────────────────────────────────────────
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
FILENAME="${DB_NAME}_${TIMESTAMP}.dump.gpg"
BACKUP_PATH="${BACKUP_DIR}/${FILENAME}"

mkdir -p "$BACKUP_DIR"
log "Starting backup of database: $DB_NAME"

pg_dump -U "$DB_USER" -Fc "$DB_NAME" | \
  gpg --batch --yes \
      --passphrase-file "$GPG_PASSPHRASE_FILE" \
      --symmetric \
      --cipher-algo AES256 \
      --compress-algo none \
  > "$BACKUP_PATH"

BACKUP_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1)
log "Backup complete: $FILENAME ($BACKUP_SIZE)"

# ── Upload to S3 ─────────────────────────────────────────────────────────────
log "Uploading to $RCLONE_DEST"
rclone copy "$BACKUP_PATH" "$RCLONE_DEST/" \
  --progress \
  --log-file="$LOG_FILE" \
  --log-level INFO

log "Upload complete"

# ── Local retention cleanup ───────────────────────────────────────────────────
log "Removing local backups older than ${RETAIN_LOCAL_DAYS} days"
find "$BACKUP_DIR" -name "*.dump.gpg" -mtime +"$RETAIN_LOCAL_DAYS" -delete

# ── Remote retention cleanup ──────────────────────────────────────────────────
log "Removing remote backups older than ${RETAIN_REMOTE_DAYS} days"
rclone delete "$RCLONE_DEST" \
  --min-age "${RETAIN_REMOTE_DAYS}d" \
  --include "*.dump.gpg"

# ── Success ping ──────────────────────────────────────────────────────────────
curl -fsS --retry 3 "$HEALTHCHECK_URL" > /dev/null || true
log "Backup job finished successfully"

Crontab entry:

0 2 * * * backup-user /opt/scripts/backup-postgres.sh >> /var/log/backup-cron.log 2>&1

Retention and Rotation Schedule

TypeFrequencyKeep CountTotal Retained
DailyEvery night at 02:0077 days
WeeklyEvery Sunday at 03:0044 weeks
Monthly1st of month at 04:0033 months

Shell snippet for rotation logic:

# Keep only last N files matching pattern (sorted by modification time)
keep_last_n() {
  local dir="$1" pattern="$2" keep="$3"
  ls -t "$dir"/$pattern 2>/dev/null | tail -n +$((keep + 1)) | xargs -r rm --
}
keep_last_n /var/backups/postgres "*.dump.gpg" 7

AWS IAM Policy for Backup User

Minimal S3 permissions — no ListAllMyBuckets, no delete on critical prefixes:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::my-backup-bucket/postgres/*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::my-backup-bucket",
      "Condition": {
        "StringLike": {"s3:prefix": ["postgres/*"]}
      }
    }
  ]
}

Disaster Recovery Concepts

RTO (Recovery Time Objective): Maximum acceptable downtime after a disaster. Drives decisions about hot/warm/cold standby and restore automation.

RPO (Recovery Point Objective): Maximum acceptable data loss measured in time. Drives backup frequency.

ScenarioTarget RTOTarget RPOStrategy
Accidental data deletion1 hour24 hoursDaily backup + point-in-time restore
Server hardware failure4 hours24 hoursOffsite backup + server rebuild runbook
Datacenter outage24 hours24 hoursCross-region offsite backup
Ransomware24–48 hours24 hoursImmutable offsite backup (object lock)

Restore Runbook Template

# DISASTER RECOVERY RUNBOOK — [Service Name]
# Last tested: [DATE]  Tested by: [NAME]

## Step 1: Provision New Server
- [ ] Provision server (size: ____, region: ____)
- [ ] Apply Terraform/Ansible baseline configuration
- [ ] Verify SSH access and firewall rules

## Step 2: Install Dependencies
- [ ] sudo apt install postgresql-16 (or appropriate version)
- [ ] Install application dependencies from runbook in DEPLOY.md

## Step 3: Download Latest Backup
  rclone copy s3:my-backup-bucket/postgres/ /var/restore/ --include "*.dump.gpg"
  # Identify most recent file
  ls -lt /var/restore/ | head -5

## Step 4: Decrypt
  gpg --batch --passphrase-file /etc/backup/gpg-passphrase \
      --decrypt /var/restore/mydb_YYYYMMDD_HHMMSS.dump.gpg \
      > /var/restore/mydb.dump

## Step 5: Restore Database
  createdb -U postgres mydb
  pg_restore -U postgres -d mydb -j 4 --clean --if-exists /var/restore/mydb.dump

## Step 6: Verify Data
  psql -U postgres -d mydb -c "SELECT COUNT(*) FROM users;"
  psql -U postgres -d mydb -c "SELECT MAX(created_at) FROM orders;"

## Step 7: Start Application
  systemctl start myapp
  curl -f http://localhost:3000/health

## Step 8: Update DNS / Load Balancer
  - [ ] Point DNS A record to new server IP
  - [ ] Update load balancer target group
  - [ ] Verify external access

## Step 9: Post-Recovery
  - [ ] Set up monitoring on new server
  - [ ] Schedule first backup of new server
  - [ ] Write incident report
  - [ ] Update runbook if steps were incorrect

Anti-Patterns

Anti-PatternProblemFix
No offsite backupLocal disaster (fire, ransomware) destroys all copiesAlways push to a different geographic region using rclone
Never testing restoresBackup files are corrupt or incomplete — discovered only during a disasterMonthly restore drill to a test environment
Unencrypted sensitive data uploaded to cloudPII, credentials, financial data exposed if bucket becomes publicPipe all backups through GPG before uploading
No monitoring of backup successBackup script fails silently for weeksIntegrate healthchecks.io; alert on missing check-in
Storing backup credentials in backup scriptCredential leak via script in git or logsUse separate credentials file with chmod 600, or AWS IAM role
Backing up to same disk as primary dataDisk failure takes both primary and backupUse a separate disk, remote server, or cloud storage
No retention policyStorage fills up and script starts failingAlways implement find -mtime +N -delete or rclone --min-age
pg_dump without --single-transaction for InnoDBInconsistent backup if tables change during dumpAlways use --single-transaction for live database dumps
Backing up only the application, not the databaseRestore brings back code but data is months oldInclude both database dumps and file backups in every backup job

Troubleshooting

SymptomLikely CauseDiagnostic & Fix
pg_restore: error: could not execute querySchema version mismatch or missing extensionsCheck Postgres version; restore globals first; ensure extensions installed
pg_dump: error: query was canceled due to conflictReplication conflict on a hot standbyUse --no-password --lock-wait-timeout=10s or dump from primary
rclone 403 ForbiddenIAM key lacks permission or wrong bucket namerclone lsd s3: to test credentials; verify IAM policy covers the bucket/prefix
rclone 429 Too Many RequestsB2 or S3 rate limit hitAdd --tpslimit 10 to rclone command; reduce parallelism with --transfers 4
Backup taking too longLarge database, no compression, slow networkUse -Fc for pg_dump (built-in compression); add --bwlimit to avoid saturating connection
Restore takes too longSingle-threaded restore of large custom-format dumpUse pg_restore -j 4 for parallel restore
GPG decryption failsWrong passphrase file or key not importedgpg --list-keys; verify passphrase file contents (no trailing newline issues)
Healthchecks.io shows "Late" statusCron job not running, or curl call before error trap firesCheck crontab, verify backup user's cron, ensure curl is in PATH
S3 bucket size growing unboundedRetention cleanup script not running or misconfiguredTest cleanup manually with rclone delete --dry-run; check rclone logs
find: No such file or directory in cleanupBackup directory path wrong in cleanup commandLog $BACKUP_DIR before cleanup; add mkdir -p at script start
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

2026年9月24日

分类

未分类

许可证

MIT

源路径

skills/infrastructure/backup-recovery

默认分支

main

最新提交

9496306

Tree SHA

fe4e2f1