How to Automate Server Backups with Cron and rsync on a Linux VPS

Ask any sysadmin who has been doing this a while and they will tell you the same story with different details. There was a backup. It had been running for months. Nobody had ever restored from it. The day it was needed, it turned out the script had been silently failing since a disk filled up in March.

A backup you have never restored is not a backup. It is a feeling.

Here is how to build one that actually runs, actually leaves the server, and that you have actually tested, using rsync and cron, both of which are already sitting on your VPS.

Why Clicking “Generate Backup” Isn’t Enough

The cPanel backup button is genuinely useful, and our guide to generating a full backup in cPanel walks through it. Take one before any risky change.

But it depends on a human remembering. And the week you forget is, with a reliability that borders on supernatural, the week the server has a problem. Automation removes the human from the equation entirely.

rsync in Thirty Seconds

rsync copies files, but only the parts that changed since last time. On a 40GB website that is the difference between a backup taking two minutes and taking an hour.

rsync -avh /var/www/ /backups/www/

The flags earn their keep: -a preserves permissions, timestamps and symlinks, -v tells you what it’s doing, -h prints sizes humans can read.

Note the trailing slash on /var/www/. With it, you copy the contents of the directory. Without it, you copy the directory itself, nested one level deeper than you wanted. This trips up everybody at least once.

The Script

Put the whole routine in a file so cron can run it unattended. Create /root/backup.sh:

#!/bin/bash
DATE=$(date +%F)
BACKUP_DIR="/backups/$DATE"
mkdir -p "$BACKUP_DIR"

# Website files
rsync -avh /var/www/ "$BACKUP_DIR/www/"

# Every database on the server
mysqldump --defaults-extra-file=/root/.my.cnf --all-databases > "$BACKUP_DIR/all-databases.sql"

# Squash it into one archive
tar -czf "/backups/backup-$DATE.tar.gz" -C /backups "$DATE"
rm -rf "$BACKUP_DIR"

# Keep two weeks of history, discard the rest
find /backups -name "backup-*.tar.gz" -mtime +14 -delete
chmod +x /root/backup.sh

Notice the database password is not in the script. It lives in /root/.my.cnf, locked to chmod 600:

[mysqldump]
user=root
password=your_password_here

Put a password directly into a shell script and it becomes visible to anyone running ps aux while the job executes. It also ends up in your backup archive, which then contains the keys to the thing it is backing up. Don’t.

Handing It to Cron

crontab -e

Run it nightly at 2am, and keep a log:

0 2 * * * /root/backup.sh >> /var/log/backup.log 2>&1

The five fields are minute, hour, day of month, month, day of week. So 0 2 * * * reads as “minute 0 of hour 2, every day”. The 2>&1 matters more than it looks: without it, error messages go nowhere and a broken backup fails in total silence.

Get It Off the Server

A backup sitting on the same disk as the thing it backs up will not save you from a failed disk, a deleted VPS, or ransomware that encrypts everything it can reach.

Set up SSH key authentication to a second machine, then add one line to the script:

rsync -avh -e ssh /backups/backup-$DATE.tar.gz user@remote-server:/offsite-backups/

Keys, not passwords. Cron cannot type a password at a prompt, and a job that hangs waiting for one will sit there forever, backing up nothing.

Restoring, Which Is the Whole Point

Restore website files:

tar -xzf backup-2026-07-14.tar.gz -C /restore-location/
rsync -avh /restore-location/2026-07-14/www/ /var/www/

Restore a database:

mysql -u root -p < all-databases.sql

A dump made with --all-databases recreates every database and table on its own, so you don't need to specify a target database name.

Now do this on a spare VPS, this week, while nothing is on fire. An untested restore procedure has a way of revealing its problems at the worst imaginable moment, and discovering your archive is corrupt at 2am during a live outage is an experience worth paying to avoid.

The Failure Nobody Sees Coming

Everything above protects you against losing data. Almost nothing protects you against the backup quietly stopping.

Disk fills up. Permissions change. The mysqldump credentials expire. The cron job keeps firing, the script keeps failing, and the log file keeps recording that failure to a directory nobody has looked at in six months.

So build the alarm, not just the backup:

  • Alert on failure, not success. A daily "backup OK" email becomes noise you filter within a week. A "backup FAILED" email is one you actually read
  • Check the archive has size. A 0-byte tarball is a failed backup that looks like a successful one
  • Follow 3-2-1: three copies, two kinds of storage, one off-site
  • Encrypt anything leaving the server, especially database dumps holding customer data
  • Set a retention policy or backups will quietly fill the disk and, in a fitting piece of irony, cause the outage they were meant to protect against

Your Homework

Pick a Tuesday. Take last night's archive, spin up a throwaway VPS, and restore the whole thing end to end. Time yourself.

Whatever you learn in that hour is the difference between having backups and being able to recover. They are not the same thing, and most people only discover the gap when it is far too late to do anything about it.