Skip to main content

Professional Rsync Backup Strategy

A well-designed backup strategy is the difference between "we lost everything" and "we were back online in 20 minutes." This guide presents a production-tested approach to building a layered rsync backup system for any Linux server.

info

This strategy is application-centric — it focuses on backing up your apps, databases, and configs rather than full OS imaging. It works for any server stack: PHP/MySQL, Python/PostgreSQL, Node.js, static sites, Docker containers, or any combination.

Strategy Goals

GoalDescription
Protect essential assetsBack up application files, databases, and configs first
Speed up recoveryKeep local restore points for fast rollback (minutes, not hours)
Add resilienceMaintain encrypted offsite copies for disaster recovery
Keep scope simpleFocus on application data, not full OS imaging
Automate everythingRemove human error from the backup process
Verify regularlyTest restores — a backup you can't restore is worthless

The 3-2-1 Backup Rule

The industry-standard approach that this strategy implements:

3 copies of your data
2 different storage types (local disk + remote/cloud)
1 copy offsite (different server, region, or provider)
CopyLocationPurposeRecovery Speed
Production/var/www/, /var/lib/mysql/Live dataN/A
Local backup/backup/ on same VPS or attached storageFast rollbackMinutes
Offsite backupRemote server, S3, B2, WasabiDisaster recoveryMinutes to hours

Core Backup Layers

Layer 1: Application Files

Your web applications, static files, and user-uploaded content.

ContentExample PathSync Frequency
Web application code/var/www/html/, /var/www/app/Daily
User uploads / media/var/www/html/uploads/Hourly or daily
Configuration files/etc/nginx/, /etc/php/On change
Docker data/opt/docker-data/Daily
# Daily application backup
rsync -avz --delete \
--exclude 'cache/' \
--exclude '*.log' \
--exclude 'node_modules/' \
/var/www/html/ /backup/daily/www/

Layer 2: Database Backups

Export databases first, then rsync the dumps. Never rsync live database files directly.

# MySQL / MariaDB
mysqldump -u root -p --all-databases | gzip > /var/backups/db/all_databases_$(date +%F).sql.gz

# PostgreSQL
pg_dumpall -U postgres | gzip > /var/backups/db/all_databases_$(date +%F).sql.gz

# Sync database dumps to backup location
rsync -avz /var/backups/db/ /backup/daily/db/
warning

Never rsync live database data directories (/var/lib/mysql/, /var/lib/postgresql/). The files change constantly during operation and you'll get a corrupt backup. Always use mysqldump or pg_dump first, then rsync the exported files.

Layer 3: Offsite Replication

Push local backups to a remote server or cloud storage for disaster recovery:

# Rsync to a remote backup server
rsync -avz --delete /backup/daily/ user@backup-server:/offsite/daily/

# Alternative: Use rclone for cloud storage (S3, B2, Wasabi)
rclone sync /backup/daily/ remote:my-bucket/daily/

Retention Policy

Not every backup needs to be kept forever. A Grandfather-Father-Son (GFS) retention policy gives you multiple restore points while managing storage:

TierKeepCountUse Case
HourlyLast 24 hours24 copiesQuick rollback for frequent changes
DailyLast 7 days7 copiesStandard restore points
WeeklyLast 4 weeks4 copiesHistorical recovery
MonthlyLast 3–12 months3–12 copiesCompliance, auditing
OffsiteLatest daily + weekly2+ copiesDisaster recovery

Implementing Retention with Rsync

#!/bin/bash
# retention-rotate.sh — Rotate daily backups into weekly/monthly

BACKUP_BASE="/backup"
TODAY=$(date +%F)
DAY_OF_WEEK=$(date +%u) # 1=Monday, 7=Sunday
DAY_OF_MONTH=$(date +%d)

# Daily backup (always runs)
rsync -avz --delete /var/www/html/ "$BACKUP_BASE/daily/$TODAY/"

# Weekly snapshot (every Sunday)
if [ "$DAY_OF_WEEK" -eq 7 ]; then
cp -al "$BACKUP_BASE/daily/$TODAY/" "$BACKUP_BASE/weekly/week-$(date +%V)/"
fi

# Monthly snapshot (1st of each month)
if [ "$DAY_OF_MONTH" -eq "01" ]; then
cp -al "$BACKUP_BASE/daily/$TODAY/" "$BACKUP_BASE/monthly/$(date +%Y-%m)/"
fi

# Clean up old dailies (keep last 7 days)
find "$BACKUP_BASE/daily/" -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;

# Clean up old weeklies (keep last 4 weeks)
find "$BACKUP_BASE/weekly/" -maxdepth 1 -type d -mtime +28 -exec rm -rf {} \;
tip

The cp -al command creates hard-linked copies — they share disk blocks with the original, so weekly/monthly snapshots use almost zero extra disk space until files change.

Reference Architecture

flowchart TD
APP["Application Files<br/>/var/www/html/"] --> LFB["Local File Backup<br/>/backup/daily/www/"]
DB["Database Export<br/>mysqldump / pg_dump"] --> LDB["Local DB Backup<br/>/backup/daily/db/"]
CFG["Config Files<br/>/etc/nginx/, .env"] --> LCFG["Local Config Backup<br/>/backup/daily/config/"]

LFB --> RET["Retention Rotation<br/>Daily → Weekly → Monthly"]
LDB --> RET
LCFG --> RET

RET --> OFF["Offsite Replication<br/>rsync or rclone to remote server/cloud"]
OFF --> VER["Integrity Verification<br/>Checksum comparison"]
VER --> TEST["Restore Test<br/>Periodic restore to staging"]

Professional Patterns

PatternDescriptionBest ForTrade-off
Rsync incremental + DB dumpSync changed files, export databases regularlyMost VPS setupsNeeds local disk planning
Hard-linked snapshotsrsync --link-dest for space-efficient versioningBudget-conscious serversSlightly complex setup
Encrypted repositoryUse restic or borg for compression + encryptionCompliance requirementsMore tooling complexity
Hybrid GFS retentionHourly → daily → weekly tiersHigh-change applicationsMore policy management
Cloud mirrorPush backups to S3/B2/WasabiMaximum durabilitySlower restores

Operational Checklist

AreaExpected Practice
SchedulingAutomated via cron or systemd timer — no manual runs
Database exportsmysqldump / pg_dump runs before file sync
EncryptionOffsite backups encrypted at rest (GPG, rclone crypt)
VerificationChecksum or file count checks after each backup
MonitoringAlerts on backup failure (email, Slack, webhook)
Restore testingRestore to staging at least monthly
DocumentationBackup paths, credentials, and procedures documented

Quick-Start: Minimal Production Backup

For a single VPS running a PHP/MySQL application, here's the minimum viable backup:

#!/bin/bash
# minimal-backup.sh — Run daily via cron

set -e
TIMESTAMP=$(date +%F_%H%M)
BACKUP_DIR="/backup/$TIMESTAMP"
REMOTE="user@backup-server:/offsite"

# 1. Export database
mkdir -p "$BACKUP_DIR/db"
mysqldump -u root -p'yourpassword' --all-databases \
| gzip > "$BACKUP_DIR/db/all_databases.sql.gz"

# 2. Sync application files
rsync -az --delete \
--exclude 'cache/' \
--exclude '*.log' \
/var/www/html/ "$BACKUP_DIR/www/"

# 3. Copy configs
rsync -a /etc/nginx/ "$BACKUP_DIR/config/nginx/"
cp /var/www/html/.env "$BACKUP_DIR/config/env_backup"

# 4. Push to offsite
rsync -avz "$BACKUP_DIR/" "$REMOTE/$TIMESTAMP/"

# 5. Clean local backups older than 7 days
find /backup/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;

echo " Backup completed: $TIMESTAMP"

Add to cron for daily execution:

# Run at 2:00 AM daily
0 2 * * * /usr/local/bin/minimal-backup.sh >> /var/log/backup.log 2>&1

Common Pitfalls

PitfallImpactPrevention
Backing up live database filesCorrupt, unusable backupAlways use mysqldump / pg_dump first
No offsite copySingle point of failureReplicate to remote server or cloud
Never testing restoresDiscover backup is broken only during disasterSchedule monthly restore tests
No monitoring or alertsBackup silently fails for weeksAdd success/failure notifications
Storing passwords in scriptsSecurity riskUse .my.cnf or environment variables
No retention policyDisk fills up or oldest backups lostImplement GFS rotation

What's Next

Continue building your rsync knowledge:

  1. Source and Destination — Master path rules for reliable syncs
  2. Cron Automation — Set up automated backup schedules
  3. Backup Strategies — Deep dive into advanced backup architectures
  4. Disaster Recovery — Plan for worst-case scenarios