A small script dumps your databases and copies your files with rsync to a second server over SSH. A cron job runs it every night.
203.0.113.20 with the IP address of the backup server.1. Prepare the backup server
On the backup server, create a user that only receives backups, and a folder for this server's backups:
sudo adduser serverbackup
sudo mkdir -p /backups/web1
sudo chown serverbackup:serverbackup /backups/web1
Don't call the user backup: Ubuntu already has a system user with that name, which can't log in. Make sure rsync is installed on both servers (sudo apt install rsync; it usually is).
2. Give the web server a key for the backup server
The script runs as root without anyone typing a password, so it needs its own SSH key without a passphrase. On the server you want to back up:
sudo ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""
sudo ssh-copy-id -i /root/.ssh/backup_key.pub [email protected]
ssh-copy-id asks for the serverbackup user's password once. Then test the login, and answer yes to the fingerprint question so cron doesn't get stuck on it later:
sudo ssh -i /root/.ssh/backup_key [email protected] 'echo ok'
It should print ok without asking for a password.
3. Write the backup script
sudo nano /usr/local/bin/server-backup.sh
Paste this and adjust the first lines:
#!/bin/bash
set -euo pipefail
umask 077
REMOTE="[email protected]"
REMOTE_DIR="/backups/web1"
KEY="/root/.ssh/backup_key"
DUMP_DIR="/var/backups/mysql"
TODAY=$(date +%F)
# 1. Dump all databases into one file per day, keep 7 days
mkdir -p "$DUMP_DIR"
mysqldump --single-transaction --routines --events --all-databases | gzip > "$DUMP_DIR/all-$TODAY.sql.gz"
find "$DUMP_DIR" -name 'all-*.sql.gz' -mtime +7 -delete
# 2. Copy websites, settings and dumps to the backup server.
# Files that were changed or deleted since the last run are kept in changed-DATE.
rsync -a --delete -e "ssh -i $KEY" \
--backup --backup-dir="$REMOTE_DIR/changed-$TODAY" \
/var/www /etc "$DUMP_DIR" \
"$REMOTE:$REMOTE_DIR/current/"
echo "$(date) backup finished"
Save with Ctrl+O, Enter, close with Ctrl+X, and make the script executable for root only:
sudo chmod 700 /usr/local/bin/server-backup.sh
What the script does:
mysqldump --single-transactionsaves the databases while the website keeps running. As root on Ubuntu it needs no password. With MariaDB, writemariadb-dumpinstead ofmysqldump. No database on the server? Delete the three lines under "1.".rsync -acopies folders with their permissions and dates, and only sends what changed since the last run, so later runs are quick.--deletealso removes files on the backup server that you deleted on the web server, socurrentis an exact copy.--backup-dirkeeps those deleted and overwritten files in a folder per day, so a mistake doesn't wipe your backup.umask 077makes the dump files readable by root only. The backup contains passwords and customer data: protect the backup server as well as the web server.- The backup user isn't root, so the backup server doesn't keep the original file owners. That's harmless: when you restore as root, set the owners again if needed, for example
sudo chown -R www-data:www-datafor upload folders. /var/www /etcare examples: add every folder you need, such as/homeor the folder of your app.
4. Run it once by hand
Try the copy first without changing anything on the backup server (-n is a dry run, -v lists the files):
sudo rsync -anv --delete -e "ssh -i /root/.ssh/backup_key" /var/www /etc [email protected]:/backups/web1/current/
If the list looks right, run the real backup:
sudo /usr/local/bin/server-backup.sh
The first run copies everything and takes the longest. It ends with backup finished. On the backup server, check ls /backups/web1/current: you should see www, etc and mysql.
5. Run it every night with cron
Open root's cron table:
sudo crontab -e
Add this line at the end, for a backup at 2:30 every night (server time), with a log file:
30 2 * * * /usr/local/bin/server-backup.sh >> /var/log/server-backup.log 2>&1
The five fields are minute, hour, day of month, month and day of week. Check the entry with sudo crontab -l. The next morning, look at the end of the log:
sudo tail -n 20 /var/log/server-backup.log
The changed-DATE folders on the backup server grow over time. Clean up old ones there, for example older than 30 days, with a cron job on the backup server:
find /backups/web1 -maxdepth 1 -name 'changed-*' -mtime +30 -exec rm -rf {} +
Restore from the backup
Files, for example one website (the slashes at the end matter: they copy the folder's contents into the target folder):
sudo rsync -a -e "ssh -i /root/.ssh/backup_key" [email protected]:/backups/web1/current/www/example.com/ /var/www/example.com/
Databases: copy the dump you want back to the server, then import it. This overwrites the databases in the file with their state on that day:
gunzip < all-2026-11-01.sql.gz | sudo mysql
Test a restore now and then, for example on a test server. A backup you've never restored is only a hope.
Common problems
Permission denied (publickey) when cron runs
The script runs as root, so it uses root's key. Check that you created the key with sudo and that the test login in step 2 works with sudo.
Host key verification failed
Root has never connected to the backup server, or the backup server was reinstalled. Run the test login from step 2 again and confirm the fingerprint.
The cron job doesn't run
Check that the line is in sudo crontab -l and that the script is executable. Cron's own messages are in the system journal: journalctl -u cron --since today. Use full paths in scripts that cron runs.
mysqldump: Access denied
Run the script as root (with sudo or from root's crontab). If your database's root user has a password instead of the system login, put the login details into /root/.my.cnf with permissions 600.
Related articles
- Secure a new Linux server: first 10 steps
- Connect to your server with SSH
- Zip and unzip folders over SSH
- Install a LEMP stack (Nginx, MySQL, PHP) on Ubuntu
Still stuck? Open a support ticket and we'll help.