Automate Server Backups with Cron and Rsync on Ubuntu 24.04

Daniel MercerDaniel MercerSenior Systems Engineer
Intermediate
Updated Apr 8, 202614 min read~35 minutes total
Backup
Security
Automation
DevOps
Ubuntu
Linux
Automate Server Backups with Cron and Rsync on Ubuntu 24.04

On This Page

Prerequisites

A Raff VM running Ubuntu 24.04 with at least 1 vCPU and 2 GB RAM (Tier 2 or higher), SSH access configured with SSH keys, a non-root user with sudo privileges, a running application or database to back up (e.g. PostgreSQL or a web directory)

Don't have a server yet? Deploy a Raff VM in 60 seconds.

Deploy a VM

Introduction

A server without automated backups is a server waiting to lose data. Whether it is a corrupted database, an accidental rm -rf, or a failed deployment that overwrites production files, the question is never if you will need a backup — it is when.

This tutorial builds a two-layer backup system on your Raff Ubuntu 24.04 VM. The first layer uses cron, rsync, and tar at the operating system level to back up files and databases on a schedule you control. The second layer uses Raff's built-in snapshot and automated backup features to capture the entire VM state. Together, these layers give you both granular file-level recovery and full-server rollback — the combination we use across our own infrastructure. When we expanded our Ceph storage cluster from 6 to 18 OSDs, we relied on exactly this approach to ensure zero data loss during the migration.

In this tutorial, you will create a backup script that archives your web files and database, schedule it with cron to run automatically every night, configure rsync to copy backups to a remote server, set up retention to prevent disk space from filling up, and verify the entire pipeline with a test restore.

Step 1 — Create the Backup Directory Structure

Before writing any scripts, set up a dedicated directory structure for local backups. Keeping backups organized by date makes rotation and cleanup straightforward.

bashsudo mkdir -p /var/backups/automated/{files,database,logs}
sudo chown -R $USER:$USER /var/backups/automated

The files directory will hold compressed archives of your application files. The database directory stores database dumps. The logs directory captures backup script output for troubleshooting.

Verify the structure:

bashls -la /var/backups/automated/

Expected output:

total 12
drwxr-xr-x 5 deploy deploy 4096 Apr  8 10:00 .
drwxr-xr-x 3 root   root   4096 Apr  8 10:00 ..
drwxr-xr-x 2 deploy deploy 4096 Apr  8 10:00 database
drwxr-xr-x 2 deploy deploy 4096 Apr  8 10:00 files
drwxr-xr-x 2 deploy deploy 4096 Apr  8 10:00 logs

Step 2 — Write the Backup Script

Create a single backup script that handles both file archives and database dumps. A single script is easier to schedule, monitor, and debug than multiple separate cron jobs.

bashnano /opt/backup.sh

Add the following content:

bash#!/bin/bash
# ============================================
# Automated Backup Script for Raff VM
# Backs up web files + database with retention
# ============================================

set -euo pipefail

# --- Configuration ---
BACKUP_DIR="/var/backups/automated"
DATE=$(date +%Y-%m-%d_%H%M)
RETENTION_DAYS=7
LOG_FILE="${BACKUP_DIR}/logs/backup-${DATE}.log"

# What to back up (adjust these paths)
WEB_DIR="/var/www/html"
DB_NAME="myapp_production"
DB_USER="myapp"

# Remote destination (optional — leave empty to skip)
REMOTE_USER="backup"
REMOTE_HOST=""
REMOTE_DIR="/backups/$(hostname)"

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

cleanup_old_backups() {
    log "Cleaning backups older than ${RETENTION_DAYS} days..."
    find "${BACKUP_DIR}/files" -name "*.tar.gz" -mtime +${RETENTION_DAYS} -delete
    find "${BACKUP_DIR}/database" -name "*.sql.gz" -mtime +${RETENTION_DAYS} -delete
    find "${BACKUP_DIR}/logs" -name "*.log" -mtime +30 -delete
}

# --- Start ---
log "========== Backup started =========="

# 1. Back up web files
if [ -d "$WEB_DIR" ]; then
    FILE_ARCHIVE="${BACKUP_DIR}/files/www-${DATE}.tar.gz"
    log "Archiving ${WEB_DIR}..."
    tar -czf "$FILE_ARCHIVE" -C "$(dirname $WEB_DIR)" "$(basename $WEB_DIR)" 2>> "$LOG_FILE"
    FILE_SIZE=$(du -sh "$FILE_ARCHIVE" | cut -f1)
    log "File backup complete: ${FILE_ARCHIVE} (${FILE_SIZE})"
else
    log "WARNING: ${WEB_DIR} does not exist, skipping file backup"
fi

# 2. Back up database (PostgreSQL example)
if command -v pg_dump &> /dev/null && sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw "$DB_NAME"; then
    DB_ARCHIVE="${BACKUP_DIR}/database/db-${DATE}.sql.gz"
    log "Dumping database ${DB_NAME}..."
    sudo -u postgres pg_dump "$DB_NAME" | gzip > "$DB_ARCHIVE" 2>> "$LOG_FILE"
    DB_SIZE=$(du -sh "$DB_ARCHIVE" | cut -f1)
    log "Database backup complete: ${DB_ARCHIVE} (${DB_SIZE})"
else
    log "INFO: PostgreSQL database ${DB_NAME} not found, skipping database backup"
fi

# 3. Sync to remote server (if configured)
if [ -n "$REMOTE_HOST" ]; then
    log "Syncing to ${REMOTE_HOST}:${REMOTE_DIR}..."
    rsync -avz --delete \
        -e "ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10" \
        "${BACKUP_DIR}/files/" \
        "${BACKUP_DIR}/database/" \
        "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/" 2>> "$LOG_FILE"
    log "Remote sync complete"
else
    log "INFO: No remote host configured, skipping remote sync"
fi

# 4. Clean up old backups
cleanup_old_backups

# 5. Summary
log "========== Backup finished =========="
TOTAL_SIZE=$(du -sh "${BACKUP_DIR}" | cut -f1)
log "Total backup storage used: ${TOTAL_SIZE}"

Save and close the file. Make it executable:

bashchmod 755 /opt/backup.sh

Note

This script uses PostgreSQL as the database example. For MariaDB/MySQL, replace the pg_dump block with: mysqldump -u ${DB_USER} -p${DB_PASS} ${DB_NAME} | gzip > "$DB_ARCHIVE". For a MariaDB setup, see our MariaDB installation tutorial.

Step 3 — Configure the Script for Your Environment

Edit the configuration variables at the top of the script to match your server:

bashnano /opt/backup.sh

Adjust these lines:

bashWEB_DIR="/var/www/html"         # Your application directory
DB_NAME="myapp_production"      # Your database name
DB_USER="myapp"                 # Your database user
RETENTION_DAYS=7                # How many days to keep backups

If you are running WordPress with the default Raff setup, use:

bashWEB_DIR="/var/www/wordpress"
DB_NAME="wordpress"
DB_USER="wordpress"

Step 4 — Test the Script Manually

Always run a backup script manually before scheduling it. This catches permission errors, missing directories, and configuration mistakes while you can still see the output in real time.

bash/opt/backup.sh

Expected output:

[2026-04-08 10:15:00] ========== Backup started ==========
[2026-04-08 10:15:00] Archiving /var/www/html...
[2026-04-08 10:15:03] File backup complete: /var/backups/automated/files/www-2026-04-08_1015.tar.gz (45M)
[2026-04-08 10:15:03] Dumping database myapp_production...
[2026-04-08 10:15:05] Database backup complete: /var/backups/automated/database/db-2026-04-08_1015.sql.gz (12M)
[2026-04-08 10:15:05] INFO: No remote host configured, skipping remote sync
[2026-04-08 10:15:05] Cleaning backups older than 7 days...
[2026-04-08 10:15:05] ========== Backup finished ==========
[2026-04-08 10:15:05] Total backup storage used: 57M

Verify the backup files were created:

bashls -lh /var/backups/automated/files/
ls -lh /var/backups/automated/database/

Step 5 — Schedule the Backup with Cron

Cron is the standard job scheduler on Linux. Each line in a crontab file defines a schedule and the command to run. Open your crontab:

bashcrontab -e

If this is your first time, you will be asked to choose an editor. Select nano (option 1).

Add this line at the bottom of the file:

0 2 * * * /opt/backup.sh >> /var/backups/automated/logs/cron.log 2>&1

This runs the backup script every day at 2:00 AM server time. The five fields represent:

FieldValueMeaning
Minute0At minute 0
Hour2At 2 AM
Day of month*Every day
Month*Every month
Day of week*Every day of the week

Save and close the file. Verify the crontab was saved:

bashcrontab -l

You should see your backup line in the output.

Tip

Choose a backup time when your server has low traffic. For most applications, 2:00–4:00 AM in your users' timezone works well. Avoid running backups during peak hours — the tar and pg_dump operations consume CPU and disk I/O that could affect application performance.

Common cron schedules for reference:

bash0 2 * * *       # Daily at 2:00 AM
0 */6 * * *     # Every 6 hours
0 2 * * 0       # Weekly on Sunday at 2:00 AM
0 2 1 * *       # Monthly on the 1st at 2:00 AM
*/30 * * * *    # Every 30 minutes (for critical databases)

Step 6 — Configure Rsync for Off-Server Backups

Local backups protect against accidental deletions and application errors, but they do not protect against disk failure or server compromise. Copying backups to a second server adds a critical layer of protection.

On your backup destination server (a second Raff VM or any remote Linux server), create the backup directory:

bashsudo mkdir -p /backups
sudo useradd -m -s /bin/bash backup
sudo chown backup:backup /backups

On your primary server, set up SSH key-based authentication to the backup server so rsync can run without a password prompt:

bashssh-keygen -t ed25519 -f ~/.ssh/backup_key -N ""
ssh-copy-id -i ~/.ssh/backup_key.pub backup@BACKUP_SERVER_IP

Test the connection:

bashssh -i ~/.ssh/backup_key backup@BACKUP_SERVER_IP "echo 'Connection successful'"

Now update the backup script to enable remote sync. Edit /opt/backup.sh and set:

bashREMOTE_USER="backup"
REMOTE_HOST="BACKUP_SERVER_IP"
REMOTE_DIR="/backups/$(hostname)"

Also update the rsync command's SSH flag to use your dedicated key:

bashrsync -avz --delete \
    -e "ssh -i /home/deploy/.ssh/backup_key -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10" \
    "${BACKUP_DIR}/files/" \
    "${BACKUP_DIR}/database/" \
    "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/" 2>> "$LOG_FILE"

Run the script manually to verify remote sync works:

bash/opt/backup.sh

Check the log for "Remote sync complete." Then verify on the backup server:

bashssh -i ~/.ssh/backup_key backup@BACKUP_SERVER_IP "ls -lh /backups/$(hostname)/"

The --delete flag in rsync removes files from the destination that no longer exist in the source. This keeps the remote backup in sync with your retention policy — when local cleanup deletes old backups, the next rsync run removes them from the remote server too.

Step 7 — Combine with Raff Snapshots for Full-Server Protection

File-level backups with cron and rsync give you granular recovery — you can restore a single database or a specific directory. But if your entire server needs to be rebuilt (kernel failure, compromised root account, catastrophic misconfiguration), restoring individual files is slow.

Raff's snapshot and automated backup features capture the complete VM state — operating system, installed packages, configuration files, application data, everything. A snapshot restore brings your entire server back in minutes, not hours.

The best strategy combines both approaches:

LayerToolProtects AgainstRecovery Time
Application backupcron + rsync + tarAccidental deletion, corrupted data, bad deployMinutes (restore specific files)
Database backupcron + pg_dump/mysqldumpData corruption, dropped tables, logical errorsMinutes (restore database)
Off-server copyrsync to remote serverDisk failure, server compromise10-30 minutes (copy back + restore)
VM snapshotRaff automated snapshotsOS failure, kernel panic, total compromise2-5 minutes (full server restore)

Configure Raff automated backups through the dashboard:

  1. Navigate to your VM in the Raff control panel
  2. Go to Backups & Snapshots
  3. Enable automated backups with your preferred schedule (daily, weekly, or monthly)
  4. Set the retention period

Tip

A typical 5 GB snapshot costs approximately $0.25/month on Raff. For a production server, enabling daily automated snapshots alongside your cron-based file backups gives you complete coverage for under $10/month.

Step 8 — Set Up Backup Monitoring

A backup system that fails silently is worse than no backup system at all — it gives you false confidence. Add a simple monitoring check that alerts you when backups stop working.

Create a health check script:

bashnano /opt/backup-check.sh

Add the following:

bash#!/bin/bash
# Check if today's backup exists

BACKUP_DIR="/var/backups/automated"
TODAY=$(date +%Y-%m-%d)

FILE_COUNT=$(find "${BACKUP_DIR}/files" -name "*${TODAY}*" -type f | wc -l)
DB_COUNT=$(find "${BACKUP_DIR}/database" -name "*${TODAY}*" -type f | wc -l)

if [ "$FILE_COUNT" -eq 0 ] || [ "$DB_COUNT" -eq 0 ]; then
    echo "BACKUP ALERT: Missing backup for ${TODAY}" >&2
    echo "  File backups found: ${FILE_COUNT}"
    echo "  Database backups found: ${DB_COUNT}"
    exit 1
fi

echo "Backup OK: ${FILE_COUNT} file archive(s), ${DB_COUNT} database dump(s) for ${TODAY}"
exit 0

Make it executable:

bashchmod 755 /opt/backup-check.sh

Schedule the health check to run after backups complete. Add to your crontab:

bashcrontab -e

Add this line below your backup schedule:

30 3 * * * /opt/backup-check.sh >> /var/backups/automated/logs/check.log 2>&1

This runs at 3:30 AM — 90 minutes after the backup starts — giving enough time for the backup to finish.

If you are running Uptime Kuma for monitoring, you can use a push monitor: add curl -s https://your-kuma-instance/api/push/MONITOR_TOKEN?status=up to the end of the backup script (on success) so Kuma alerts you if the push stops arriving.

Step 9 — Verify with a Test Restore

An untested backup is not a backup. Verify that your archives are valid and you can restore from them.

Test file restoration to a temporary directory:

bashmkdir /tmp/restore-test
tar -xzf /var/backups/automated/files/www-2026-04-08_0200.tar.gz -C /tmp/restore-test/
ls -la /tmp/restore-test/html/

You should see your web files with correct permissions and timestamps.

Test database restoration to a temporary database:

bashsudo -u postgres createdb myapp_restore_test
gunzip -c /var/backups/automated/database/db-2026-04-08_0200.sql.gz | sudo -u postgres psql myapp_restore_test

Verify the data:

bashsudo -u postgres psql myapp_restore_test -c "SELECT count(*) FROM your_main_table;"

If the count matches your production database, the backup is valid. Clean up the test:

bashsudo -u postgres dropdb myapp_restore_test
rm -rf /tmp/restore-test

Conclusion

You have built a complete automated backup system on your Raff Ubuntu 24.04 VM: a bash script that archives web files and database dumps, cron scheduling for hands-free daily execution, rsync for off-server copies, retention management to control disk usage, a health check for monitoring, and a tested restore procedure that you can trust.

Combined with Raff's automated snapshots, this setup gives you four layers of protection — application files, database, off-server copy, and full VM state — at a total cost of a few dollars per month.

From here, you can:

  • Add email notifications to the backup script using mailutils or a webhook to Slack
  • Encrypt backup archives with gpg before syncing off-server for sensitive data
  • Extend the script to back up multiple databases or application directories
  • Set up Raff Object Storage as an S3-compatible backup destination using rclone

This tutorial was tested by our systems engineering team on a Raff CPU-Optimized Tier 3 VM. The backup script runs nightly across our own infrastructure — every Raff customer's data protection depends on the same cron + rsync + snapshot approach described here.

Get notified when we publish new tutorials

Cloud tips, step-by-step guides, and infrastructure insights — straight to your inbox.

Frequently Asked Questions

Ready to get started?

Deploy an Ubuntu 24.04 VM and follow along in under 60 seconds.

Deploy a VM Now