VPS Backup Automation with rsync: Hard-Link Snapshots, a Restricted SSH Key and Cron
rsync with --link-dest gives you daily snapshots that look like full copies but only store changed files. Here is a pull-based setup with a locked-down SSH key and a cron schedule.
The simplest automated backup for a Linux VPS is a nightly rsync run from a separate backup server, using --link-dest so each dated snapshot looks like a full copy while unchanged files are stored only once as hard links. Pull from the backup side with a restricted SSH key, dump databases first, prune old snapshots, and test a restore every month.
If you need encryption and block-level deduplication instead, our earlier BorgBackup automation guide covers that route. rsync snapshots trade those features for plain files you can browse and copy back with any tool.
Prerequisites
- The production VPS you want to protect.
- A second machine for backups, ideally on a different node (for example production in Mumbai, backups in Jaipur). VPSWala plans do not include off-server backups, so this copy is yours to build.
- A file system on the backup side that supports hard links (ext4 and XFS do).
- rsync installed on both ends.
Step 1: Create a restricted SSH key
Pulling from the backup server means a compromised production VPS holds no credentials that can delete old backups. On the backup server, generate a key, then add it to /root/.ssh/authorized_keys on the production VPS with options that limit what it can do:
from="203.0.113.20",restrict ssh-ed25519 AAAA... backup@jaipurThe sshd manual describes from= as requiring the connecting host to match a pattern list (CIDR is supported), and restrict as disabling port, agent and X11 forwarding, PTY allocation and ~/.ssh/rc. You can go further with command=, which forces a fixed command for that key and exposes the client's original request in SSH_ORIGINAL_COMMAND, so a wrapper script can allow only rsync in read-only mode.
Step 2: Dump databases before copying files
Copying live database files gives you an inconsistent backup. Have the production VPS write a dump shortly before the rsync window:
# /etc/cron.d/db-dump on the production VPS
15 1 * * * root mysqldump --single-transaction --all-databases | gzip > /var/backups/mysql.sql.gzUse pg_dump or pg_dumpall for PostgreSQL in the same way.
Step 3: The snapshot script
#!/bin/sh
set -eu
SRC="root@vps1.example.com:/"
DEST="/backups/vps1"
NOW=$(date +%Y-%m-%d)
rsync -aHAX --numeric-ids --delete \
-e "ssh -i /root/.ssh/vps1_backup" \
--exclude-from=/etc/backup/vps1.exclude \
--link-dest="$DEST/latest" \
--log-file="$DEST/rsync-$NOW.log" \
"$SRC" "$DEST/$NOW/"
ln -sfn "$DEST/$NOW" "$DEST/latest"What each part does, according to the rsync manual:
-ais shorthand for-rlptgoD. It does not include hard links, ACLs or extended attributes, which is why-H,-Aand-Xare added.--numeric-idskeeps user and group numbers as they are, instead of mapping names on the backup server.--link-dest=DIRhard-links files from DIR when they are unchanged. Files must match in all preserved attributes to be linked, a relative DIR is taken relative to the destination, and the manual says it works best when copying into an empty destination, which a new dated folder is.--deleteremoves files from the new snapshot that no longer exist on the source. It only affects the new folder, not older snapshots. The manual calls it dangerous if misused and recommends a--dry-runfirst; it is also disabled automatically if the sender hits I/O errors.
The exclude file should list /proc, /sys, /dev, /run, /tmp and any cache directories. On the first run latest does not exist yet, so rsync copies everything; later runs only transfer changes.
Step 4: Schedule and prune
# /etc/cron.d/vps1-backup on the backup server
45 1 * * * root /usr/local/sbin/backup-vps1.sh
30 5 * * * root find /backups/vps1 -mindepth 1 -maxdepth 1 -type d -name '20*' -mtime +30 -exec rm -rf {} +Deleting an old snapshot is safe: a file still referenced by a newer snapshot keeps its data until the last hard link is gone. If you want hourly, daily and weekly tiers without writing rotation logic, rsnapshot wraps this same technique. Its manual describes retain levels, a tab-separated config file, and rsnapshot configtest to check it.
Verify the backups
- Check the log for errors and a non-zero exit status; send yourself an alert when the script fails.
- Run
du -sh /backups/vps1/*. The newest snapshot should add only a small amount on top of the previous one. - Once a month, restore a directory and the database dump to a test VPS and start the application.
Common mistakes
- Pushing from production with a key that can also delete backups.
- Forgetting
-H,-Aand-X, then finding restored files with the wrong permissions or links. - Keeping the only backup on the same VPS or the same node.
- Never testing a restore.
A small VPSWala KVM VPS with enough RAID NVMe disk makes a good backup target, and production workloads can sit on the 9950X VPS range. Both come with full root access and are unmanaged unless agreed in writing.
Sources
Not sure which size?
Send the stack, get a size.
Tell us the operating system, application stack, current traffic, database size and where it hurts today. You get a sizing recommendation, the matching plan and a price.
Related
Magento VPS Performance: Varnish Full Page Cache, Redis Sessions and Separate Cache Databases
Most Magento speed on a VPS comes from caching layers, not raw CPU. This guide sets up Varnish for full page cache and Redis for sessions and default cache, using Adobe's documented commands.
Windows RDP VPS Server in Kota, Rajasthan: Remote Desktop Hosting for Coaching Institutes and Traders
A Windows RDP VPS gives Kota institutes and trading firms one remote office desktop for Tally, billing and admin software. Here is how to plan the node, licences, sizing and security.
Ruby on Rails VPS Deployment with Kamal 2: deploy.yml, kamal-proxy and Automatic SSL
Kamal turns a plain Linux VPS into a Rails deploy target with zero-downtime releases and Let's Encrypt SSL. Here is the setup, the config that matters and the errors you will meet first.