MHOSTMHOST

Backups with rsync

rsync copies directories fast and efficiently by transferring only the changed data — locally to another disk or to a remote server over SSH. This article covers the key flags -a, -v, -z, --delete, backing up a directory to another mHost server, and a wrapper script you can schedule with cron.

What you'll need

  • An mHost VPS on Ubuntu 22.04/24.04, Debian 11/12, or AlmaLinux/Rocky Linux 8/9 — the source server you're backing up.
  • A second server (for example, another mHost VPS) or a separate disk/partition on the same server — somewhere to keep the copies.
  • SSH access as root or a user with sudo rights: to the source server, and — for the remote case — to the destination server too.
  • The rsync package — usually already on the system, but worth checking (Step 1).
Tip: the commands below are given with sudo. If you're already connected as root and there's no sudo on the system, run the same commands without the sudo prefix.

Key flags: -a, -v, -z, --delete

The -a (--archive) flag is the most important one: it's shorthand for -rlptgoD. rsync recurses into directories (-r), preserves symlinks as symlinks (-l), permissions (-p), modification times (-t), group (-g) and owner (-o), and device/special files (-D). Note that -a does not preserve ACLs (-A), extended attributes (-X), access times (-U), creation times (-N), or hard links (-H) — add those separately if you need them.

  • -v (--verbose) — prints the files being transferred and a short summary at the end. Use -vv for more detail.
  • -z (--compress) — compresses data on the fly before sending it over the network. Noticeably helpful on slow or bandwidth-limited links; on a fast link between servers in the same data center the gain is usually small, and close to zero for already-compressed files (archives, video, images).
  • --delete — removes files on the receiving side that no longer exist on the sending side, so the copy stays an exact mirror instead of accumulating files for years.
Important: --delete is a potentially dangerous flag — a wrong path in the command and you can delete the wrong thing in the wrong place. Before the first run of a command with --delete, always check it with -n (--dry-run) — rsync will show what would happen without changing anything on disk.
Tip: pay attention to the trailing slash on the source path. rsync -avz /var/www/ /mnt/backup/ copies the contents of the /var/www directory into /mnt/backup. But rsync -avz /var/www /mnt/backup (no trailing slash on the source) creates an extra nested www directory inside /mnt/backup. Keep the trailing slash on the source path — and, for consistency, on the destination too — the same in every command and in the script: it's the most common mistake when getting started with rsync.

Step 1. Make sure rsync is installed

rsync is needed on both sides — the source server and the server where the copies are kept.

Check whether it's already there:

bash
rsync --version

If the command isn't found, install the package.

Ubuntu / Debian:

bash
sudo apt update
sudo apt install -y rsync

AlmaLinux / Rocky Linux:

bash
sudo dnf install -y rsync

Repeat the check (and the install, if needed) on both servers.

Step 2. Set up SSH key access to the destination server

The wrapper script (Step 5) and cron (Step 6) will run rsync unattended — password login won't work here, you need SSH key authentication with no passphrase. Run the commands below on the source server, as the user the backup will run as (usually root).

  1. Generate a dedicated key for backups:
bash
ssh-keygen -t ed25519 -f /root/.ssh/id_rsync_backup -N ""

-N "" sets an empty passphrase — without it, a script launched from cron has no way to type the key's password.

Important: a passphrase-less key is a deliberate trade-off for automation. Use it only for this task; on the destination server, where possible, set up a separate user that only has access to the backup directory.
  1. Copy the public key to the destination server — backup in the example below can be any existing user on the destination server (including root) that has write access to the target directory:
bash
ssh-copy-id -i /root/.ssh/id_rsync_backup.pub backup@203.0.113.10

The first time, ssh-copy-id will ask for a password (or use your current key) to log in to backup@203.0.113.10 — that's expected; after that, the new key logs in without a password.

Replace backup@203.0.113.10 with the user and address of your own destination server.

  1. Confirm the connection works without a password:
bash
ssh -i /root/.ssh/id_rsync_backup backup@203.0.113.10 "echo OK"

If you get OK back with no password prompt, you're ready for rsync itself.

Step 3. Local directory sync

The local option is useful when you just need to mirror a directory to another disk, partition, or mounted network storage on the same server — no SSH, no second server.

Start with a dry run using -n (--dry-run), which changes nothing on disk:

bash
rsync -avzn --delete /var/www/ /mnt/backup/www/

Check the output, and if it looks right, run it for real (without -n):

bash
rsync -avz --delete /var/www/ /mnt/backup/www/
Tip: if /mnt/backup/www/ doesn't exist yet, create it first: mkdir -p /mnt/backup/www. Newer rsync versions can create missing directories on the fly with the --mkpath flag, but a directory you created in advance works just as reliably on any version.

Step 4. Back up a directory to another server over SSH

The difference from the local case is that the destination is given as user@host:path, with a colon. Start with the same kind of dry run:

bash
rsync -avzn --delete -e "ssh -i /root/.ssh/id_rsync_backup" /var/www/ backup@203.0.113.10:/backup/myserver/www/

Check the output, then run it for real:

bash
rsync -avz --delete -e "ssh -i /root/.ssh/id_rsync_backup" /var/www/ backup@203.0.113.10:/backup/myserver/www/

What's new compared to the local case:

  • backup@203.0.113.10:/backup/myserver/www/ — the user, the IP (or domain) of the destination server, and the path on it.
  • -e "ssh -i /root/.ssh/id_rsync_backup" — explicitly tells rsync to use SSH with the specific key file from Step 2. If the key were saved under a standard name (~/.ssh/id_ed25519 or id_rsa) on the standard port 22, you could skip -e entirely — modern rsync already uses SSH by default. -e is needed when SSH needs non-default settings: a specific key file, or a non-standard port (-e "ssh -p 2222 -i /root/.ssh/id_rsync_backup").

As with the local case, create the destination directory in advance:

bash
ssh -i /root/.ssh/id_rsync_backup backup@203.0.113.10 "mkdir -p /backup/myserver/www"
Tip: this reuses the same SSH port you already use to administer the destination server, so there's nothing new to open in the firewall. If you're setting up ufw on the destination server from scratch, don't forget to allow SSH itself first — see Firewall with ufw.

Step 5. A wrapper script for regular backups

Instead of typing the command by hand, wrap it in a script: with logging, error handling, and protection against overlapping runs if the previous backup hasn't finished yet.

Create the file:

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

Contents:

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

# Source directory and backup destination
SRC="/var/www/"
DEST_USER="backup"
DEST_HOST="203.0.113.10"
DEST_PATH="/backup/myserver/www/"
SSH_KEY="/root/.ssh/id_rsync_backup"

LOG_FILE="/var/log/rsync-backup.log"
LOCK_FILE="/var/lock/rsync-backup.lock"

# Prevent overlapping runs if the previous backup is still going
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
  echo "$(date '+%Y-%m-%d %H:%M:%S') Previous backup is still running, exiting" >> "$LOG_FILE"
  exit 1
fi

echo "$(date '+%Y-%m-%d %H:%M:%S') Backup started" >> "$LOG_FILE"

if rsync -avz --delete -e "ssh -i ${SSH_KEY}" "$SRC" "${DEST_USER}@${DEST_HOST}:${DEST_PATH}" >> "$LOG_FILE" 2>&1; then
  echo "$(date '+%Y-%m-%d %H:%M:%S') Backup finished OK" >> "$LOG_FILE"
else
  echo "$(date '+%Y-%m-%d %H:%M:%S') Backup FAILED" >> "$LOG_FILE"
  exit 1
fi

Make the script executable and root-only:

bash
sudo chmod 700 /usr/local/sbin/rsync-backup.sh

Run it by hand and check the log:

bash
sudo /usr/local/sbin/rsync-backup.sh
tail -n 20 /var/log/rsync-backup.log
Done: if the log shows "Backup finished OK" and the files showed up on the destination server, the script is ready to be scheduled.
Tip: replace SRC, DEST_USER, DEST_HOST, DEST_PATH, and SSH_KEY at the top of the script with your own values — you don't need to touch the rest of the logic (logging, the overlap lock, error handling).

Step 6. Schedule the script with cron

Open root's crontab (the same user the SSH key was set up for in Step 2):

bash
sudo crontab -e

Add a line — for example, a daily run at 02:30:

30 2 * * * /usr/local/sbin/rsync-backup.sh

The script logs to /var/log/rsync-backup.log on its own, so redirecting output in crontab too isn't necessary.

For a detailed look at cron syntax, other scheduling options (hourly, specific weekdays), and common mistakes, see Automating backups with cron.

What's next

rsync with --delete only keeps the latest state of a directory, not versions or history: if a file quietly got corrupted a while back, there's no older copy left — the same --delete removed it there too. For versioned, encrypted, deduplicated backups, see the article on restic: Backups with restic.