Two tools cover almost every server backup need. rsync makes a mirror: an exact, browsable copy of a directory somewhere else, transferring only what changed. restic makes snapshots: encrypted, deduplicated, versioned, to almost any storage, so you can restore a file as it was on any day. Use rsync when you want simplicity; use restic when you want history and off-site safety. Many servers use both.
rsync: a mirror
rsync -avz --delete /var/www/ backup@203.0.113.50:/srv/backups/web01/www/-apreserves permissions, ownership, timestamps and symlinks.-vshows what it does;-zcompresses in transit.--deleteremoves files at the destination that no longer exist at the source — a true mirror. Leave it off and deleted files accumulate at the destination, which is sometimes what you want.- Trailing slashes matter:
/var/www/copies the contents;/var/wwwcopies the directory itself.
Run it from cron nightly and you have a copy of yesterday. Its weakness is that it is only one copy: a file corrupted or encrypted by ransomware on Monday is mirrored on Monday night, and the good version is gone. Two ways round that: keep dated copies using hard links, so unchanged files cost no extra space —
TODAY=/srv/backups/web01/$(date +%F)
YESTERDAY=/srv/backups/web01/$(date -d yesterday +%F)
rsync -a --delete --link-dest="$YESTERDAY" /var/www/ "$TODAY/"— or use restic, which does this properly.
restic: snapshots
restic stores backups in a repository — a directory, an SFTP server, an S3 bucket, Backblaze B2, or via rclone almost anything — encrypted with a password you choose. Each run creates a snapshot; identical blocks are stored once across all snapshots, so daily backups of a 20 GB site cost 20 GB plus the changes.
Install and initialise:
sudo apt install restic -y # dnf install restic
export RESTIC_REPOSITORY=s3:s3.eu-west-2.amazonaws.com/my-backups/web01
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export RESTIC_PASSWORD='a-long-passphrase-stored-somewhere-safe'
restic initLose the password and the backups are unreadable. Put it in a password manager now.
Back up:
restic backup /var/www /etc /home --exclude='**/cache/**' --exclude='*.log'Databases need a dump first, because copying live MySQL files gives an inconsistent result:
mysqldump --single-transaction --all-databases | gzip > /var/backups/mysql-all.sql.gz
restic backup /var/backups/mysql-all.sql.gzOr pipe it straight in with no temporary file: mysqldump ... | restic backup --stdin --stdin-filename mysql-all.sql.
See what you have, and prune old snapshots on a policy:
restic snapshots
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --pruneRestore a whole snapshot, or one file as it was on a date:
restic restore latest --target /tmp/restore
restic restore 4f2a9c1b --target /tmp/restore --include /var/www/example.com/wp-config.phprestic mount /mnt/restic presents every snapshot as a browsable directory tree, which is the easiest way to find "the version of that file from last Tuesday".
The nightly script
/usr/local/bin/backup.sh, run from cron at 02:00, with the environment in a root-only file:
#!/bin/bash
set -euo pipefail
source /root/.restic.env # the RESTIC_* and AWS_* exports, chmod 600
mysqldump --single-transaction --quick --routines --all-databases \
| restic backup --stdin --stdin-filename mysql-all.sql --tag mysql --quiet
restic backup /var/www /etc /home \
--exclude='**/cache/**' --exclude='**/node_modules/**' --exclude='*.log' \
--tag files --quiet
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune --quiet
restic check --read-data-subset=5% --quiet
curl -fsS -m 10 https://hc-ping.com/your-uuid > /dev/null # dead man's switchrestic check --read-data-subset verifies a random 5% of the stored data each night, so bit rot at the storage provider is found in weeks, not at restore time. The final curl pings a monitor only on success; if the script fails at any line, set -e stops it before the ping and the monitor alerts you. See knowing when cron jobs fail.
Protecting the repository
The server holds credentials to the bucket, so a compromised server could delete the backups. Use an S3 key with no delete permission for the nightly job (restic's forget --prune then needs to run from elsewhere with a fuller key), or enable object versioning on the bucket so deletions are recoverable. Or run restic's REST server in --append-only mode on a second machine.
Test it
Once a quarter: restore the latest snapshot to a fresh VPS, import the database, load the site. The steps and why they matter are in automatic backups and testing the restore.
On a VPSPioneer managed VPS, this is the pattern we run for you — nightly restic snapshots to off-site storage with 14-day retention and a monitored job — and we will add a second repository under your own account if you want a copy that is entirely yours.