VPS Snaps

How to back up a server with rsync

sudo rsync -a /var/www/ /backups/www/ copies a directory with its permissions, owners and timestamps, and every later run sends only what changed. Put user@host: in front of the destination to back up over SSH, and add --link-dest to keep a complete copy for every day while storing only the files that changed.

8 min readUpdated Tested on Ubuntu 24.04 LTS, rsync 3.2.7

Your first backup

rsync copies a source to a destination and, on later runs, skips files whose size and modification time already match. Run it as root so it can read everything and set owners on the copy:

Terminal
sudo rsync -a /var/www/ /backups/www/

The trailing slash on the source changes the result:

CommandResult
rsync -a /var/www/ /backups/www/The contents of /var/www land in /backups/www.
rsync -a /var/www /backups/rsync creates /backups/www and copies into it.
rsync -a /var/www /backups/www/You get /backups/www/www, usually by mistake.

-v lists each file it sends (add -h for readable sizes). A second run with nothing changed sends almost nothing:

Terminal
sudo rsync -av /var/www/ /backups/www/
Output
sending incremental file list

sent 380 bytes  received 17 bytes  794.00 bytes/sec
total size is 5,000,057  speedup is 12,594.60

rsync creates only the last directory of the destination. If /backups does not exist, it fails with exit code 11; add --mkpath to create the whole path.

What -a includes, and what it leaves out

-a (archive) is shorthand for -rlptgoD:

FlagKeeps
-rRecurses into directories.
-lSymlinks, as symlinks.
-pPermissions.
-tModification times. Without them, every run has to re-check every file.
-gGroup.
-oOwner. Only works when the receiving side runs as root.
-DDevice and special files (devices also need root).

It does not keep hard links (-H), ACLs (-A) or extended attributes (-X). For a faithful copy, add them, plus --numeric-ids so owners are kept by number instead of being mapped to same-named users on the receiving side:

Terminal
sudo rsync -aHAX --numeric-ids /var/www/ /backups/www/

Back up to another server over SSH

Put user@host: before the destination path. rsync starts itself on the other machine over SSH, so it must be installed on both ends, and only the changed parts of files cross the network:

Terminal
sudo rsync -az /var/www/ [email protected]:/backups/www/

-z compresses data in transit only; the copies are stored uncompressed. Because backup is not root on the other server, the copies are owned by backup and the original owners are not kept there. If you need them, receive as root or keep a tar archive, which records owners inside the file.

For a non-standard port or a dedicated key, give rsync the ssh command with -e:

Terminal
sudo rsync -az -e 'ssh -p 2222 -i /root/.ssh/backup_ed25519' /var/www/ [email protected]:/backups/www/

To stop a backup from filling the server's uplink, cap it with --bwlimit. A plain number is KiB per second; --bwlimit=5m is 5 MiB per second. In our test, --bwlimit=1000 took 4.97 seconds to copy a 5,000,000-byte file.

Consider pulling instead of pushing: run rsync on the backup server, with the production server as the source. A compromised web server then holds no credentials that could reach, or delete, its own backups.

Set up key login first: SSH key authentication. For one-off copies between two servers with scp or rsync, see how to copy files between servers.

--delete: mirror deletions, carefully

By default rsync never removes anything from the destination, so files you delete on the server stay in the backup forever. --delete removes destination files that no longer exist in the source. That keeps a mirror exact, and it also mirrors mistakes: delete a folder by accident and the next run deletes it from the backup too.

Preview first with -n (--dry-run) and -i (--itemize-changes):

Terminal
sudo rsync -ani --delete /var/www/ /backups/www/
Output
*deleting   html/error.log
.d..t...... html/
>f+++++++++ html/new-page.php

*deleting is a file that would be removed, >f+++++++++ a new file to send, and .d..t...... a directory whose timestamp would change. Files you exclude are also protected from deletion unless you add --delete-excluded.

If the source is empty, for example because a disk failed to mount, --delete empties the backup. Add --max-delete=100: when a run would delete more than 100 files, rsync stops deleting and exits with code 25. Better still, keep dated copies, as below, so no single run can wipe your history.

--link-dest=DIR compares each file with the copy in DIR. Unchanged files become hard links to that copy instead of new files, so every day's directory is complete but only changed files take new space. This script keeps one directory per day and a latest link to the newest:

/usr/local/bin/rsync-snapshot.sh
#!/bin/sh
set -eu

SRC=/var/www/
DEST=/backups/www
TODAY=$(date +%F)

mkdir -p "$DEST"
rsync -a --delete --link-dest="$DEST/latest" "$SRC" "$DEST/$TODAY/"
ln -sfn "$TODAY" "$DEST/latest"

On the first run there is no latest yet: rsync warns that the --link-dest directory does not exist and makes a normal full copy. After that, each day links against the previous one. With a 5 MB file that did not change, the second day took 28 KB:

Terminal
cd /backups/www && du -sh 2026-10-02 2026-10-03
Output
4.9M	2026-10-02
28K	2026-10-03

ls -li shows why: both names point to the same inode, with a link count of 2. Deleting an old day removes only its names; the data stays while a newer day still links to it.

Terminal
ls -li 2026-10-02/html/uploads/video.mp4 2026-10-03/html/uploads/video.mp4
Output
2658379 -rw-r--r-- 2 root root 5000000 Oct  3 16:44 2026-10-02/html/uploads/video.mp4
2658379 -rw-r--r-- 2 root root 5000000 Oct  3 16:44 2026-10-03/html/uploads/video.mp4

Prune these directories by the date in their name, not with find -mtime. rsync -a gives each copy the source directory's modification time, so in our test a copy made today already matched -mtime +30. This keeps the newest 30 days:

Terminal
find /backups/www -mindepth 1 -maxdepth 1 -type d -name '20??-??-??' | sort | head -n -30 | xargs -r -d '\n' rm -rf --

Hard-linked days share one copy of each unchanged file, so damage to that file on disk affects every day that links to it. They protect you from deletions and bad edits, not from losing the backup disk: keep a second copy elsewhere. The backup disk must support hard links (ext4 and XFS do).

rsnapshot automates exactly this rotation, with hourly, daily and weekly levels in one config file: see how to back up a server with rsnapshot.

Exclude files

Exclude patterns are matched against paths inside the transfer. A trailing / matches directories only, and a leading / anchors the pattern to the top of the transfer:

Terminal
sudo rsync -a --exclude='cache/' --exclude='node_modules/' --exclude='*.log' /var/www/ /backups/www/
  • cache/ matches every directory named cache, at any depth.
  • /html/cache/ matches only /var/www/html/cache, because the transfer starts at /var/www/.
  • --exclude-from=/etc/rsync/www.exclude reads one pattern per line.

Restore from an rsync backup

A restore is the same command with source and destination swapped. Restore to a new directory first and compare:

Terminal
sudo rsync -a --mkpath /backups/www/2026-10-02/ /restore/www/
Terminal
sudo diff -r /backups/www/2026-10-02 /restore/www

Copy back a single file:

Terminal
sudo rsync -a /backups/www/2026-10-02/html/wp-config.php /var/www/html/

From a remote backup, put the host on the source side:

Terminal
sudo rsync -a --mkpath [email protected]:/backups/www/ /restore/www/

To roll a whole directory back, preview it. With --delete, files created after the backup are removed. Here only one file would change; s and t mean its size and time differ:

Terminal
sudo rsync -ani --delete /backups/www/2026-10-02/ /var/www/
Output
>f.st...... html/index.php

When the list looks right, run it again without -n.

Verify the backup

rsync's quick check compares only size and modification time. To prove the copy matches byte for byte, run a dry run with -c (--checksum) right after the backup. No output means the copies match:

Terminal
sudo rsync -anic --delete /var/www/ /backups/www/2026-10-03/

In our test we changed one byte inside a backed-up file and restored its size and timestamp. The normal dry run reported nothing. The checksum run caught it:

Output
>fc........ html/index.php

rsync exited with status 0 in both runs, so a script must check for output, not the exit code. -c reads every file on both sides, which takes time on large trees; weekly is often enough. For the backup run itself, the exit status does matter:

Exit codeMeaning
0Success.
11Error in file I/O, such as a missing destination parent directory.
23Partial transfer due to error: some files were not copied. Read the messages.
24Partial transfer due to vanished source files: files were deleted while rsync ran. Common on busy servers.
25The --max-delete limit stopped deletions.

Then restore it somewhere and use it. A backup you have not restored is a hope; see how to test a backup restore.

rsync keeps one copy. When you need history, restic and BorgBackup keep many, deduplicated and encrypted. rsync is also the tool for moving a server to a new provider.

Frequently asked questions

Should the rsync source have a trailing slash?
Use one when you want the contents of a directory: /var/www/ copies what is inside /var/www. Without it, /var/www copies the directory itself, creating www inside the destination.
Is rsync a backup or just a copy?
A plain mirror is a copy: with --delete it reproduces deletions on the next run. Dated --link-dest copies, plus a second copy away from the server, turn it into a backup. See snapshots versus backups.
Does rsync compress the backup?
-z compresses data in transit only. Files are stored uncompressed, which is what makes single-file restores and --link-dest work.
Can rsync back up a running database?
Not safely: the files change during the copy. Dump the database first with pg_dump, mysqldump or mongodump, and back up the dump.
How do I limit rsync's bandwidth?
--bwlimit=5m caps it at 5 MiB per second. A plain number, such as --bwlimit=1000, is in KiB per second.

How this was checked

The commands were run on Ubuntu 24.04 LTS, rsync 3.2.7 on October 3, 2026. Any that need something this test server does not have, such as a second server, a cloud account or another database engine, were checked against the official pages below instead.

Sources, on October 3, 2026: