Automate Server Backups with rsync: Incremental, Offsite and Tested
Use rsync over SSH to copy site files and database dumps to another machine, keep dated snapshots with hard links, and avoid the common backup mistakes.
Most people discover the importance of backups after losing something. A good backup is automatic, stored somewhere else, and proven to restore. The classic Linux tool for copying files efficiently is rsync: it transfers only what changed, works over SSH and is installed or one command away on virtually every server. This guide builds a simple but solid backup routine around it.
The principles first
- Off the server. A backup stored only on the machine it protects disappears with that machine. Use a separate server, storage box or at least a separate provider account.
- Automatic. Backups that depend on remembering get forgotten. Schedule them.
- Versioned. A mirror copies your mistakes too. If you delete a file today, tonight's mirror deletes it from the backup as well. Keep several dated generations.
- Tested. Restore regularly into a scratch location and look at the result.
A well-known guideline is "3-2-1": three copies of your data, on two kinds of storage, with one kept offsite. You do not have to follow it to the letter, but it is a good target.
rsync basics
The general form is rsync [options] source destination. The most useful options:
-a(archive) preserves permissions, ownership, timestamps and symbolic links, and recurses into folders.-vlists what is transferred, and-zcompresses data in transit.-n(dry run) shows what would happen without changing anything.--deleteremoves files at the destination that no longer exist at the source.--excludeskips matching paths, such as caches.
Always rehearse with -n first, especially with --delete:
rsync -avn --delete /var/www/ [email protected]:/backups/web/
Pay attention to the trailing slash. /var/www/ means "the contents of the folder", while /var/www without a slash copies the folder itself into the destination. Mixing these up is the classic way to create a nested duplicate or, with --delete, to delete the wrong thing.
Back up over SSH with a dedicated key
Create a key for the backup job (no passphrase, since it runs unattended, so restrict what it can do), and install its public half on the backup machine:
ssh-keygen -t ed25519 -f ~/.ssh/backup_key -N "" -C "backup job"
ssh-copy-id -i ~/.ssh/backup_key.pub [email protected]
rsync -az -e "ssh -i ~/.ssh/backup_key" /var/www/ [email protected]:/backups/web/
Better still, pull backups instead of pushing them: run rsync on the backup machine and fetch from the web server. That way a compromised web server holds no credentials to the backup storage and cannot delete its history. A read-only account on the web server for the backup job limits the damage further.
Dated snapshots and databases
Dated snapshots with hard links
rsync's --link-dest option creates a full-looking folder per day while storing unchanged files only once, using hard links to the previous copy:
#!/bin/bash
set -e
SRC="/var/www/"
DEST="/backups/web"
TODAY=$(date +%F)
rsync -a --delete --link-dest="$DEST/latest" "$SRC" "$DEST/$TODAY/"
ln -sfn "$DEST/$TODAY" "$DEST/latest"
Each dated folder looks like a complete backup you can browse and restore from, yet unchanged files cost almost no extra space. To expire old snapshots, delete folders older than your retention period, for instance with find /backups/web -maxdepth 1 -type d -mtime +30. Check what that command lists before you add a delete action. Hard links only work within one filesystem, so keep all snapshots on the same disk.
Do not forget databases
Copying a running database's data folder with rsync can produce an inconsistent, unusable backup. Dump databases first with mysqldump or pg_dump into a folder, then include that folder in the rsync job. Compress and date the dumps, and keep a number of generations. For stateful applications such as Docker volumes, stop the service briefly or use the application's own export tool.
Schedule it, watch it, test it
Schedule it and watch for failure
Run the script from cron, with a lock to prevent overlaps and a log:
30 2 * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
The more important part is noticing when it fails. Have the script send an alert if rsync exits with a non-zero status, or check from your monitoring that the newest backup is less than 26 hours old. Silent failure is the typical backup tragedy. Also use --bwlimit if the transfer saturates your connection during busy hours.
Test the restore
Once a quarter, pick a snapshot, restore it into a scratch folder or test server, load a database dump, and verify that the site works. Time the process, because the restore duration is part of your real recovery plan. Write the restore steps down. A restore from a snapshot can be as simple as copying a dated folder back into a scratch location and comparing it with the live site:
rsync -a /backups/web/2026-01-15/ /tmp/restore-test/
diff -rq /tmp/restore-test /var/www | head
Differences are expected for files that changed since that day. What you are checking is that the files are present, readable and complete.
What to include
List what your site truly needs to be rebuilt: web files, uploads, database dumps, server configuration (such as /etc/nginx and your crontabs), and the environment files that hold settings. Leave out what can be regenerated, like caches and node_modules, using --exclude. Remember that email, if you host it, has its own storage that needs a plan. See the security page for how backups fit into protection.
Backups and Zebra Hosting
You remain responsible for having a backup strategy that fits your needs, whatever your hosting. If you run a VPS, build a routine like this one from the start.