Automated Backups with Rsync and Cron on Linux 2026

The backup you never tested is not a backup — it is a hope. Every administrator who has lost data has a version of the same story: the job had been "running fine for months," and the first time anyone tried to restore from it, the files were empty, stale, or never there at all. The fix is not exotic software. rsync and cron ship with every Linux distribution and, used carefully, give you incremental off-server backups that run on a schedule and that you can actually verify. This guide builds that pipeline step by step: a sync script, a dry-run safety net, a cron schedule, and — the part most guides skip — a restore-integrity check.

Quick Verdict
rsync -avz --delete mirrors a directory to a remote host efficiently, copying only what changed. The --delete flag is powerful and dangerous: it removes files on the destination that are gone from the source, so always run the command with --dry-run first to see what it would do. Schedule it with cron, then periodically verify a restore with rsync -c (checksum) so you find a broken backup before you need it.

Understand the Core Command

Everything here is built on one rsync invocation. Understanding each flag is what keeps it safe:

1rsync -avz --delete /var/www/ backup@remote-host:/backups/www/
  • -a (archive) is shorthand for "recurse and preserve almost everything" — permissions, timestamps, symlinks, and ownership. It is the flag you almost always want.
  • -v (verbose) reports what is being transferred so the logs are meaningful.
  • -z (compress) compresses data in transit, which speeds up transfers over a network link.
  • --delete removes files on the destination that no longer exist on the source, keeping the backup a true mirror rather than an ever-growing pile of deleted files.

The trailing slash on the source (/var/www/) matters: it means "the contents of this directory." Without it, rsync would create a www subdirectory inside the destination. This is the most common rsync mistake, so be deliberate about it.


Always Dry-Run First

--delete does exactly what it says, and on the wrong path it will cheerfully erase the destination to match an empty or mistyped source. Before any real run — and every time you change the command — preview it with --dry-run (short flag -n):

1rsync -avz --delete --dry-run /var/www/ backup@remote-host:/backups/www/

This performs a trial run that makes no changes but prints the same output a real run would. Read it: every line prefixed for deletion is a file rsync is about to remove from the backup. If that list looks wrong — especially if it proposes deleting everything — stop and check your paths. A dry-run costs a few seconds and has saved countless backups from being wiped by a stray slash.


Set Up Key-Based Access

A scheduled backup cannot stop to type a password, so the source server needs key-based SSH access to the backup host. Generate a dedicated key if you do not already have one, then copy its public half to the backup account:

1ssh-keygen -t ed25519 -f ~/.ssh/backup_key -N ""
2ssh-copy-id -i ~/.ssh/backup_key.pub backup@remote-host

Test that the key works without a prompt before going further:

1ssh -i ~/.ssh/backup_key backup@remote-host echo OK

For a backup-only account you can lock the key down further on the remote side, but a passphrase-less key reachable only from your server is the minimum a cron job needs.


Wrap It in a Script

Putting the command in a script keeps the cron entry clean, lets you add logging, and gives you one place to edit. Create /usr/local/bin/backup.sh:

 1#!/bin/bash
 2set -euo pipefail
 3
 4SRC="/var/www/"
 5DEST="backup@remote-host:/backups/www/"
 6KEY="/root/.ssh/backup_key"
 7LOG="/var/log/backup.log"
 8
 9echo "=== backup started $(date) ===" >> "$LOG"
10rsync -avz --delete -e "ssh -i $KEY" "$SRC" "$DEST" >> "$LOG" 2>&1
11echo "=== backup finished $(date) rc=$? ===" >> "$LOG"

set -euo pipefail makes the script abort on any error rather than ploughing on after a failed transfer. The -e "ssh -i $KEY" tells rsync which SSH key to use. All output is appended to a log you can inspect. Make it executable:

1sudo chmod +x /usr/local/bin/backup.sh

Run it once by hand and read the log to confirm a real transfer succeeds before you ever schedule it.


Schedule It with Cron

With a working script, scheduling is one line. Edit root's crontab (the source files often need root to read):

1sudo crontab -e

Add an entry to run nightly at 2:30 a.m.:

130 2 * * * /usr/local/bin/backup.sh

The five fields are minute, hour, day-of-month, month, and day-of-week. 30 2 * * * means 02:30 every day. Pick an hour when the source is quiet so the backup captures a consistent state and does not compete with peak traffic. After the first scheduled run, check /var/log/backup.log for the start and finish markers — a job that is scheduled but silently failing looks identical to one that is working until you actually read the log.


Verify the Restore — Do Not Skip This

A backup is only proven when you have restored from it. The cheapest verification is to pull a copy back to a scratch directory and compare it to the source by checksum, not just by size and timestamp:

1rsync -avzc --dry-run -e "ssh -i /root/.ssh/backup_key" \
2  backup@remote-host:/backups/www/ /tmp/restore-check/

The -c flag forces rsync to compare file content hashes rather than the usual size-and-mtime heuristic. Combined with --dry-run, a clean run that reports nothing to transfer means every file on the backup is byte-for-byte identical to the source — genuine proof of integrity, not just that files of the right name exist. Run this check on a calendar reminder, monthly at least. A backup you have restore-tested is the only kind worth having.

It is worth being honest about what this protects against. A live mirror defends you against hardware failure, an accidental rm, or a server that has to be rebuilt — you copy the backup back and carry on. It does not defend against a mistake that propagates: if a file is corrupted or maliciously encrypted and the backup runs before you notice, --delete faithfully mirrors the damage and your good copy is overwritten. That is why a single mirror is a starting point, not a complete strategy, and why the dated-snapshot approach below matters once the basics are working — it keeps yesterday's good version even after today's bad one syncs.


A Pipeline You Can Trust

The components here are deliberately boring: rsync to move only what changed, cron to run it without you, a script to keep it tidy and logged, and a checksum dry-run to prove it works. The discipline is what makes it dependable — dry-run before every real --delete, read the log after the first scheduled run, and restore-test on a schedule.

For a richer history, point each run at a dated path or layer in rsync's --link-dest for space-efficient snapshots; for off-site durability, send to a host in a different region. But start with the simple, verified mirror. A two-line cron entry that you have actually restored from protects you far better than an elaborate scheme nobody has ever tested.

References

Posts in this series