VPS Backup Automation: How to Build a Complete Backup Pipeline with Shell Scripts

Losing data on a VPS is not a matter of if but when. Disk failure, accidental rm -rf, security breaches, or provider outages can wipe out critical data in seconds. A robust automated backup strategy is your safety net. This guide walks through building a complete backup pipeline with shell scripts — supporting databases, files, and off-site storage — all running on your VPS without third-party backup agents.

Why Automate Backups on Your VPS?

Manual backups are unreliable. You forget. You get busy. You convince yourself it won’t happen today. Automation removes human error from the equation. A well-designed backup pipeline gives you:

  • Recovery point objective (RPO) control — How much data you are willing to lose (e.g., 1 hour, 24 hours).
  • Recovery time objective (RTO) — How fast you can restore from backup.
  • Off-site redundancy — Backups stored on a different VPS or cloud storage, safe from local catastrophes.
  • Point-in-time recovery — The ability to restore to any previous backup snapshot.

When evaluating VPS providers for your backup infrastructure, visit our VPS comparison page to find plans with sufficient storage and bandwidth for backup operations.

Backup Architecture Overview

Our backup pipeline follows the 3-2-1 rule: three copies of your data, on two different media types, with one copy off-site. The pipeline consists of four stages:

  • Stage 1 — Database dump (MySQL/PostgreSQL)
  • Stage 2 — Filesystem archive (tar/gzip)
  • Stage 3 — Encryption (GPG or openssl)
  • Stage 4 — Off-site transfer (rsync/rclone/S3)

Stage 1: Database Backup Script

Create /usr/local/bin/backup-db.sh:

#!/bin/bash
# Database backup script for MySQL/MariaDB
set -euo pipefail

DB_HOST="localhost"
DB_USER="backup_user"
DB_PASS="$(cat /etc/backup/mysql_password)"
BACKUP_DIR="/var/backups/db"
RETENTION_DAYS=30
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

# List all databases except system ones
DATABASES=$(mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS"   -e "SHOW DATABASES;" | grep -Ev "^(Database|information_schema|performance_schema|mysql|sys)$")

for DB in $DATABASES; do
  echo "Backing up: $DB"
  mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS"     --single-transaction --quick --lock-tables=false     "$DB" | gzip > "$BACKUP_DIR/${DB}_${TIMESTAMP}.sql.gz"

  # Verify backup integrity
  gunzip -t "$BACKUP_DIR/${DB}_${TIMESTAMP}.sql.gz" || {
    echo "ERROR: Backup verification failed for $DB"
    rm -f "$BACKUP_DIR/${DB}_${TIMESTAMP}.sql.gz"
  }
done

# Clean old backups
find "$BACKUP_DIR" -name "*.sql.gz" -type f -mtime +$RETENTION_DAYS -delete

echo "Database backup completed at $(date)"

Make it executable: sudo chmod +x /usr/local/bin/backup-db.sh

Stage 2: Filesystem Backup Script

Create /usr/local/bin/backup-files.sh:

#!/bin/bash
# Incremental filesystem backup using rsync
set -euo pipefail

BACKUP_DIR="/var/backups/files"
SNAPSHOT_DIR="$BACKUP_DIR/current"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
ARCHIVE_DIR="$BACKUP_DIR/archive/$TIMESTAMP"

# Directories to back up (customize for your stack)
SOURCE_DIRS=(
  "/etc/nginx"
  "/etc/letsencrypt"
  "/var/www"
  "/home"
  "/etc/ssh"
)

mkdir -p "$SNAPSHOT_DIR" "$BACKUP_DIR/archive"

for DIR in "${SOURCE_DIRS[@]}"; do
  if [ -d "$DIR" ]; then
    rsync -a --delete --link-dest="$SNAPSHOT_DIR" "$DIR" "$BACKUP_DIR/staging/"
  fi
done

# Create a dated hard-link snapshot
cp -al "$BACKUP_DIR/staging" "$ARCHIVE_DIR"

# Create a compressed archive for off-site transfer
tar czf "$BACKUP_DIR/archive/files_${TIMESTAMP}.tar.gz"   -C "$BACKUP_DIR" "$TIMESTAMP"

echo "Filesystem backup completed at $(date)"

This uses rsync’s --link-dest for efficient incremental backups — each snapshot is a full directory tree, but unchanged files are hard links.

Stage 3: Encryption

Never store backups unencrypted, especially off-site. Create an encryption wrapper:

#!/bin/bash
# encrypt-backup.sh - Encrypt a backup file with GPG
set -euo pipefail

INPUT_FILE="$1"
GPG_RECIPIENT="[email protected]"

if [ ! -f "$INPUT_FILE" ]; then
  echo "Usage: $0 <backup_file>"
  exit 1
fi

gpg --encrypt --recipient "$GPG_RECIPIENT" \
  --output "${INPUT_FILE}.gpg" "$INPUT_FILE"

# Securely delete the original
shred -u "$INPUT_FILE"

echo "Encrypted: ${INPUT_FILE}.gpg"

Stage 4: Off-Site Transfer

Create /usr/local/bin/backup-transfer.sh to push backups to off-site storage. Options include:

  • rclone — Supports 40+ cloud storage backends (S3, Backblaze B2, Google Drive, Dropbox)
  • scp/rsync — To a second VPS in a different datacenter
  • S3 CLI — For AWS S3 or compatible storage
#!/bin/bash
# Transfer backups to off-site storage via rclone
set -euo pipefail

RCLONE_REMOTE="backups3"
RCLONE_PATH="vps-backups/$(hostname)"
BACKUP_DIR="/var/backups"

# rclone config must be set up beforehand:
# rclone config create backups3 s3 provider=aws env_auth=true

rclone copy "$BACKUP_DIR/db" "$RCLONE_REMOTE:$RCLONE_PATH/db" --progress
rclone copy "$BACKUP_DIR/files" "$RCLONE_REMOTE:$RCLONE_PATH/files" --progress

echo "Off-site transfer completed at $(date)"

Orchestrating with a Master Script

Create /usr/local/bin/backup-all.sh that chains everything together:

#!/bin/bash
# Master backup orchestration script
set -euo pipefail

LOG_FILE="/var/log/backup.log"
NOTIFY_EMAIL="[email protected]"

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

log "Starting backup pipeline..."

# Stage 1: Database
log "Stage 1: Database backup"
/usr/local/bin/backup-db.sh 2>&1 | tee -a "$LOG_FILE"

# Stage 2: Filesystem
log "Stage 2: Filesystem backup"
/usr/local/bin/backup-files.sh 2>&1 | tee -a "$LOG_FILE"

# Stage 3: Encrypt
log "Stage 3: Encryption"
find /var/backups/db -name "*.sql.gz" -mmin -10 | while read f; do
  /usr/local/bin/encrypt-backup.sh "$f"
done

# Stage 4: Transfer
log "Stage 4: Off-site transfer"
/usr/local/bin/backup-transfer.sh 2>&1 | tee -a "$LOG_FILE"

log "Backup pipeline completed successfully"

# Send notification
mail -s "Backup Complete: $(hostname) on $(date '+%Y-%m-%d')" "$NOTIFY_EMAIL" < "$LOG_FILE"

Cron Schedule

Add these entries to your crontab (sudo crontab -e):

# Hourly database backup (point-in-time recovery)
0 * * * * /usr/local/bin/backup-db.sh

# Daily full backup pipeline at 2 AM
0 2 * * * /usr/local/bin/backup-all.sh

# Weekly integrity check
0 3 * * 0 /usr/local/bin/verify-backups.sh

Restoring from Backup

A backup is only as good as its restore process. Create a restore script and test it regularly:

#!/bin/bash
# restore-db.sh - Restore a database from backup
set -euo pipefail

BACKUP_FILE="$1"
DB_NAME="$2"

if [ ! -f "$BACKUP_FILE" ]; then
  echo "Backup file not found: $BACKUP_FILE"
  exit 1
fi

# Decrypt if needed
if [[ "$BACKUP_FILE" == *.gpg ]]; then
  DECRYPTED="${BACKUP_FILE%.gpg}"
  gpg --decrypt --output "$DECRYPTED" "$BACKUP_FILE"
  BACKUP_FILE="$DECRYPTED"
fi

# Drop existing connections and restore
echo "DROP DATABASE IF EXISTS $DB_NAME; CREATE DATABASE $DB_NAME;" | mysql -u root
gunzip -c "$BACKUP_FILE" | mysql -u root "$DB_NAME"

echo "Database $DB_NAME restored from $BACKUP_FILE"

Automated Backup Testing with Docker

To truly verify backups, restore them to a sandbox environment. Docker makes this easy:

#!/bin/bash
# verify-backups.sh - Test database backup integrity
docker run --rm -v /var/backups/db:/backups mysql:8.0 \
  bash -c "gunzip -c /backups/db_test_20260725.sql.gz | mysql -h testhost -u test -ptest testdb"

For managed VPS solutions with built-in automated backups, check Cloudways managed hosting which includes one-click backup and restore features. For affordable VPS plans with ample storage for local backups, our VPS comparison tool can help you find the right provider.

Conclusion

You now have a complete, automated backup pipeline running on your VPS — covering database dumps, filesystem snapshots, encryption, off-site transfer, and automated restore verification. The 3-2-1 rule is fully implemented with minimal dependencies and zero licensing cost. The key to backup reliability is consistency: schedule it, log it, monitor it, and most importantly, test your restores regularly. A backup that has not been tested is just a wish.

Leave a Reply