Skip to main content

Cron Automation

Cron turns rsync from a manual tool into an automated backup system. By scheduling rsync jobs, you ensure backups run consistently — even when you're not watching.

Cron Basics

Cron Schedule Format

┌─────────── minute (0-59)
│ ┌───────── hour (0-23)
│ │ ┌─────── day of month (1-31)
│ │ │ ┌───── month (1-12)
│ │ │ │ ┌─── day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * * command

Common Schedules

ScheduleExpressionUse Case
Every day at 2 AM0 2 * * *Nightly backups
Every 6 hours0 */6 * * *Frequent sync for active sites
Every Sunday at 3 AM0 3 * * 0Weekly full backup
First of month at 1 AM0 1 1 * *Monthly archive
Every weekday at 11 PM0 23 * * 1-5End-of-business backup

Editing Cron

# Edit your user's crontab
crontab -e

# View current cron jobs
crontab -l

# Edit system-wide crontab (as root)
sudo nano /etc/crontab

Simple Cron + Rsync

Nightly File Backup

# Daily at 2 AM — sync web files to local backup
0 2 * * * rsync -av /var/www/html/ /backup/www/

Nightly Backup to Remote Server

# Daily at 2 AM — sync to offsite backup
0 2 * * * rsync -avz /var/www/html/ user@backup-server:/backups/www/

Hourly Uploads Sync (High-Traffic Sites)

# Every hour — sync new uploads
0 * * * * rsync -av --ignore-existing /var/www/html/uploads/ user@backup:/backups/uploads/
Cron Date Formatting

In crontab, % has special meaning (newline). You must escape it as \%:

# Correct — escaped percent signs
0 2 * * * rsync -av /var/www/ /backup/www-$(date +\%F)/

# WRONG — will fail silently
0 2 * * * rsync -av /var/www/ /backup/www-$(date +%F)/

Production Backup Script

For anything beyond simple one-liners, use a wrapper script:

/usr/local/bin/daily-backup.sh
#!/bin/bash
# daily-backup.sh — Full server backup with logging and alerts
set -euo pipefail

# Configuration
SOURCE="/var/www/html/"
BACKUP_BASE="/backup"
REMOTE="user@backup-server:/offsite"
TIMESTAMP=$(date +%F_%H%M)
LOG="/var/log/rsync/backup_$TIMESTAMP.log"

mkdir -p /var/log/rsync "$BACKUP_BASE/$TIMESTAMP"

echo "=== Backup started: $TIMESTAMP ===" >> "$LOG"

# Step 1: Database export
if command -v mysqldump &> /dev/null; then
mysqldump --single-transaction --routines --triggers \
--all-databases | gzip > "$BACKUP_BASE/$TIMESTAMP/databases.sql.gz"
echo " Database export complete" >> "$LOG"
fi

# Step 2: Incremental file backup with link-dest
rsync -av --stats \
--link-dest="$BACKUP_BASE/latest" \
--exclude='cache/' \
--exclude='*.log' \
--exclude='*.tmp' \
"$SOURCE" "$BACKUP_BASE/$TIMESTAMP/files/" \
>> "$LOG" 2>&1

# Step 3: Update latest symlink
ln -sfn "$BACKUP_BASE/$TIMESTAMP" "$BACKUP_BASE/latest"
echo " File backup complete" >> "$LOG"

# Step 4: Offsite transfer
rsync -avz --stats \
"$BACKUP_BASE/$TIMESTAMP/" "$REMOTE/$TIMESTAMP/" \
>> "$LOG" 2>&1
echo " Offsite transfer complete" >> "$LOG"

# Step 5: Cleanup old backups (keep 14 days)
find "$BACKUP_BASE" -maxdepth 1 -type d -name "20*" -mtime +14 -exec rm -rf {} \;
find /var/log/rsync/ -name "*.log" -mtime +30 -delete
echo " Cleanup complete" >> "$LOG"

echo "=== Backup finished: $(date +%F_%H%M) ===" >> "$LOG"

Make it executable and schedule it:

sudo chmod +x /usr/local/bin/daily-backup.sh

# Add to crontab
crontab -e
# Add this line:
0 2 * * * /usr/local/bin/daily-backup.sh

Error Handling and Alerts

Email on Failure

0 2 * * * /usr/local/bin/daily-backup.sh 2>&1 || \
echo "Backup failed on $(hostname)" | mail -s " Backup Failed" admin@example.com

Script with Error Handling

/usr/local/bin/backup-with-alerts.sh
#!/bin/bash
set -euo pipefail

LOG="/var/log/rsync/backup_$(date +%F).log"

if rsync -avz --stats \
/var/www/html/ user@backup:/backups/www/ \
>> "$LOG" 2>&1; then
echo "[$(date)] Backup SUCCESS" >> "$LOG"
else
EXIT_CODE=$?
echo "[$(date)] Backup FAILED (exit: $EXIT_CODE)" >> "$LOG"
echo "Rsync backup failed. Exit code: $EXIT_CODE. See $LOG" | \
mail -s " Backup Failed - $(hostname)" admin@example.com
exit $EXIT_CODE
fi

Systemd Timers (Alternative to Cron)

Systemd timers provide better logging, dependency management, and failure tracking:

/etc/systemd/system/rsync-backup.service
[Unit]
Description=Daily rsync backup
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/daily-backup.sh
User=root
StandardOutput=journal
StandardError=journal
/etc/systemd/system/rsync-backup.timer
[Unit]
Description=Run rsync backup daily at 2 AM

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
# Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable rsync-backup.timer
sudo systemctl start rsync-backup.timer

# Check timer status
systemctl list-timers | grep rsync

# View logs
journalctl -u rsync-backup.service --since today
Systemd vs Cron
FeatureCronSystemd Timer
Setup complexityLow — one-line entryMedium — two files
LoggingManual (--log-file)Built-in (journalctl)
Failure trackingNone built-inTracks failed runs
Missed runsSkippedPersistent=true catches up
DependenciesNoneCan wait for network, etc.

Monitoring Cron Jobs

Verify Cron Is Running

# Check cron service status
systemctl status cron # Debian/Ubuntu
systemctl status crond # CentOS/RHEL

# View recent cron activity
grep CRON /var/log/syslog | tail -20 # Debian/Ubuntu
grep CRON /var/log/cron | tail -20 # CentOS/RHEL

Verify Backups Are Fresh

# Check latest backup timestamp
ls -la /backup/latest

# Check backup size is reasonable
du -sh /backup/latest/

# Alert if no backup in 48 hours
find /backup/ -maxdepth 1 -name "latest" -mmin +2880 \
-exec echo " No backup in 48 hours!" \;

Common Pitfalls

PitfallConsequencePrevention
Unescaped % in crontabCommand silently failsUse \% in crontab date formatting
No PATH in cron environmentrsync: command not foundUse full path: /usr/bin/rsync
No SSH key for cron userRemote rsync prompts for password (hangs)Set up key-based auth for cron user
No error handlingFailures go unnoticedCheck exit codes, send alerts
No log rotation/var/log/rsync/ fills diskUse logrotate or find -mtime -delete
Overlapping runsTwo backup jobs fight over filesUse flock to prevent overlap

Preventing Overlapping Runs

# Use flock to ensure only one backup runs at a time
0 2 * * * flock -n /tmp/backup.lock /usr/local/bin/daily-backup.sh

Quick Reference

# Simple daily backup
0 2 * * * rsync -av /var/www/ /backup/www/

# Daily with logging
0 2 * * * rsync -av --stats --log-file="/var/log/rsync/backup_$(date +\%F).log" /var/www/ /backup/

# Incremental with link-dest
0 2 * * * rsync -av --link-dest=/backup/latest /var/www/ /backup/$(date +\%F)/ && ln -sfn /backup/$(date +\%F) /backup/latest

# With error notification
0 2 * * * /usr/local/bin/daily-backup.sh 2>&1 || mail -s "Backup Failed" admin@example.com

# Prevent overlapping runs
0 2 * * * flock -n /tmp/backup.lock /usr/local/bin/daily-backup.sh

# Check cron status
crontab -l
systemctl status cron

What's Next