rsync for backups: the flags that matter and the ones that quietly change what is copied
rsync is the right tool for most file-level backups and the wrong tool to run
from memory. A few of its defaults are surprising, and one of them — the trailing
slash on the source path — changes what gets copied in a way that is easy to miss
until you are restoring.
The trailing slash rule
This is the single most important thing to get right:
rsync -a /data/ /backup/data/ # copies the CONTENTS of /data into /backup/data
rsync -a /data /backup/data/ # copies /data ITSELF, as /backup/data/data
A trailing slash on the source means "the contents of this directory". No trailing slash means "this directory, as an entry". Both are useful and the difference is invisible in the command unless you are looking for it.
The habit that prevents the mistake is to always use a trailing slash on both
sides and be explicit about what that means, or to test with --dry-run before
the first real run.
Always dry-run first
--dry-run (or -n) prints what would be transferred and changes nothing.
Combined with -v it is a complete rehearsal:
rsync -avn --delete /data/ /backup/data/
Read the output before removing the n. This is especially important with
--delete, where a wrong source path means the backup target gets emptied.
The archive flag, and what it actually includes
-a is short for -rlptgoD, and each letter matters:
| Flag | Preserves |
|---|---|
-r |
recurse into directories |
-l |
symlinks as symlinks |
-p |
permissions |
-t |
modification times |
-g |
group |
-o |
owner |
-D |
device files and special files |
-t is the one that makes rsync efficient: because modification times are
preserved, the next run can skip unchanged files by comparing size and mtime
instead of reading content. Without it, every run re-copies everything, which is
correct and useless.
-o and -g only work when running as root. As a normal user they are silently
ignored, which is worth knowing because a backup that looks complete may have
lost ownership information.
The flags that change behaviour in ways that bite
--delete removes files at the destination that no longer exist at the
source. This is what makes a backup mirror the source rather than accumulate
history. It is also the flag that turns a typo into data loss, and it does not
prompt.
# safe form: delete only what is missing, but keep a safety net
rsync -av --delete --backup --backup-dir=/backup/trash/ /data/ /backup/data/
--backup-dir moves anything that would be deleted into a separate directory
instead of destroying it. For a first-time setup this is worth the disk space.
--exclude is evaluated against the transfer, and the pattern rules are not
shell globs. A common error is --exclude '*.log', which works, versus
--exclude 'cache', which excludes any file or directory named cache at any
depth — usually what you want, occasionally a surprise.
rsync -av --exclude='*.tmp' --exclude='.cache/' /data/ /backup/data/
The trailing slash on .cache/ restricts the match to directories, which is the
documented way to avoid matching a file of the same name.
--exclude-from reads patterns from a file, which is the maintainable form
once there are more than a handful:
rsync -av --exclude-from=/etc/backup-excludes.txt /data/ /backup/data/
--checksum makes rsync compare content hashes instead of size and mtime.
It is much slower and it catches the case where a file changed without its
modification time changing — rare, but real on files restored from an archive.
Use it for verification runs, not for every backup.
--partial keeps a partially transferred file so the next run resumes
instead of starting over. For large files over a slow link this is the difference
between a backup that finishes and one that never does:
rsync -av --partial --progress /data/ /backup/data/
-z compresses during transfer. It helps over slow links and costs CPU. On a
fast local network it slows things down, which is worth knowing before adding it
by habit.
The whole command, with the pieces that matter
rsync -aHv --delete --partial --exclude-from=/etc/backup-excludes.txt \
--numeric-ids /data/ /backup/data/
-H preserves hard links, which matters for anything with a mail spool or a
deduplicating store — without it, hard-linked files become separate copies and
the backup can be much larger than the source. --numeric-ids prevents user and
group names being mapped through the local name service, which keeps ownership
correct when the backup is restored on a machine with different users.
Verifying, because a completed rsync is not a verified backup
rsync exiting zero means it transferred what it decided to transfer. It does not mean the destination matches the source. A verification pass compares the two directories without transferring:
rsync -avn --checksum --delete /data/ /backup/data/ | tail -20
With -n and --checksum, anything listed is a difference. An empty list means
the two trees match by content, which is the check worth running after a backup
that matters.
Running it on a schedule
Under systemd, a oneshot service with a timer is cleaner than a cron entry because the output lands in the journal:
# /etc/systemd/system/backup-data.service
[Unit]
Description=Back up /data
[Service]
Type=oneshot
ExecStart=/usr/bin/rsync -aHv --delete --partial \
--exclude-from=/etc/backup-excludes.txt /data/ /backup/data/
Paired with a timer at a quiet hour, and with Persistent=true so a run missed
while the machine was off happens at the next boot.
The two rules worth keeping
Dry-run with --delete until the output matches what you expect, and check the
trailing slash on the source every single time. Those two habits prevent almost
every rsync backup disaster, and both cost seconds.