Server
Difficulty: Beginner
8 min read

rsync: File Synchronization and Backup Strategies

Master rsync to synchronize your files and set up robust incremental backup strategies with automatic rotation.

Back to tutorials
About rsync
rsync is a fast and versatile file synchronization tool for Linux. It uses a delta transfer algorithm that only transmits the differences between source and destination, which makes it extremely efficient for regular backups and the synchronization of large data sets.

Prerequisites

  • Operating system: Linux distribution (Debian, Ubuntu, CentOS, RHEL, Fedora, Arch)
  • Privileges: root or sudo access for installation and system backups
  • Network: SSH access configured for remote synchronizations
  • Disk space: Sufficient space on the destination to store the backups
  • Knowledge: Basic Linux command line skills and file permissions

Installation

rsync is generally preinstalled on most Linux distributions. Check whether it is present and install it if necessary.

Checking the installation

rsync --version

Installation on Debian / Ubuntu

sudo apt update
sudo apt install -y rsync

Installation on CentOS / RHEL / Fedora

# CentOS / RHEL
sudo yum install -y rsync

# Fedora
sudo dnf install -y rsync

Installation on Arch Linux

sudo pacman -S rsync
Verification
After installation, confirm with rsync --version. You should see the installed version and the list of compiled-in capabilities.

Basic syntax

The general syntax of rsync is as follows:

rsync [OPTIONS] SOURCE DESTINATION

Main options

Here are the options most commonly used on a daily basis:

# -a (archive): preserves permissions, owner, group, timestamps, symbolic links
# Equivalent to -rlptgoD
rsync -a /source/ /destination/

# -v (verbose): displays the transferred files
rsync -av /source/ /destination/

# -z (compress): compresses the data during transfer
# Useful for network transfers, pointless locally
rsync -avz /source/ server:/destination/

# -P (progress + partial): displays progress and allows resuming
rsync -avP /source/ /destination/

# --delete: deletes files on the destination that are absent from the source
# Creates an exact mirror of the source
rsync -av --delete /source/ /destination/

# --exclude: excludes files or directories by pattern
rsync -av --exclude='*.log' --exclude='.cache/' /source/ /destination/

# --dry-run (-n): simulates the execution without modifying anything
rsync -avn --delete /source/ /destination/
Mind the trailing slash
The trailing slash / at the end of the source path matters. /source/ copies the contents of the directory. /source (without the slash) copies the directory itself into the destination. This distinction is a frequent source of errors.

Local synchronization

Simple directory copy

Copy a directory while preserving all attributes:

# Copy the contents of /home/user/documents to /backup/documents
rsync -avP /home/user/documents/ /backup/documents/

Creating an exact mirror

The --delete option creates a mirror copy by removing from the destination the files that no longer exist in the source:

# Exact mirror: the destination will be identical to the source
rsync -av --delete /home/user/projects/ /backup/projects/

# Always test with --dry-run before using --delete
rsync -avn --delete /home/user/projects/ /backup/projects/

Excluding files and directories

Use exclusion patterns to filter out unnecessary files:

# Exclude by pattern
rsync -av \
    --exclude='*.log' \
    --exclude='*.tmp' \
    --exclude='.cache/' \
    --exclude='node_modules/' \
    --exclude='__pycache__/' \
    /home/user/projects/ /backup/projects/

# Use an exclusion file for complex rules
rsync -av --exclude-from='/etc/rsync-excludes.txt' /source/ /destination/

Example exclusion file:

# /etc/rsync-excludes.txt
# Temporary files
*.tmp
*.swp
*.bak
*~

# Caches and logs
.cache/
*.log
/var/log/

# Development
node_modules/
__pycache__/
.git/
vendor/

# System
/proc/
/sys/
/dev/
/tmp/
/run/

Remote synchronization

Transfer over SSH

rsync uses SSH by default for remote transfers:

# Push files to a remote server
rsync -avz /home/user/documents/ user@server:/backup/documents/

# Pull files from a remote server
rsync -avz user@server:/backup/documents/ /home/user/documents/

# Synchronization with detailed progress
rsync -avzP /home/user/documents/ user@server:/backup/documents/

Custom SSH options

Use -e to specify SSH options:

# Custom SSH port
rsync -avz -e 'ssh -p 2222' /source/ user@server:/destination/

# Specific SSH key
rsync -avz -e 'ssh -i /home/user/.ssh/id_backup' /source/ user@server:/destination/

# Combining port + key + SSH cipher
rsync -avz -e 'ssh -p 2222 -i /home/user/.ssh/id_backup -c aes128-ctr' \
    /source/ user@server:/destination/

Bandwidth limiting

To avoid saturating the network connection:

# Limit to 5 MB/s (value in KB/s)
rsync -avz --bwlimit=5000 /source/ user@server:/destination/

Incremental backup strategy

The principle of --link-dest

The --link-dest option is the key to efficient incremental backups with rsync. It compares each file with a reference backup: if the file has not changed, a hard link is created instead of copying the file.

# Backup structure
# /backup/
# ├── 2026-02-10/    (full backup)
# ├── 2026-02-11/    (incremental, hard links to 2026-02-10)
# └── 2026-02-12/    (incremental, hard links to 2026-02-11)

# Incremental backup with --link-dest
rsync -av --delete \
    --link-dest=/backup/2026-02-11 \
    /source/ \
    /backup/2026-02-12/
Benefit of hard links
Each backup appears as a full copy (you can browse and restore any day independently), but only the files that were actually modified take up additional disk space. A 100 GB backup with 2% daily change only consumes ~2 GB extra per day.

Manual daily rotation

# Variables
BACKUP_DIR="/backup/server"
DATE=$(date +%Y-%m-%d)
LATEST=$(ls -1d ${BACKUP_DIR}/20* 2>/dev/null | tail -1)

# Create the destination directory
mkdir -p "${BACKUP_DIR}/${DATE}"

# Incremental backup
if [ -n "$LATEST" ]; then
    rsync -av --delete --link-dest="${LATEST}" /source/ "${BACKUP_DIR}/${DATE}/"
else
    rsync -av --delete /source/ "${BACKUP_DIR}/${DATE}/"
fi

# Create a symbolic link to the latest backup
ln -snf "${BACKUP_DIR}/${DATE}" "${BACKUP_DIR}/latest"

Automatic backup script

Here is a complete backup script with GFS (Grandfather-Father-Son) rotation: 7 daily backups, 4 weekly and 12 monthly.

#!/bin/bash
# =============================================================================
# rsync-backup.sh - Incremental backup with GFS rotation
# Usage: ./rsync-backup.sh [source] [destination]
# =============================================================================

set -euo pipefail

# --- Configuration ---
SOURCE="${1:-/home}"
BACKUP_BASE="${2:-/backup}"
RETENTION_DAILY=7
RETENTION_WEEKLY=4
RETENTION_MONTHLY=12

LOG_DIR="/var/log/rsync-backup"
LOG_FILE="${LOG_DIR}/backup-$(date +%Y%m%d-%H%M%S).log"
LOCK_FILE="/tmp/rsync-backup.lock"

EXCLUDE_FILE="/etc/rsync-backup-excludes.txt"

# --- Utility functions ---
log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

cleanup() {
    rm -f "$LOCK_FILE"
    log "Lock removed."
}

# --- Preliminary checks ---
mkdir -p "$LOG_DIR"

# Make sure another instance is not running
if [ -f "$LOCK_FILE" ]; then
    LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null)
    if kill -0 "$LOCK_PID" 2>/dev/null; then
        log "ERROR: Backup already in progress (PID: $LOCK_PID). Aborting."
        exit 1
    else
        log "WARNING: Orphan lock detected. Cleaning up."
        rm -f "$LOCK_FILE"
    fi
fi

echo $$ > "$LOCK_FILE"
trap cleanup EXIT

log "=== Starting the backup ==="
log "Source: $SOURCE"
log "Destination: $BACKUP_BASE"

# --- Rotation directories ---
DAILY_DIR="${BACKUP_BASE}/daily"
WEEKLY_DIR="${BACKUP_BASE}/weekly"
MONTHLY_DIR="${BACKUP_BASE}/monthly"
mkdir -p "$DAILY_DIR" "$WEEKLY_DIR" "$MONTHLY_DIR"

# --- Daily incremental backup ---
DATE=$(date +%Y-%m-%d)
TODAY_DIR="${DAILY_DIR}/${DATE}"
LATEST=$(ls -1d ${DAILY_DIR}/20* 2>/dev/null | tail -1 || true)

LINKDEST_OPT=""
if [ -n "$LATEST" ] && [ -d "$LATEST" ]; then
    LINKDEST_OPT="--link-dest=${LATEST}"
    log "Incremental reference: $LATEST"
else
    log "No previous backup. Full backup."
fi

EXCLUDE_OPT=""
if [ -f "$EXCLUDE_FILE" ]; then
    EXCLUDE_OPT="--exclude-from=${EXCLUDE_FILE}"
fi

log "Launching rsync..."
rsync -a --delete \
    ${LINKDEST_OPT} \
    ${EXCLUDE_OPT} \
    --stats \
    "$SOURCE/" \
    "$TODAY_DIR/" \
    >> "$LOG_FILE" 2>&1

RSYNC_EXIT=$?

if [ $RSYNC_EXIT -eq 0 ]; then
    log "Daily backup successful: $TODAY_DIR"
elif [ $RSYNC_EXIT -eq 24 ]; then
    log "WARNING: Some files vanished during the transfer (code 24)."
else
    log "ERROR: rsync failed with code $RSYNC_EXIT"
    exit $RSYNC_EXIT
fi

# Update the latest symbolic link
ln -snf "$TODAY_DIR" "${BACKUP_BASE}/latest"

# --- Weekly rotation (Sunday) ---
DOW=$(date +%u)
if [ "$DOW" -eq 7 ]; then
    WEEK_LABEL=$(date +%Y-W%V)
    if [ ! -d "${WEEKLY_DIR}/${WEEK_LABEL}" ]; then
        cp -al "$TODAY_DIR" "${WEEKLY_DIR}/${WEEK_LABEL}"
        log "Weekly backup created: ${WEEK_LABEL}"
    fi
fi

# --- Monthly rotation (1st of the month) ---
DOM=$(date +%d)
if [ "$DOM" -eq "01" ]; then
    MONTH_LABEL=$(date +%Y-%m)
    if [ ! -d "${MONTHLY_DIR}/${MONTH_LABEL}" ]; then
        cp -al "$TODAY_DIR" "${MONTHLY_DIR}/${MONTH_LABEL}"
        log "Monthly backup created: ${MONTH_LABEL}"
    fi
fi

# --- Purge old backups ---
log "Purging expired backups..."

# Daily: keep the N most recent
DAILY_COUNT=$(ls -1d ${DAILY_DIR}/20* 2>/dev/null | wc -l)
if [ "$DAILY_COUNT" -gt "$RETENTION_DAILY" ]; then
    ls -1d ${DAILY_DIR}/20* | head -n -${RETENTION_DAILY} | while read OLD_DIR; do
        log "Deleting daily: $(basename $OLD_DIR)"
        rm -rf "$OLD_DIR"
    done
fi

# Weekly: keep the N most recent
WEEKLY_COUNT=$(ls -1d ${WEEKLY_DIR}/20* 2>/dev/null | wc -l || echo 0)
if [ "$WEEKLY_COUNT" -gt "$RETENTION_WEEKLY" ]; then
    ls -1d ${WEEKLY_DIR}/20* | head -n -${RETENTION_WEEKLY} | while read OLD_DIR; do
        log "Deleting weekly: $(basename $OLD_DIR)"
        rm -rf "$OLD_DIR"
    done
fi

# Monthly: keep the N most recent
MONTHLY_COUNT=$(ls -1d ${MONTHLY_DIR}/20* 2>/dev/null | wc -l || echo 0)
if [ "$MONTHLY_COUNT" -gt "$RETENTION_MONTHLY" ]; then
    ls -1d ${MONTHLY_DIR}/20* | head -n -${RETENTION_MONTHLY} | while read OLD_DIR; do
        log "Deleting monthly: $(basename $OLD_DIR)"
        rm -rf "$OLD_DIR"
    done
fi

# --- Final report ---
TOTAL_SIZE=$(du -sh "$BACKUP_BASE" 2>/dev/null | cut -f1)
log "Total space used: $TOTAL_SIZE"
log "=== Backup completed successfully ==="

Make the script executable and create the exclusion file:

# Make it executable
chmod +x /usr/local/bin/rsync-backup.sh

# Create the exclusion file
cat > /etc/rsync-backup-excludes.txt << 'EXCL'
# Temporary files and caches
*.tmp
*.swp
*~
.cache/
.thumbnails/

# Large logs
*.log

# Development
node_modules/
__pycache__/
.git/objects/

# System (if backing up root)
/proc/
/sys/
/dev/
/tmp/
/run/
/mnt/
/media/
EXCL

Backup with cron

Configuring the cron job

Automate the execution of the backup script with cron:

# Edit root's crontab
sudo crontab -e

# Daily backup at 2:00 AM
0 2 * * * /usr/local/bin/rsync-backup.sh /home /backup/home >> /var/log/rsync-backup/cron.log 2>&1

# Backup of /etc every 6 hours
0 */6 * * * /usr/local/bin/rsync-backup.sh /etc /backup/etc >> /var/log/rsync-backup/cron-etc.log 2>&1

# Backup to a remote server on Sunday at 3:00 AM
0 3 * * 0 rsync -avz --delete -e 'ssh -i /root/.ssh/id_backup' /backup/ backup@nas:/volume1/backups/server/ >> /var/log/rsync-backup/cron-remote.log 2>&1

Log rotation

Configure logrotate to prevent log files from filling up the disk:

# /etc/logrotate.d/rsync-backup
/var/log/rsync-backup/*.log {
    weekly
    rotate 8
    compress
    delaycompress
    missingok
    notifempty
    create 640 root root
}
Tip
Use systemd timers as an alternative to cron for better control: dependency management, integrated logging with journalctl, and deferred execution if the machine is powered off at the scheduled time.

rsync daemon

Daemon mode serves files over the native rsync protocol (port 873), without SSH. It is useful for transfers within a trusted network.

Configuring rsyncd.conf

# /etc/rsyncd.conf

# Global configuration
uid = nobody
gid = nogroup
use chroot = yes
max connections = 4
log file = /var/log/rsyncd.log
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsyncd.lock
timeout = 300

# Backup module
[backup]
    path = /backup/shared
    comment = Shared backup directory
    read only = no
    list = yes
    auth users = backup_user
    secrets file = /etc/rsyncd.secrets
    hosts allow = 192.168.1.0/24
    hosts deny = *

# Read-only public module
[public]
    path = /srv/public
    comment = Public files
    read only = yes
    list = yes

Configuring authentication

# Create the secrets file (format user:password)
echo 'backup_user:SecurePassword123' | sudo tee /etc/rsyncd.secrets
sudo chmod 600 /etc/rsyncd.secrets

# Start the rsync daemon
sudo systemctl enable rsync
sudo systemctl start rsync

# Check the status
sudo systemctl status rsync

Using the daemon

# List the available modules
rsync rsync://server/

# Synchronize with the backup module
rsync -avz /source/ rsync://backup_user@server/backup/folder/

# With the password in a file (for automation)
echo 'SecurePassword123' > ~/.rsync-password
chmod 600 ~/.rsync-password
rsync -avz --password-file=~/.rsync-password /source/ rsync://backup_user@server/backup/
Daemon security
The native rsync protocol does not encrypt data. Use it only on a trusted local network or through an SSH/VPN tunnel. For transfers over the Internet, always prefer rsync over SSH.

Restoration

Restoration is the ultimate test of any backup strategy. With rsync, the procedure is simple because backups are direct copies of the file system.

Full restoration

# Identify the backup to restore
ls -la /backup/daily/
ls -la /backup/weekly/
ls -la /backup/monthly/

# Full restoration from the latest backup
rsync -avP --delete /backup/latest/ /home/

# Restoration from a specific date
rsync -avP /backup/daily/2026-02-10/ /home/

Selective restoration

# Restore a single file
rsync -avP /backup/latest/user/documents/report.pdf /home/user/documents/

# Restore a specific directory
rsync -avP /backup/latest/user/projects/webapp/ /home/user/projects/webapp/

# Restore with a prior dry-run
rsync -avPn /backup/daily/2026-02-10/user/ /home/user/
# If the result is correct, run again without -n

Restoration from a remote server

# Full restoration from the NAS
rsync -avzP backup@nas:/volume1/backups/server/latest/ /home/

# Restoration of a single file from the remote host
rsync -avzP backup@nas:/volume1/backups/server/latest/etc/nginx/nginx.conf /etc/nginx/nginx.conf
Precaution before restoration
Always use --dry-run before a restoration with --delete. Check that you are restoring the right backup and to the right directory. A path error can overwrite current data.

Backup monitoring

Verification script

#!/bin/bash
# check-backup.sh - Checking the status of backups

BACKUP_BASE="/backup"
MAX_AGE_HOURS=26
ALERT_EMAIL="[email protected]"
ERRORS=0

check_backup() {
    local NAME="$1"
    local DIR="$2"
    local MAX_H="$3"

    if [ ! -d "$DIR" ]; then
        echo "CRITICAL: Directory $DIR not found for $NAME"
        ERRORS=$((ERRORS + 1))
        return
    fi

    LATEST=$(ls -1d ${DIR}/20* 2>/dev/null | tail -1)
    if [ -z "$LATEST" ]; then
        echo "CRITICAL: No backup found for $NAME"
        ERRORS=$((ERRORS + 1))
        return
    fi

    # Check the age of the latest backup
    LAST_MOD=$(stat -c %Y "$LATEST" 2>/dev/null)
    NOW=$(date +%s)
    AGE_H=$(( (NOW - LAST_MOD) / 3600 ))

    if [ "$AGE_H" -gt "$MAX_H" ]; then
        echo "ALERT: $NAME - last backup ${AGE_H}h ago (max: ${MAX_H}h)"
        ERRORS=$((ERRORS + 1))
    else
        SIZE=$(du -sh "$LATEST" | cut -f1)
        FILES=$(find "$LATEST" -type f | wc -l)
        echo "OK: $NAME - $(basename $LATEST) - ${SIZE} - ${FILES} files - ${AGE_H}h"
    fi
}

echo "=== Backup report $(date) ==="
check_backup "Daily" "${BACKUP_BASE}/daily" "$MAX_AGE_HOURS"
check_backup "Weekly" "${BACKUP_BASE}/weekly" 170
check_backup "Monthly" "${BACKUP_BASE}/monthly" 744

# Disk space
DISK_USAGE=$(df -h "$BACKUP_BASE" | tail -1 | awk '{print $5}')
DISK_PCT=${DISK_USAGE%\%}
if [ "$DISK_PCT" -gt 85 ]; then
    echo "ALERT: Backup disk space at ${DISK_USAGE}"
    ERRORS=$((ERRORS + 1))
else
    echo "OK: Backup disk space at ${DISK_USAGE}"
fi

# Send an alert if there are errors
if [ "$ERRORS" -gt 0 ]; then
    echo "=== ${ERRORS} problem(s) detected ==="
    # Uncomment to enable email alerts
    # mail -s "[BACKUP] ${ERRORS} alert(s)" "$ALERT_EMAIL" < /tmp/backup-report.txt
    exit 1
fi

echo "=== All backups are OK ==="
exit 0

Automating the monitoring

# Daily check at 8:00 AM
0 8 * * * /usr/local/bin/check-backup.sh >> /var/log/rsync-backup/check.log 2>&1

Hardening

SSH keys dedicated to backups

Create a specific key pair for backup operations:

# Generate a dedicated key
ssh-keygen -t ed25519 -f /root/.ssh/id_backup -N '' -C 'rsync-backup@server'

# Copy the public key to the destination server
ssh-copy-id -i /root/.ssh/id_backup.pub backup@nas

SSH command restriction

Restrict the key to running only rsync on the destination server:

# On the destination server: ~/.ssh/authorized_keys
command="rsync --server --sender -logDtprze.iLsfxCIvu . /volume1/backups/",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3Nza... rsync-backup@server
Tip
To find the exact command rsync sends to the server, run rsync with the -e 'ssh -v' option and look for the line debug1: Sending command in the output.

Chroot to isolate access

# /etc/ssh/sshd_config - Restrict the backup user
Match User backup
    ChrootDirectory /volume1/backups
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no

Firewall and network

# Allow rsync daemon only from the local network
sudo ufw allow from 192.168.1.0/24 to any port 873 proto tcp comment 'rsync daemon LAN'

# Allow SSH only from the backup server
sudo ufw allow from 192.168.1.50 to any port 22 proto tcp comment 'SSH backup server'

Troubleshooting

Common errors

# Error: "rsync: failed to set times on ... Operation not permitted"
# Cause: the destination file system does not support timestamps
# Solution: add --no-times or --modify-window=1 for FAT/NTFS file systems
rsync -av --no-times /source/ /mnt/usb/backup/

# Error: "rsync error: some files/attrs were not transferred (code 23)"
# Cause: permission issues or locked files
# Solution: run as root or add --no-perms
sudo rsync -av /source/ /destination/

# Error: "rsync: connection unexpectedly closed"
# Cause: SSH timeout, full disk space, or OOM killer
# Solutions:
rsync -avz -e 'ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3' /source/ user@server:/dest/

# Error: "rsync error: max connections reached (code 10)"
# Cause: too many simultaneous connections to the daemon
# Solution: increase max connections in rsyncd.conf or wait

Performance tuning

# Disable compression for fast networks (Gigabit LAN)
rsync -av --no-compress /source/ user@server:/destination/

# Use a faster checksum algorithm (rsync 3.2+)
rsync -av --checksum-choice=xxh3 /source/ /destination/

# Parallel transfer with xargs (large files)
find /source/ -maxdepth 1 -type d | xargs -P 4 -I {} \
    rsync -av {}/ /destination/{}/

# Exclude files above a certain size
rsync -av --max-size='100M' /source/ /destination/

# Inplace mode to avoid the temporary copy (saves space)
rsync -av --inplace /source/ /destination/

Network diagnostics

# Test SSH connectivity
ssh -v user@server 'echo OK'

# Measure rsync throughput
rsync -avP --stats /source/ user@server:/destination/ 2>&1 | tail -20

# Check the differences without transferring
rsync -avni /source/ /destination/ | head -50

Conclusion

rsync is an essential tool for synchronizing and backing up data on Linux. Thanks to its delta transfer algorithm and the --link-dest option, it lets you set up efficient incremental backup strategies that consume little disk space while offering fast restoration to any point in time.

  • Use rsync -av for simple local copies
  • Add -z and -P for network transfers
  • Leverage --link-dest for space-efficient incremental backups
  • Automate with cron and a GFS rotation script
  • Secure with dedicated SSH keys and command restrictions
  • Monitor your backups and test restoration regularly
Golden rule
A backup that has never been tested by restoring it is not a backup. Schedule regular restoration tests to validate the integrity of your data.

Written by

Morgann Riu

Cybersecurity and Linux administration expert. I share my knowledge through free tutorials and training to help system administrators and developers secure their infrastructures.

Share this tutorial

Did you enjoy this article?

Comments

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.