
A snapshot feels like a backup until you need it fast, under pressure. This VPS snapshot backup tutorial shows how to schedule provider snapshots, keep an offsite copy of important data, and run restore tests on Ubuntu in 2026.
The goal is simple: prove recovery works before you need it.
You’ll build three layers: (1) quick snapshots for “oh no” rollbacks, (2) file-level backups you can restore one directory at a time, and (3) a repeatable restore test on a clean server.
Together, this keeps incidents boring and predictable.
What you’re building: a 3-layer backup plan for a hosting VPS
Here’s the end state you’re aiming for.
Keep this structure even if you swap tools later.
- Layer 1: VPS snapshots (hourly/daily). Fast rollback for OS breakage, bad updates, or accidental deletions.
- Layer 2: Offsite file backups (daily). rsync-based snapshots to a separate backup box you control.
- Layer 3: Restore tests (weekly/monthly). Boot a fresh VPS, pull a backup, and validate your site comes up.
If you host client sites, don’t treat snapshots as your only recovery path.
Snapshots can disappear, fail, or become inaccessible during a provider incident.
Offsite copies keep you provider-agnostic and give you a way out.
Prerequisites (Ubuntu VPS, access, and a place to store backups)
This guide assumes Ubuntu Server 24.04 LTS or 22.04 LTS, root or sudo access, and systemd timers (installed by default).
You’ll also need:
- A hosting VPS that runs your workload (Nginx/Apache, WordPress, mail, etc.).
- A separate backup target: another VPS, a dedicated server, or a storage VM.
- SSH key-based auth between the primary server and backup target.
If you’re starting from scratch, a HostMyCode VPS gives you predictable resources for both the app server and a separate backup node.
If you prefer someone else to handle routine server care, managed VPS hosting is often the simplest way to cut operational risk.
Before you touch backups, clean up SSH access.
If you still log in with passwords, fix that first.
This tutorial pairs well with SSH key setup and safe SSHD hardening.
Step 1: Decide what must be backed up (and what can be rebuilt)
On most hosting VPS setups, you can rebuild packages and OS defaults quickly.
You usually can’t recreate customer data, uploads, or your real-world config.
Start by separating “rebuildable” from “must keep.”
Usually must keep:
/var/www(or your virtual hosts directory)- Application config:
/etc/nginx,/etc/apache2,/etc/php,/etc/letsencrypt - Mail:
/var/mail,/home/*/mail, and MTA config if you run mail services - Databases: backups/dumps or raw data (database-heavy strategies are out of scope here; keep it simple and consistent)
- Crontabs:
/etc/crontab,/etc/cron.d, user crontabs
Usually rebuildable:
- Base OS packages (APT)
- Most logs (keep only what you need for security/audit)
- Caches:
/var/cache, app caches
Quick inventory commands:
df -h
sudo du -xh --max-depth=1 /var | sort -h | tail
sudo du -xh --max-depth=1 /home | sort -h | tail
sudo crontab -l || true
sudo ls -la /etc/cron.d
Step 2: Automate VPS snapshots (fast rollbacks) without betting the business on them
Most providers expose snapshots in the dashboard and via an API.
Either way, you’re aiming for the same result:
- Create snapshots on a fixed schedule.
- Keep short retention (for example: 24 hourly + 14 daily) to control costs.
- Label snapshots with timestamps so you can pick the right one quickly.
Snapshot schedule (practical default):
- Hourly: last 24
- Daily: last 14
- Monthly: last 3 (optional)
Two pitfalls to avoid:
- Snapshots during high write load can capture inconsistent app state. If you can, snapshot during a quiet window. For DB-heavy apps, consider briefly pausing writes or flushing.
- Snapshots only ties your recovery to one provider account. If the account is compromised or snapshots get purged, you’re out of options.
For a controlled maintenance window during snapshots or big updates, use a safe update workflow.
That means staging checks plus a rollback plan.
If you run cPanel servers, this is similar in spirit to WHM maintenance mode planning.
Step 3: Build an offsite backup target (a separate VPS or dedicated box)
Your offsite target should be isolated from production.
Assume a worst-case scenario: root access on the primary server.
The backup host should survive anyway.
Minimum requirements for the backup target:
- Separate instance (different VPS or a dedicated server)
- Enough storage for retention (snapshots add up quickly)
- SSH access restricted to a single backup user and key
Create a backup user on the backup server:
sudo adduser backup
sudo usermod -aG sudo backup
On the production VPS, create a dedicated SSH key for backups:
sudo mkdir -p /root/.ssh
sudo chmod 700 /root/.ssh
sudo ssh-keygen -t ed25519 -a 100 -f /root/.ssh/id_ed25519_backup -C "backup-key"
Copy the public key to the backup server:
sudo ssh-copy-id -i /root/.ssh/id_ed25519_backup.pub backup@BACKUP_SERVER_IP
Then harden that key on the backup server by restricting what it can do.
Edit /home/backup/.ssh/authorized_keys and prepend options (adjust paths):
command="/usr/bin/rrsync -ro /backups/",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc ssh-ed25519 AAAA... backup-key
Install rrsync (ships with rsync docs on Ubuntu):
sudo apt update
sudo apt install -y rsync
Create the storage path on the backup server:
sudo mkdir -p /backups
sudo chown backup:backup /backups
sudo chmod 750 /backups
If you want a cleaner access model (especially with teams), put SSH behind a bastion/jump host.
Then keep production SSH closed except from that bastion.
See SSH bastion host with ProxyJump and audit-friendly access.
Step 4: Create file-level backups with rsync + snapshot directories (hard links)
This approach gives you incrementals that behave like full backups.
Each run creates a new directory.
Unchanged files hard-link to the previous run, so storage growth stays reasonable.
On the production VPS, create a simple backup script at /usr/local/sbin/offsite-backup.sh:
sudo nano /usr/local/sbin/offsite-backup.sh
Paste and edit to match your setup:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_HOST="backup@BACKUP_SERVER_IP"
SSH_KEY="/root/.ssh/id_ed25519_backup"
BASE_DIR="/backups/$(hostname -s)"
TS="$(date -u +%Y-%m-%dT%H%M%SZ)"
DEST="$BASE_DIR/$TS"
LATEST="$BASE_DIR/latest"
# What to back up (add/remove paths)
INCLUDE=(
/etc
/var/www
/etc/letsencrypt
)
# Exclusions to keep backups small and less noisy
EXCLUDES=(
"/var/www/*/wp-content/cache"
"/var/www/*/cache"
"/var/log"
"/var/cache"
"/tmp"
)
SSH_OPTS=("-i" "$SSH_KEY" "-o" "BatchMode=yes" "-o" "StrictHostKeyChecking=accept-new")
# Create destination directory
ssh "${SSH_OPTS[@]}" "$BACKUP_HOST" "mkdir -p '$DEST' '$BASE_DIR'"
# Use --link-dest to hardlink unchanged files from the previous backup
LINKDEST_OPT=()
if ssh "${SSH_OPTS[@]}" "$BACKUP_HOST" "test -d '$LATEST'"; then
LINKDEST_OPT=("--link-dest=$LATEST")
fi
# Build rsync exclude args
EXCLUDE_ARGS=()
for e in "${EXCLUDES[@]}"; do
EXCLUDE_ARGS+=("--exclude=$e")
done
# Run backup
rsync -aHAX --numeric-ids --delete \
"${EXCLUDE_ARGS[@]}" \
"${LINKDEST_OPT[@]}" \
-e "ssh ${SSH_OPTS[*]}" \
"${INCLUDE[@]}" \
"$BACKUP_HOST:$DEST/"
# Move latest pointer
ssh "${SSH_OPTS[@]}" "$BACKUP_HOST" "rm -f '$LATEST' && ln -s '$DEST' '$LATEST'"
echo "Offsite backup complete: $DEST"
Make it executable:
sudo chmod 750 /usr/local/sbin/offsite-backup.sh
Test it manually:
sudo /usr/local/sbin/offsite-backup.sh
Quick diagnostic if rsync is slow:
- Large numbers of small files (WordPress uploads, cache) will dominate runtime.
- Exclude caches aggressively and consider a CDN for static assets.
- Check latency and throughput between servers (
mtr,iperf3).
If you want a deeper variant of this approach (restore tests, snapshots, retention), you can also compare against our dedicated guide on rsync over SSH incremental backups.
The steps here stay focused on the snapshot-plus-offsite workflow.
Step 5: Add retention pruning (keep 14 daily + 8 weekly, for example)
Backups that grow forever eventually turn into a disk-full incident.
Prune on the backup server, not on production.
Create /usr/local/sbin/prune-backups.sh on the backup server:
sudo nano /usr/local/sbin/prune-backups.sh
#!/usr/bin/env bash
set -euo pipefail
ROOT="/backups"
# Keep: last 14 daily and last 8 weekly (Sundays), delete the rest.
# Assumes directories named like 2026-01-31T235959Z
for hostdir in "$ROOT"/*; do
[ -d "$hostdir" ] || continue
# List backups newest-first
mapfile -t backups < <(ls -1 "$hostdir" | grep -E '^[0-9]{4}-' | sort -r)
# Keep newest 14
keep_daily=("${backups[@]:0:14}")
# Keep newest 8 Sundays (weekly)
keep_weekly=()
for b in "${backups[@]}"; do
d="${b:0:10}"
if [ "$(date -d "$d" +%u)" = "7" ]; then
keep_weekly+=("$b")
fi
[ "${#keep_weekly[@]}" -ge 8 ] && break
done
# Build keep set
keep_set=$(printf "%s\n" "${keep_daily[@]}" "${keep_weekly[@]}" | sort -u)
# Delete anything not in keep_set
while IFS= read -r b; do
if ! grep -qx "$b" <<< "$keep_set"; then
echo "Pruning $hostdir/$b"
rm -rf "$hostdir/$b"
fi
done < <(printf "%s\n" "${backups[@]}")
done
Make it executable:
sudo chmod 750 /usr/local/sbin/prune-backups.sh
Run it once with care:
sudo /usr/local/sbin/prune-backups.sh
If you want a policy-driven way to set retention across multiple servers, see server backup retention policy design for a more formal framework.
Step 6: Schedule everything with systemd timers (cleaner than cron)
Systemd timers are easier to operate than cron on most modern Ubuntu servers.
You get predictable scheduling, structured logs, and clear exit codes.
Below, you’ll schedule the offsite backup daily and pruning weekly.
On the production VPS, create a service unit:
sudo nano /etc/systemd/system/offsite-backup.service
[Unit]
Description=Offsite rsync backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/offsite-backup.sh
Create the timer:
sudo nano /etc/systemd/system/offsite-backup.timer
[Unit]
Description=Run offsite backup daily
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=900
[Install]
WantedBy=timers.target
Enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now offsite-backup.timer
sudo systemctl list-timers | grep offsite-backup
On the backup server, add pruning weekly:
sudo nano /etc/systemd/system/prune-backups.service
[Unit]
Description=Prune old backup directories
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/prune-backups.sh
sudo nano /etc/systemd/system/prune-backups.timer
[Unit]
Description=Run backup pruning weekly
[Timer]
OnCalendar=Sun *-*-* 04:30:00
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now prune-backups.timer
Check logs when something fails:
sudo journalctl -u offsite-backup.service --since "24 hours ago" -e
sudo journalctl -u prune-backups.service --since "7 days ago" -e
If you want earlier warning before disks fill or timers stop firing, add basic log monitoring and alerts.
This pairs well with log monitoring with journalctl, Logwatch, and Fail2Ban signals.
Step 7: Verify backups with a real restore test (the part people skip)
Verification means more than “the job ran.”
You need proof you can restore and that the site actually works.
Do this on a fresh VPS at least monthly.
Also run it after major changes (PHP upgrade, web server swap, app rewrites).
Restore test checklist:
- Provision a new VPS with the same Ubuntu version.
- Install your web stack (Nginx/Apache, PHP, etc.).
- Pull the latest backup from the backup server.
- Restore site files and critical config.
- Point a temporary hosts entry to the new IP and validate the site.
- Validate TLS and renewals if you’re restoring
/etc/letsencrypt.
On the restore-test VPS, pull files (example for /var/www and /etc/nginx):
sudo rsync -aHAX -e "ssh -i /root/.ssh/id_ed25519_backup" \
backup@BACKUP_SERVER_IP:/backups/PROD_HOSTNAME/latest/var/www/ /var/www/
sudo rsync -aHAX -e "ssh -i /root/.ssh/id_ed25519_backup" \
backup@BACKUP_SERVER_IP:/backups/PROD_HOSTNAME/latest/etc/nginx/ /etc/nginx/
Reload services and check:
sudo nginx -t
sudo systemctl restart nginx
curl -I http://127.0.0.1
If your site uses Let’s Encrypt, you have two sane options during restore tests:
- Option A (recommended): request new certificates for the test box (safer than reusing keys).
- Option B: restore
/etc/letsencryptand verify permissions + renewal jobs carefully.
For TLS setup and debugging details, use our Let’s Encrypt deployment and troubleshooting guide.
Step 8: Hardening tips so backups survive real incidents
A backup you can delete from a compromised server isn’t much of a backup.
Treat the backup host as a separate security boundary.
Because it is.
- Restrict SSH keys on the backup server (forced command, no TTY, no forwarding).
- Separate credentials: don’t reuse production keys on the backup server.
- Firewall the backup server: only allow SSH from production IPs and your admin IPs.
- Monitor disk usage: alert at 70%/85% so pruning has time to work.
If you haven’t locked down firewall rules yet, follow this UFW firewall setup.
Apply it to both production and backup servers (with different allowed sources).
Troubleshooting: common backup failures and quick fixes
- “Permission denied (publickey)”: confirm you used the correct key (
-i), and the public key exists inauthorized_keysfor the right user. - Backups are huge: remove logs/caches, exclude
wp-content/cache, and avoid backing upnode_modulesor build artifacts. - Rsync deletes too much: validate your source paths. A missing trailing slash changes meaning. Run once without
--deleteuntil you trust it. - Timer didn’t run: check
systemctl status offsite-backup.timerand logs viajournalctl. - Disk fills on backup server: run prune immediately, then reduce retention or expand storage.
Summary: snapshots for speed, offsite backups for survival
Use snapshots for quick rollbacks.
Use offsite backups to survive provider issues and account-level problems.
Then run restore tests on a schedule.
That’s the gap between “we have backups” and “we can recover.”
If you want this setup on predictable infrastructure, run production and backups on separate instances with a HostMyCode VPS.
If you’d rather hand off patching, monitoring, and routine admin work while keeping a clear backup plan, choose managed VPS hosting for the servers that carry revenue.
If you’re moving sites to a new server or tightening up recovery, split the roles: one VPS for production and another for offsite backups. HostMyCode offers HostMyCode VPS for DIY admins and managed VPS hosting if you want help keeping patching, monitoring, and day-to-day ops under control.
FAQ
Are VPS snapshots enough for backups?
No. Snapshots are great for fast rollbacks, but they’re still inside the same provider account and platform.
Keep at least one offsite copy you control.
How often should I run backups for a WordPress hosting VPS?
Daily is a solid default for most sites.
If you run WooCommerce or frequent content updates, add an extra run (for example, every 6–12 hours) and keep shorter retention.
Should I back up /var/log?
Usually not for recovery.
Keep security logs if you have compliance needs, but logs can explode in size and don’t help you restore service quickly.
How do I know my backups aren’t corrupted?
Run restore tests.
For critical archives, add checksum verification (for example, generate SHA256 manifests on the backup server) and validate before/after transfer.
What’s the simplest way to avoid locking myself out while hardening access?
Use SSH keys, keep one tested break-glass path, and apply firewall changes with a live session open.
If you centralize access, use a bastion/jump host so production doesn’t expose SSH broadly.