Prerequisites
- A Linux system (Ubuntu, Debian, CentOS...)
- Terminal access with
sudorights rsyncinstalled (available by default on most distributions)- A backup destination: local disk, NAS, or remote server over SSH
Check that rsync is available:
rsync --version
If not:
sudo apt install rsync # Debian/Ubuntu
sudo yum install rsync # CentOS/RHEL
What rsync does (and why it is the right tool)
rsync synchronizes files between two locations. Its main advantage: it only transfers what has changed since the last run. No full copy every time, which saves both time and bandwidth.
Basic syntax:
rsync [options] source/ destination/
Common options:
| Option | Description |
|---|---|
-a | Archive mode: preserves permissions, timestamps, symbolic links |
-v | Verbose output, shows files being processed |
-z | Compresses data during transfer |
--delete | Removes files at the destination that no longer exist at the source |
--exclude | Excludes specific files or directories |
-e ssh | Uses SSH for remote transfers |
Local example:
rsync -av /var/www/mysite/ /mnt/backup/mysite/
Remote example over SSH:
rsync -avz -e ssh /var/www/mysite/ user@192.168.1.100:/backups/mysite/
Setting up the backup directory structure
Splitting backups by frequency makes retention and management easier:
sudo mkdir -p /backups/daily
sudo mkdir -p /backups/weekly
sudo mkdir -p /backups/monthly
This structure lets you apply different retention policies per type. For example, keep 7 days of daily backups but 3 months of monthly ones.
The backup script
Create the file:
sudo nano /usr/local/bin/backup.sh
Content:
#!/bin/bash
# ============================================================
# Automated backup script
# Author : Pascal SARR
# ============================================================
# --- VARIABLES ---
SOURCE="/var/www/"
BACKUP_ROOT="/backups"
DATE=$(date +"%Y-%m-%d")
DAY_OF_WEEK=$(date +"%u") # 1=Monday ... 7=Sunday
DAY_OF_MONTH=$(date +"%d") # 01 to 31
LOG_FILE="/var/log/backup.log"
# --- BACKUP TYPE ---
if [ "$DAY_OF_MONTH" = "01" ]; then
BACKUP_TYPE="monthly"
elif [ "$DAY_OF_WEEK" = "7" ]; then
BACKUP_TYPE="weekly"
else
BACKUP_TYPE="daily"
fi
DEST="$BACKUP_ROOT/$BACKUP_TYPE/$DATE"
mkdir -p "$DEST"
echo "[$DATE] Starting $BACKUP_TYPE backup..." >> "$LOG_FILE"
rsync -az --delete \
--exclude="*.log" \
--exclude="node_modules/" \
--exclude=".git/" \
"$SOURCE" "$DEST/" >> "$LOG_FILE" 2>&1
if [ $? -eq 0 ]; then
echo "[$DATE] $BACKUP_TYPE backup OK -> $DEST" >> "$LOG_FILE"
else
echo "[$DATE] $BACKUP_TYPE backup FAILED" >> "$LOG_FILE"
fi
# --- RETENTION ---
find "$BACKUP_ROOT/daily/" -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;
find "$BACKUP_ROOT/weekly/" -maxdepth 1 -type d -mtime +28 -exec rm -rf {} \;
find "$BACKUP_ROOT/monthly/" -maxdepth 1 -type d -mtime +90 -exec rm -rf {} \;
echo "[$DATE] Cleanup done." >> "$LOG_FILE"
Make it executable:
sudo chmod +x /usr/local/bin/backup.sh
Including a database
If the project uses a database, dump it before running rsync. Backing up raw data files is useless, only the SQL dump is actually usable at restore time.
PostgreSQL:
pg_dump -U postgres database_name > /tmp/db_backup.sql
rsync -az /tmp/db_backup.sql "$DEST/"
MySQL / MariaDB:
mysqldump -u root -p'password' database_name > /tmp/db_backup.sql
rsync -az /tmp/db_backup.sql "$DEST/"
For MySQL, avoid putting the password in plain text inside the script. Use a ~/.my.cnf file instead:
[mysqldump]
user=root
password=your_password
Test before automating
Run the script manually first:
sudo /usr/local/bin/backup.sh
Then check:
ls /backups/daily/
cat /var/log/backup.log
Do not move to the next step if the test fails. An untested backup script is worth nothing.
Scheduling with cron
cron runs commands on a schedule. To edit root's cron jobs:
sudo crontab -e
To run the backup every day at 2:00 AM:
0 2 * * * /usr/local/bin/backup.sh
Syntax reference:
┌──────── minute (0-59)
│ ┌────── hour (0-23)
│ │ ┌──── day of month (1-31)
│ │ │ ┌── month (1-12)
│ │ │ │ ┌ day of week (0=Sun, 7=Sun)
│ │ │ │ │
0 2 * * * /usr/local/bin/backup.sh
Some useful examples:
| Cron expression | When it runs |
|---|---|
0 2 * * * | Every day at 02:00 |
0 3 * * 0 | Every Sunday at 03:00 |
0 1 1 * * | The 1st of every month at 01:00 |
*/30 * * * * | Every 30 minutes |
Backup to a remote server
To send backups to a remote server, set up SSH key authentication. The script needs to connect without any user interaction.
Generate the key:
ssh-keygen -t ed25519 -C "backup-key"
# Leave the passphrase empty
Copy the public key to the target server:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@SERVER_IP
Test the connection:
ssh user@SERVER_IP
If it connects without asking for a password, you are good.
Update the script:
DEST="user@SERVER_IP:/backups/$BACKUP_TYPE/$DATE"
rsync -az -e "ssh -i ~/.ssh/id_ed25519" --delete "$SOURCE" "$DEST/"
Email notifications
To receive a report after each run:
sudo apt install mailutils
Add these lines at the end of the script:
STATUS=$(tail -1 "$LOG_FILE")
echo "$STATUS" | mail -s "[BACKUP] Report for $DATE" you@email.com
Restoring a backup
Full directory:
rsync -av /backups/daily/2026-03-27/ /var/www/mysite/
Single file:
cp /backups/daily/2026-03-27/index.html /var/www/mysite/index.html
Database:
# PostgreSQL
psql -U postgres database_name < /backups/daily/2026-03-27/db_backup.sql
# MySQL / MariaDB
mysql -u root -p database_name < /backups/daily/2026-03-27/db_backup.sql
Run a restore test at least once in a non-critical environment before you actually need it in an emergency.
Pre-production checklist
-
rsyncinstalled and working - Script created at
/usr/local/bin/backup.shand executable - Manual test passed without errors
- Logs readable at
/var/log/backup.log - Database included in the backup
- Retention policy active
-
cronjob configured - Passwordless SSH working (if remote backup)
- Email notification configured
- Restore tested at least once
On strategy: The 3-2-1 rule is the standard: 3 copies, on 2 different media, with 1 offsite. It is not overkill, it is what actually holds up when something goes wrong.
