Back to tutorials
Tutorial

VPS Snapshot Backup Tutorial (2026): Automated Offsite Copies, Retention, and Restore Checks on Ubuntu

VPS snapshot backup tutorial for 2026: automate snapshots, copy offsite, set retention, and test restores on Ubuntu.

By Anurag Singh
Updated on Jun 17, 2026
Category: Tutorial
Share article
VPS Snapshot Backup Tutorial (2026): Automated Offsite Copies, Retention, and Restore Checks on Ubuntu

Snapshots are fast.

Restores are what actually save you.

This VPS snapshot backup tutorial walks through a practical Ubuntu setup for 2026. You’ll schedule provider snapshots, export a usable offsite copy, rotate old archives, and run a repeatable restore check.

This workflow fits most VPS hosting stacks: WordPress, Nginx/Apache + PHP-FPM, small SaaS apps, and “sidecar” services next to a panel.

The goal is simple.

A bad deploy, ransomware event, or disk failure should cost minutes, not days.

What you’ll build (and what it protects)

By the end you’ll have:

  • Nightly VPS snapshots (fast rollback for full-server “oops” moments).
  • Offsite copies of critical data (so one provider isn’t a single point of failure).
  • Retention rules (daily/weekly/monthly) that balance recovery options and storage cost.
  • A restore check you can run after major changes, not just during emergencies.

Don’t confuse snapshots with backups.

Snapshots are one layer. File/database backups are another.

You want both.

Prerequisites: a clean base and a place to store offsite backups

You’ll need:

  • An Ubuntu VPS (22.04 LTS or 24.04 LTS are common in 2026) with root or sudo access.
  • A remote storage target you control (another VPS, SFTP storage, or object storage). In this tutorial we’ll use SFTP to a backup host because it’s simple to audit.
  • Basic firewall access for SSH (port 22 or your custom port).

If you don’t already have a separate box for backups, a small storage VPS is usually enough for weeks of compressed site data.

A HostMyCode VPS works well for the primary server, and a second one in another region can serve as your offsite target.

Optional but recommended reading before you proceed:

Step 1: Confirm your server data layout (so you back up the right paths)

Before you automate anything, confirm what you’re protecting.

On most hosting-style VPS builds, the usual suspects are:

  • Web roots: /var/www (Nginx/Apache), or user homes under /home.
  • Nginx configs: /etc/nginx; Apache: /etc/apache2.
  • PHP-FPM pools: /etc/php/*/fpm/pool.d.
  • Let’s Encrypt: /etc/letsencrypt.
  • App env files: wherever you keep them (often inside the project directory).

Run:

df -h
ls -la /var/www
ls -la /etc/nginx /etc/apache2 2>/dev/null
sudo ls -la /etc/letsencrypt 2>/dev/null

If you’re on a control panel, the layout will differ.

With cPanel, you typically care about account data under /home, plus the DNS/config pieces WHM manages.

In that world, WHM’s built-in backup system is often the best primary method. Use offsite copies as your safety net.

Step 2: Create a dedicated SFTP user on the backup target

On your backup server (not the production VPS), create a restricted user and a dedicated directory.

This keeps backups out of random admin home folders. It also makes permissions easier to reason about later.

sudo adduser backup
sudo mkdir -p /srv/backups/prod-vps-1
sudo chown -R backup:backup /srv/backups/prod-vps-1
sudo chmod 750 /srv/backups /srv/backups/prod-vps-1

Lock the user down to SFTP-only (optional but recommended).

Edit /etc/ssh/sshd_config on the backup server and add:

Match User backup
  ForceCommand internal-sftp
  PasswordAuthentication no
  ChrootDirectory /srv/backups
  AllowTcpForwarding no
  X11Forwarding no

Then fix up the chroot path. SSH is strict about ownership and permissions.

sudo chown root:root /srv/backups
sudo chmod 755 /srv/backups
sudo mkdir -p /srv/backups/prod-vps-1
sudo chown backup:backup /srv/backups/prod-vps-1
sudo systemctl restart ssh

If you want a more complete, hardened SFTP setup (including chroot gotchas), see: SFTP setup guide tutorial.

Step 3: Set up SSH keys for non-interactive backup transfers

On the production VPS, generate a keypair used only for backups.

A dedicated key makes rotation and audits much easier.

sudo mkdir -p /root/.ssh
sudo chmod 700 /root/.ssh
sudo ssh-keygen -t ed25519 -a 64 -f /root/.ssh/id_ed25519_backup -N "" -C "backup-key"

Copy the public key into the backup user’s authorized_keys (on the backup server):

sudo mkdir -p /home/backup/.ssh
sudo chmod 700 /home/backup/.ssh
sudo tee -a /home/backup/.ssh/authorized_keys < /root/.ssh/id_ed25519_backup.pub
sudo chmod 600 /home/backup/.ssh/authorized_keys
sudo chown -R backup:backup /home/backup/.ssh

Test from the production VPS:

ssh -i /root/.ssh/id_ed25519_backup backup@BACKUP_SERVER_IP -s sftp

If you land in SFTP and ls works, the transport is ready for automation.

Step 4: Automate provider snapshots (the fast rollback layer)

Every VPS platform handles snapshots differently.

It might be a dashboard toggle, an API call, or a scheduled job.

If your provider supports scheduled snapshots, use that first.

It’s usually less fragile than a cron job you wrote at 2 a.m.

Recommended snapshot schedule for most hosting VPS setups:

  • Daily snapshots kept for 7–14 days.
  • Weekly snapshot kept for 4–8 weeks.
  • Pre-change snapshot before OS upgrades, major PHP changes, or control panel updates.

Two things to keep in mind:

  • Snapshots often live inside the same provider account. If the account is compromised, an attacker can delete them.
  • A snapshot is a point-in-time disk image. If your database is mid-write, recovery can be messy unless you also take logical backups.

That’s why you also want the offsite layer in the next step.

Step 5: Build the offsite backup script (files + configs, with sane exclusions)

On the production VPS, you’ll create a nightly tarball of your web data and key configs.

Then you’ll push it to the backup host over SFTP.

Adjust paths to match your stack.

Back up where you actually keep state.

Create /usr/local/sbin/offsite-backup.sh:

sudo nano /usr/local/sbin/offsite-backup.sh

Paste:

#!/usr/bin/env bash
set -euo pipefail

BACKUP_HOST="BACKUP_SERVER_IP"
BACKUP_USER="backup"
BACKUP_DIR="/prod-vps-1"
SSH_KEY="/root/.ssh/id_ed25519_backup"

STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
HOST="$(hostname -s)"
WORKDIR="/root/backup-work"
ARCHIVE="${WORKDIR}/${HOST}-${STAMP}.tar.zst"

mkdir -p "${WORKDIR}"
chmod 700 "${WORKDIR}"

# What to back up (edit this list)
INCLUDE_PATHS=(
  "/var/www"
  "/etc/nginx"
  "/etc/apache2"
  "/etc/php"
  "/etc/letsencrypt"
  "/etc/cron.d"
)

# Build tar archive with exclusions for cache, logs, and node_modules
# zstd gives a good speed/ratio balance in 2026.
tar \
  --warning=no-file-changed \
  --exclude='*/cache/*' \
  --exclude='*/wp-content/cache/*' \
  --exclude='*/wp-content/uploads/cache/*' \
  --exclude='*/node_modules/*' \
  --exclude='*/.git/*' \
  --exclude='/var/www/*/storage/logs/*' \
  -I 'zstd -19 -T0' \
  -cpf "${ARCHIVE}" \
  "${INCLUDE_PATHS[@]}" 2>/dev/null || true

# Verify archive is readable
tar -I 'zstd -d' -tf "${ARCHIVE}" >/dev/null

# Upload via SFTP (batch mode)
cat > "${WORKDIR}/sftp-batch.txt" <<EOF
mkdir ${BACKUP_DIR}
cd ${BACKUP_DIR}
put ${ARCHIVE}
EOF

sftp \
  -i "${SSH_KEY}" \
  -o BatchMode=yes \
  -o StrictHostKeyChecking=accept-new \
  -b "${WORKDIR}/sftp-batch.txt" \
  "${BACKUP_USER}@${BACKUP_HOST}"

# Local cleanup (keep last 2 locally)
ls -1t "${WORKDIR}"/*.tar.zst 2>/dev/null | tail -n +3 | xargs -r rm -f

echo "Offsite backup complete: ${ARCHIVE}"

Make it executable:

sudo chmod 700 /usr/local/sbin/offsite-backup.sh

Why zstd?

On typical WordPress/PHP trees, it usually finishes much faster than gzip on the same CPU. It also compresses better than lz4.

If you also need database backups, keep them separate and consistent (dump + upload).

This tutorial stays file/config focused so you can adopt it safely across different stacks.

Step 6: Add retention on the backup server (daily/weekly/monthly)

At this point you’re shipping archives, but you’re not managing history.

Keep retention on the backup server, not production.

Production is the box most likely to fail (or get compromised).

On the backup server, create a retention script: /usr/local/sbin/backup-retention.sh

sudo nano /usr/local/sbin/backup-retention.sh

Paste (keeps 14 daily, 8 weekly, 6 monthly per host folder):

#!/usr/bin/env bash
set -euo pipefail

BASE="/srv/backups/prod-vps-1"

cd "${BASE}"

# Files look like: hostname-20260101T010203Z.tar.zst
# Keep last 14 (daily)
ls -1t *.tar.zst 2>/dev/null | tail -n +15 | xargs -r rm -f

# Weekly: keep one per week for last 8 weeks (best-effort)
# We copy candidates into a weekly folder so deletions don't break history.
mkdir -p weekly monthly

# Weekly selection: pick the newest backup from each ISO week
for w in $(ls -1 *.tar.zst 2>/dev/null | sed -E 's/.*-([0-9]{8})T.*/\1/' | \
  xargs -I{} date -d {} +%G-%V 2>/dev/null | sort -u | tail -n 8); do
  f=$(ls -1t *.tar.zst | head -n 200 | \
    awk -v W="$w" 'match($0, /-([0-9]{8})T/, a){cmd="date -d " a[1] " +%G-%V"; cmd|getline out; close(cmd); if(out==W){print $0; exit}}')
  [ -n "${f}" ] && cp -n "${f}" "weekly/" || true
done

# Monthly selection: keep one per month for last 6 months
for m in $(ls -1 *.tar.zst 2>/dev/null | sed -E 's/.*-([0-9]{8})T.*/\1/' | \
  xargs -I{} date -d {} +%Y-%m 2>/dev/null | sort -u | tail -n 6); do
  f=$(ls -1t *.tar.zst | head -n 400 | \
    awk -v M="$m" 'match($0, /-([0-9]{8})T/, a){cmd="date -d " a[1] " +%Y-%m"; cmd|getline out; close(cmd); if(out==M){print $0; exit}}')
  [ -n "${f}" ] && cp -n "${f}" "monthly/" || true
done

# Trim weekly/monthly folders to sane limits
ls -1t weekly/*.tar.zst 2>/dev/null | tail -n +9 | xargs -r rm -f
ls -1t monthly/*.tar.zst 2>/dev/null | tail -n +7 | xargs -r rm -f

echo "Retention complete in ${BASE}"

Enable it:

sudo chmod 700 /usr/local/sbin/backup-retention.sh

This is intentionally plain.

During an incident, “simple and obvious” beats “clever and confusing.”

Step 7: Schedule everything with systemd timers (cleaner than cron)

Systemd timers are easy to inspect and log cleanly.

They also behave predictably after reboots.

You’ll schedule:

  • Production: offsite upload nightly
  • Backup server: retention daily

On the production VPS: offsite backup timer

Create a service unit:

sudo nano /etc/systemd/system/offsite-backup.service
[Unit]
Description=Nightly offsite backup upload
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 nightly

[Timer]
OnCalendar=*-*-* 02:10:00
RandomizedDelaySec=900
Persistent=true

[Install]
WantedBy=timers.target

Enable:

sudo systemctl daemon-reload
sudo systemctl enable --now offsite-backup.timer
sudo systemctl list-timers | grep offsite-backup

On the backup server: retention timer

sudo nano /etc/systemd/system/backup-retention.service
[Unit]
Description=Backup retention cleanup

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/backup-retention.sh
sudo nano /etc/systemd/system/backup-retention.timer
[Unit]
Description=Run retention daily

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup-retention.timer

Step 8: Add quick diagnostics (so you notice failures fast)

Silent failures are the real backup killer.

Add one check on production (did the job run?) and one on the backup server (did a new file show up?).

Production: verify upload succeeded

After a run, check logs:

sudo journalctl -u offsite-backup.service --since "today"

If you want email alerts, pair this with a daily report tool.

A lightweight option is Logwatch: Logwatch setup tutorial.

Backup server: verify new files arrive daily

On the backup server:

ls -lah /srv/backups/prod-vps-1 | tail

A simple rule works well in practice.

If the newest file is older than 26 hours, open a ticket and investigate.

Step 9: Restore check (the part most people skip)

A restore check shouldn’t turn into a production incident.

You’re proving three things: you can fetch the archive, extract it cleanly, and get a site answering HTTP again.

Here’s a minimal drill you can run monthly or after major changes:

  1. Provision a fresh Ubuntu VPS (small size is fine for the drill).
  2. Pull the newest archive from the backup server.
  3. Extract to the right paths.
  4. Install the web stack packages (Nginx/Apache/PHP) and reload.
  5. Point a test hostname or temporary DNS record to the drill box.

On the restore test VPS:

sudo apt update
sudo apt install -y zstd openssh-client tar

Fetch one archive (replace file name):

sudo mkdir -p /root/restore
sudo sftp -i /root/.ssh/id_ed25519_backup backup@BACKUP_SERVER_IP -s sftp <<EOF
cd /prod-vps-1
get HOSTNAME-YYYYMMDDTHHMMSSZ.tar.zst /root/restore/backup.tar.zst
EOF

Extract (this overwrites paths, so do it on a fresh box):

cd /
sudo tar -I 'zstd -d' -xpf /root/restore/backup.tar.zst

Reinstall services as needed (example for Nginx + PHP-FPM):

sudo apt install -y nginx php-fpm
sudo systemctl enable --now nginx
sudo nginx -t && sudo systemctl reload nginx

To practice a low-downtime cutover with safe rollback, keep TTL low.

Validate records before switching.

This pairs well with: DNS propagation tutorial.

If you want a more realistic “full rebuild” exercise, run a complete restore drill with DNS cutover steps: VPS restore drill tutorial.

Common pitfalls (and the quick fixes)

  • Backups are huge: you’re archiving uploads/cache/logs. Add exclusions for wp-content/uploads only if you store media elsewhere; otherwise exclude just cache and logs.
  • SFTP fails with “Permission denied”: chroot permissions on the backup server are wrong. The chroot directory must be owned by root and not writable by the SFTP user.
  • Archive created but empty: one of your include paths doesn’t exist and tar exits oddly. Keep includes accurate, and review journalctl.
  • Restore boots but site errors: configs came back, but packages/modules didn’t. Treat restores as “data + rebuild”, not “magic clone”. Document your package list.

Summary: a practical backup stack for hosting VPS in 2026

You’re running a layered setup now.

You have provider snapshots for fast rollbacks, plus offsite archives with retention and a restore check you can repeat.

That combination keeps a hosting VPS recoverable after bad updates, compromise, or storage failures.

If you’re rolling this out across multiple servers or client sites, standardize naming.

Use one folder per server.

Run restore drills quarterly, and write down DNS cutover steps.

If you’d rather not maintain the backup plumbing yourself, consider a managed VPS hosting plan and pair it with a separate HostMyCode VPS as the offsite target.

If your sites bring in revenue, backups should be boring, consistent, and tested. HostMyCode offers VPS and managed VPS options that match this workflow: snapshots for quick rollback, plus space for an offsite backup node you control.

Start with a HostMyCode VPS for production, and use managed VPS hosting if you want help with hardening, monitoring, and routine maintenance.

FAQ

Are VPS snapshots enough on their own?

No. Snapshots are great for fast rollback, but they’re usually stored within the same provider account.

Keep an offsite copy so you can recover from account compromise or provider-side incidents.

How often should I run backups for WordPress hosting?

Nightly is a solid baseline for most business sites.

If you publish or sell throughout the day, add more frequent database dumps (every 1–4 hours) and keep the nightly file/config archive.

Why use systemd timers instead of cron?

Timers provide structured logs in journalctl, better visibility into failures, and “catch-up” behavior with Persistent=true after reboots.

How do I keep backups secure if the production VPS is compromised?

Store backups on a separate host with separate credentials, restrict the SFTP user, and avoid mounting the backup storage back onto production.

If you can, prevent delete permissions for the upload user and prune from the backup server only.

What’s the fastest way to cut over to a restored server?

Lower DNS TTL ahead of time, validate the restored site on a temporary hostname, then switch A/AAAA records.

Keep the old server for rollback until you’re confident.

Use this checklist: DNS propagation tutorial.

VPS Snapshot Backup Tutorial (2026): Automated Offsite Copies, Retention, and Restore Checks on Ubuntu | HostMyCode