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
You must be logged in to post a comment.