Snapshots at your VPS provider are convenient but not enough — if the provider has an outage, you lose access to both the VPS and its snapshots. Off-site backups solve that. This guide uses rsync + cron for pure DIY backup; for managed off-site backups see Hostiger Backup-as-a-Service.
1. Set up a backup destination
The destination should be in a different data center. Options:
- Another Hostiger VPS in a different region ($15/mo)
- A dedicated storage box at Hetzner (~€3/mo for 1 TB)
- An S3-compatible bucket via rclone (works, but more setup)
2. SSH key from source to destination
On the source VPS:
ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""
ssh-copy-id -i /root/.ssh/backup_key.pub [email protected]
3. The backup script
Create /usr/local/bin/backup.sh:
#!/bin/bash
set -euo pipefail
SRC_DIRS="/etc /home /var/www /var/lib/mysql-backup /root"
DEST_HOST="[email protected]"
DEST_ROOT="/backups/$(hostname)"
DATE=$(date +%Y-%m-%d)
KEY="/root/.ssh/backup_key"
# 1. Dump MySQL first
mkdir -p /var/lib/mysql-backup
mysqldump --all-databases --single-transaction > /var/lib/mysql-backup/all-databases.sql
# 2. rsync with hardlink to previous day for space-efficient snapshots
rsync -aAXH --delete \
--link-dest="$DEST_ROOT/latest" \
-e "ssh -i $KEY -o StrictHostKeyChecking=no" \
$SRC_DIRS "$DEST_HOST:$DEST_ROOT/$DATE"
# 3. Update the 'latest' symlink on the destination
ssh -i $KEY $DEST_HOST "ln -sfn $DEST_ROOT/$DATE $DEST_ROOT/latest"
chmod +x /usr/local/bin/backup.sh
4. Automate with cron
crontab -e
# Daily backup at 03:15
15 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
5. Retention policy — 7 daily / 4 weekly / 12 monthly
Add this script on the DESTINATION to prune old snapshots:
#!/bin/bash
cd /backups/$(hostname)
# Keep last 7 daily
ls -1d 20*-* | head -n -7 | while read d; do
# Keep first snapshot of the week/month
day=$(date -d "$d" +%w) # 0=Sunday
dom=$(date -d "$d" +%d) # 01-31
[ "$day" = "0" ] && continue
[ "$dom" = "01" ] && continue
rm -rf "$d"
done
6. Encrypt before shipping (optional)
For sensitive data, pipe rsync through gpg:
tar czf - $SRC_DIRS | gpg --symmetric --cipher-algo AES256 --batch --passphrase-file /root/.backup_pass \
| ssh $DEST_HOST "cat > $DEST_ROOT/$DATE.tar.gz.gpg"
7. TEST YOUR RESTORE (most-skipped step)
An untested backup is not a backup. Once a month:
# Restore a single file
rsync -av $DEST_HOST:$DEST_ROOT/latest/var/www/site/index.html /tmp/test-restore.html
diff /var/www/site/index.html /tmp/test-restore.html
And once a quarter: fully rebuild from backup onto a spare VPS. If restore takes 3 hours because you didn't plan it, discovering that in a real incident is a bad time.
What can go wrong
- MySQL dump running out of disk on the source — add
df -hcheck before the dump - Backup disk fills up — retention script must actually run (add it to destination cron too)
- SSH key gets committed to git accidentally — keep the key OUT of any /var/www or /home/git directory
- Network glitch mid-transfer — rsync resumes cleanly on next run because
--link-destcompares checksums