Back to tutorials
Tutorial

rsync Backup Tutorial (2026): Incremental VPS Backups Over SSH with Rotation and Restore Tests

rsync backup tutorial for 2026: run encrypted incremental VPS backups over SSH, rotate snapshots, and verify restores step by step.

By Anurag Singh
Updated on Sep 08, 2026
Category: Tutorial
Share article
rsync Backup Tutorial (2026): Incremental VPS Backups Over SSH with Rotation and Restore Tests

A backup you haven’t restored is just a hope with a billing cycle. This rsync backup tutorial shows a repeatable way to back up a VPS or dedicated server over SSH. You’ll keep multiple days of snapshots without duplicating unchanged data. You’ll also prove you can actually restore.

This is a push setup. The source server (web/mail/app) connects to a separate backup server over SSH.

The steps work on Ubuntu, Debian, AlmaLinux, Rocky Linux, and CentOS Stream. They still fit if you later split services across multiple nodes.

What you’ll build (and what this does better than ad-hoc tar files)

  • Encrypted transfer over SSH with a dedicated, locked-down key.
  • Incremental daily snapshots using --link-dest (hard links) so unchanged files don’t eat disk.
  • Rotation (keep 7–30 days, delete safely).
  • Restore test that checks files, ownership, and permissions.

For WordPress, you’ll usually back up /var/www (or your vhost roots) plus selected configuration.

On cPanel/WHM, WHM backups remain the default for account-level restores. rsync still helps for server config, logs, and a “break glass” recovery path.

Prerequisites and sizing checklist

Before you touch rsync, confirm what you plan to restore. Then confirm the backup target can hold it.

  • Two Linux servers: a production VPS/dedicated server + a backup VPS/dedicated server.
  • Network access: SSH (port 22 by default) from source to backup.
  • Disk sizing rule: start with 1.5× to your used data on the source if you keep 7–14 daily snapshots. If your site churns a lot (uploads, caches), plan higher.
  • Time window: schedule backups during low traffic and avoid overlap with heavy cron jobs.

If you don’t already have a separate backup host, add a second VPS.

For clean isolation and predictable performance, a HostMyCode VPS is a sensible backup target.

If you want the platform side handled on the production node (updates, baseline security), use managed VPS hosting there and keep backups independent.

Step 1: Prepare the backup server (create a dedicated user and directory)

On the backup server, create a non-root user. Give it a dedicated destination directory.

Keep permissions tight. If the production server is compromised, it shouldn’t roam around the backup box.

sudo adduser --disabled-password --gecos "" backup
sudo mkdir -p /backups/vps1
sudo chown -R backup:backup /backups/vps1
sudo chmod 750 /backups /backups/vps1

Optional (but usually worth it): put backups on a separate volume (LVM or an extra disk) mounted at /backups.

This keeps backups from competing with the OS disk. It also simplifies capacity planning and recovery.

Step 2: Create a locked-down SSH key for rsync (no interactive shell)

On the source server, generate a dedicated key pair for backups. Don’t reuse your admin key.

sudo ssh-keygen -t ed25519 -a 64 -f /root/.ssh/rsync_backup -C "rsync-backup"

Copy the public key to the backup server (as the backup user):

sudo ssh-copy-id -i /root/.ssh/rsync_backup.pub backup@BACKUP_SERVER_IP

Now restrict that key on the backup server. The goal is simple: it can only do what you intend.

Edit:

sudo -u backup nano /home/backup/.ssh/authorized_keys

Prepend a forced command and restrictions to the key line. Replace /backups/vps1 with your path:

command="rsync --server --daemon .",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAA... rsync-backup

Reality check: forced-command setups are easy to misconfigure.

A safer operational pattern is simpler:

  • Use a dedicated user.
  • Lock down its permissions.
  • Restrict inbound SSH on the backup server to the source IP at the firewall.

If you also want a tighter SSH baseline on production, pair this with SSH key setup guide.

Step 3: Decide what to back up (and what to exclude)

Don’t rsync the entire filesystem by reflex.

You’ll waste time copying pseudo filesystems, caches, and noisy logs. Back up what you can realistically restore.

Common hosting paths worth backing up:

  • /etc (service configs)
  • /var/www or your vhost roots
  • /home (if you store site files there)
  • /root (only if you keep operational scripts there)
  • /var/spool/cron or /etc/cron.* (cron jobs)
  • /var/log (optional; useful for incident review, but large)

Exclude these by default:

  • /proc, /sys, /dev, /run
  • /tmp, /var/tmp
  • Application caches (WordPress cache folders, PHP opcache files, etc.)
  • Container layers if you don’t intend to restore them from rsync

If you run WordPress, decide up front how you’ll handle the database.

File backups via rsync pair well with a separate DB dump job (or a managed database).

If your priority is consistent SSL and web config, keep a reference of your TLS settings. TLS hardening tutorial is a good checklist of what to preserve.

Step 4: Build the snapshot layout on the backup server

Store backups in a predictable layout:

  • /backups/vps1/current/ – latest snapshot
  • /backups/vps1/daily/YYYY-MM-DD/ – dated snapshots

Create directories on the backup server:

sudo -u backup mkdir -p /backups/vps1/current /backups/vps1/daily

Step 5: Write the rsync backup script (incremental via --link-dest)

On the source server, create /usr/local/sbin/rsync-backup.sh:

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

Paste and adjust (set BACKUP_HOST and paths):

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

BACKUP_HOST="backup@BACKUP_SERVER_IP"
BACKUP_ROOT="/backups/vps1"
SSH_KEY="/root/.ssh/rsync_backup"
DATE="$(date -u +%F)"

SRC_PATHS=(
  "/etc"
  "/var/www"
  "/home"
)

EXCLUDES=(
  "/proc/*"
  "/sys/*"
  "/dev/*"
  "/run/*"
  "/tmp/*"
  "/var/tmp/*"
  "**/cache/**"
  "**/wp-content/cache/**"
)

SSH_OPTS=(
  "-i" "$SSH_KEY"
  "-o" "BatchMode=yes"
  "-o" "StrictHostKeyChecking=accept-new"
  "-o" "ServerAliveInterval=30"
  "-o" "ServerAliveCountMax=6"
)

# Ensure target dirs exist
ssh "${SSH_OPTS[@]}" "$BACKUP_HOST" "mkdir -p $BACKUP_ROOT/daily/$DATE $BACKUP_ROOT/current"

# If a previous snapshot exists, use it for link-dest to create an incremental snapshot
LINK_DEST_OPT=""
if ssh "${SSH_OPTS[@]}" "$BACKUP_HOST" "test -d $BACKUP_ROOT/current && [ -n \"$(ls -A $BACKUP_ROOT/current 2>/dev/null)\" ]"; then
  LINK_DEST_OPT="--link-dest=$BACKUP_ROOT/current"
fi

EXCLUDE_ARGS=()
for ex in "${EXCLUDES[@]}"; do
  EXCLUDE_ARGS+=("--exclude=$ex")
done

# Run rsync
rsync -aHAX --numeric-ids --delete \
  --info=stats2,progress2 \
  "${EXCLUDE_ARGS[@]}" \
  $LINK_DEST_OPT \
  -e "ssh ${SSH_OPTS[*]}" \
  "${SRC_PATHS[@]}" \
  "$BACKUP_HOST:$BACKUP_ROOT/daily/$DATE/"

# Update 'current' to point to today's snapshot
ssh "${SSH_OPTS[@]}" "$BACKUP_HOST" "rm -rf $BACKUP_ROOT/current && cp -al $BACKUP_ROOT/daily/$DATE $BACKUP_ROOT/current"

Make it executable:

sudo chmod 750 /usr/local/sbin/rsync-backup.sh

Why these flags:

  • -a preserves permissions and timestamps. Critical for web roots and system configs.
  • -HAX preserves hardlinks, ACLs, and xattrs (useful on modern Linux stacks).
  • --numeric-ids avoids user/group mapping surprises across servers.
  • --delete keeps snapshots consistent (it deletes files removed on the source).
  • --link-dest creates incremental snapshots without re-copying unchanged files.

Step 6: Add retention (rotate old snapshots safely)

Decide how many dailies you want. Then enforce it.

On the source server, create /usr/local/sbin/rsync-backup-rotate.sh:

sudo nano /usr/local/sbin/rsync-backup-rotate.sh

Example: keep 14 days.

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

BACKUP_HOST="backup@BACKUP_SERVER_IP"
BACKUP_ROOT="/backups/vps1"
SSH_KEY="/root/.ssh/rsync_backup"
KEEP_DAYS=14

SSH_OPTS=(
  "-i" "$SSH_KEY"
  "-o" "BatchMode=yes"
  "-o" "StrictHostKeyChecking=accept-new"
)

# Delete snapshots older than KEEP_DAYS
ssh "${SSH_OPTS[@]}" "$BACKUP_HOST" "find $BACKUP_ROOT/daily -mindepth 1 -maxdepth 1 -type d -mtime +$KEEP_DAYS -print -exec rm -rf {} +"

Make it executable:

sudo chmod 750 /usr/local/sbin/rsync-backup-rotate.sh

Pitfall: avoid rotating by name with naive ls | head pipelines.

Use find + -mtime (as shown) or a dedicated retention tool. This makes it much harder to delete the wrong thing.

Step 7: Run the first backup and validate the snapshot

Run the first job manually. You want to see errors immediately:

sudo /usr/local/sbin/rsync-backup.sh

On the backup server, confirm a snapshot exists. Then confirm the numbers look reasonable:

sudo -u backup ls -lah /backups/vps1/daily
sudo -u backup du -sh /backups/vps1/daily/* | tail

To prove incrementals work, make a small change (create a test file) and run the job again.

The second run should finish faster. Disk growth should mostly reflect what changed.

Step 8: Schedule with systemd timers (cleaner than cron)

systemd timers make backups easier to audit.

You get status, history, and logs in one place.

Create a service on the source server:

sudo nano /etc/systemd/system/rsync-backup.service
[Unit]
Description=Nightly rsync snapshot backup

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/rsync-backup.sh

Create the timer:

sudo nano /etc/systemd/system/rsync-backup.timer
[Unit]
Description=Run rsync-backup nightly

[Timer]
OnCalendar=*-*-* 02:15:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target

Add a second service+timer for rotation. Schedule it after the backup:

sudo nano /etc/systemd/system/rsync-backup-rotate.service
[Unit]
Description=Rotate old rsync snapshots

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

[Timer]
OnCalendar=*-*-* 04:30:00
RandomizedDelaySec=10m
Persistent=true

[Install]
WantedBy=timers.target

Enable timers:

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

If you prefer cron for compatibility, that’s fine. Keep it boring and log output.

systemd generally makes “why didn’t it run?” faster to answer.

Step 9: Add quick monitoring and alerting signals

Backups tend to fail quietly. Start with checks you’ll actually run:

  • Timer status: systemctl status rsync-backup.service
  • Logs: journalctl -u rsync-backup.service --since "24 hours ago"
  • Backup freshness on the backup server: newest directory in /backups/vps1/daily should be today (UTC).

If you already have monitoring, treat “backup completed” as a first-class signal.

The patterns in uptime monitoring tutorial map neatly to backup health checks.

Step 10: Restore test (the part people skip)

Do a restore test after the first successful week. Then repeat monthly.

You’re checking more than file presence. You’re validating permissions and ownership, too.

Example: restore a single site directory to a staging path on a test server:

# On a restore target server (or the source if you’re testing locally)
mkdir -p /restore-test

# Pull from backup snapshot (choose a date)
rsync -aHAX --numeric-ids \
  -e "ssh -i /root/.ssh/rsync_backup" \
  backup@BACKUP_SERVER_IP:/backups/vps1/daily/2026-09-01/var/www/example.com/ \
  /restore-test/example.com/

Validate a few key items:

  • Ownership: stat -c "%U:%G %a %n" /restore-test/example.com
  • Symlinks: confirm expected links are preserved as links (rsync does this with -a).
  • Config sanity: if you backed up /etc/nginx or /etc/apache2, verify you can run nginx -t or apachectl -t after restoring to a test box.

If a real restore involves DNS changes, treat it as a planned cutover.

HostMyCode’s DNS cutover checklist is a solid runbook for that process.

Troubleshooting: common rsync backup failures on hosting servers

  • “Permission denied” on the backup target: check /backups permissions and that you’re writing as backup. Also confirm SSH is using the intended key and not another identity.
  • Slow transfers: make sure you’re not pulling in cache directories, and don’t compress already-compressed media. For large binaries, rsync -z often wastes CPU.
  • UID/GID mismatches: keep --numeric-ids. If you need readable names on the backup server, mirror /etc/passwd//etc/group for service users—but don’t depend on name mapping for correctness.
  • Backups overlap and pile up: systemd timers help, but large datasets can still run long. Add a lock file if needed; flock around the rsync command is a simple fix.
  • SSH host key prompts break automation: pre-accept host keys with a manual SSH once, or use StrictHostKeyChecking=accept-new as shown (safer than disabling checks).

Hardening checklist for a backup-by-SSH setup

  • Use a dedicated backup user on the backup server (not root).
  • Use a dedicated SSH key for rsync (not your admin key).
  • Restrict inbound SSH to known source IPs at the firewall.
  • Log and review SSH logins on the backup server (/var/log/auth.log or journald).
  • Patch both machines regularly. For a safer update workflow, follow VPS patch management tutorial.

Summary: a backup routine you can trust

This rsync backup tutorial gives you a practical middle ground: encrypted transfers, multiple restore points, and efficient storage thanks to hard-linked snapshots.

The payoff is operational. You end up with a restore you’ve tested, not a pile of archives nobody has verified.

If you want a clean place to land these backups, put the target on a separate HostMyCode VPS and keep it isolated from your web stack.

If you’d rather keep production maintenance predictable, managed VPS hosting can cover patching, monitoring, and baseline hygiene without taking away your access.

If you’re building a backup routine for client sites or a business-critical WordPress server, keep the backup host separate from production. HostMyCode makes that easy with an affordable HostMyCode VPS for offsite snapshots, and managed VPS hosting if you want help maintaining the production server while keeping control.

FAQ

Is rsync good enough for VPS backups in 2026?

Yes for file-level backups, especially web roots and config. Pair it with a database dump strategy and do regular restore tests.

Should I compress rsync transfers with -z?

Usually no for hosting data sets heavy on images, ZIPs, and videos. Compression can slow the job by burning CPU. Test on your workload.

Can I back up a cPanel server with rsync instead of WHM backups?

WHM backups are still the standard for account-level restores. rsync is useful for server configs, custom scripts, and an extra recovery layer.

How many snapshots should I keep?

A common baseline is 14 daily snapshots. If you run frequent updates or accept orders, consider 30. Storage growth depends on daily change rate.

What’s the fastest way to verify my backups aren’t corrupt?

Restore a directory to a test path and compare checksums for a sample set of files. For deeper checks, run a full restore rehearsal quarterly.