The 3-2-1 backup rule is the gold standard: maintain three copies of data, on two different media types, with one copy offsite. This guide shows how to implement this strategy practically on VPS infrastructure using open-source tools like restic, rclone, and ZFS.
Understanding 3-2-1
- 3 copies: Original data + 2 backups
- 2 media types: Local disk + object storage (or separate VPS)
- 1 offsite: Geographically separate location
This protects against: hardware failure (2 copies), site disaster (offsite), ransomware (versioned/immutable backups), and human error (multiple restore points).
Copy 1: Local Backups with restic
# Install restic
sudo apt install restic
# Initialize local backup repository
restic init --repo /backup/local
# Create backup script
#!/bin/bash
# /usr/local/bin/backup-local.sh
export RESTIC_REPOSITORY=/backup/local
export RESTIC_PASSWORD_FILE=/etc/restic/password
# Backup important directories
restic backup \
/var/www \
/etc \
/home \
/opt/apps \
--exclude="*.log" \
--exclude="node_modules" \
--exclude=".cache"
# Apply retention policy
restic forget \
--keep-hourly 24 \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 12 \
--prune
# Verify integrity
restic check
Copy 2: Remote VPS with restic + rclone
# Configure rclone for remote storage
rclone config
# Choose: sftp
# Host: backup-vps.example.com
# User: backup
# Key file: /root/.ssh/backup_key
# Initialize remote repository
restic -r rclone:backup-vps:/restic-repo init
# Backup to remote
#!/bin/bash
# /usr/local/bin/backup-remote.sh
export RESTIC_REPOSITORY="rclone:backup-vps:/restic-repo"
export RESTIC_PASSWORD_FILE=/etc/restic/password
restic backup /var/www /etc /home /opt/apps \
--exclude="*.log" --exclude="node_modules"
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
Copy 3: Object Storage (Offsite)
# Configure rclone for S3-compatible storage
rclone config
# Choose: s3
# Provider: Backblaze B2 / Wasabi / AWS S3
# Access Key: your-key
# Secret Key: your-secret
# Region: us-east-1
# Endpoint: s3.wasabisys.com (for Wasabi)
# Initialize S3 repository
restic -r s3:s3.wasabisys.com/my-backups init
# Backup to S3
#!/bin/bash
# /usr/local/bin/backup-offsite.sh
export RESTIC_REPOSITORY="s3:s3.wasabisys.com/my-backups"
export RESTIC_PASSWORD_FILE=/etc/restic/password
export AWS_ACCESS_KEY_ID="your-key"
export AWS_SECRET_ACCESS_KEY="your-secret"
restic backup /var/www /etc /home /opt/apps \
--exclude="*.log" --exclude="node_modules"
restic forget --keep-daily 30 --keep-weekly 12 --keep-monthly 24 --prune
Database Backups
# MySQL/MariaDB dump before restic backup
#!/bin/bash
DUMP_DIR="/backup/db-dumps"
mkdir -p "$DUMP_DIR"
# Dump all databases
mysqldump --all-databases --single-transaction --quick \
--triggers --routines --events \
| gzip > "$DUMP_DIR/mysql-$(date +%Y%m%d-%H%M).sql.gz"
# PostgreSQL
pg_dumpall | gzip > "$DUMP_DIR/postgres-$(date +%Y%m%d-%H%M).sql.gz"
# Clean old dumps (restic handles versioning)
find "$DUMP_DIR" -name "*.sql.gz" -mtime +2 -delete
Automation
# Cron schedule
# /etc/cron.d/backups
# Local backup every 6 hours
0 */6 * * * root /usr/local/bin/backup-local.sh >> /var/log/backup-local.log 2>&1
# Remote VPS backup daily at 3am
0 3 * * * root /usr/local/bin/backup-remote.sh >> /var/log/backup-remote.log 2>&1
# Offsite S3 backup daily at 4am
0 4 * * * root /usr/local/bin/backup-offsite.sh >> /var/log/backup-offsite.log 2>&1
# Database dumps before backups
30 2 * * * root /usr/local/bin/backup-databases.sh >> /var/log/backup-db.log 2>&1
Monitoring and Alerting
#!/bin/bash
# /usr/local/bin/backup-check.sh
# Run daily to verify backups are current
check_backup() {
local name=$1 repo=$2
local latest=$(restic -r "$repo" snapshots --latest 1 --json | jq -r ".[0].time")
local age_hours=$(( ($(date +%s) - $(date -d "$latest" +%s)) / 3600 ))
if [ "$age_hours" -gt 48 ]; then
echo "ALERT: $name backup is ${age_hours}h old!"
# Send alert via email, Slack, etc.
fi
}
export RESTIC_PASSWORD_FILE=/etc/restic/password
check_backup "Local" "/backup/local"
check_backup "Remote" "rclone:backup-vps:/restic-repo"
Restore Testing
# Monthly restore test (critical!)
# Restore to temporary directory
restic -r /backup/local restore latest --target /tmp/restore-test/
# Verify key files exist and are readable
diff /etc/nginx/nginx.conf /tmp/restore-test/etc/nginx/nginx.conf
diff /var/www/app/config.php /tmp/restore-test/var/www/app/config.php
# Clean up
rm -rf /tmp/restore-test/
# Document restore time for your disaster recovery plan
Summary
The 3-2-1 strategy implemented with restic provides encrypted, deduplicated, versioned backups across three locations. Local backups enable fast restores, remote VPS backups protect against hardware failure, and S3 offsite backups survive site-level disasters. The key to success is automation (cron), monitoring (alert on stale backups), and regular restore testing (monthly). With Wasabi or Backblaze B2 for offsite storage, the entire strategy costs as little as $5-10/month for typical VPS workloads.