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/
Warning
If the script fails with permission errors on the database dump, ensure the postgres system user can access the database. For PostgreSQL, the sudo -u postgres pg_dump command runs the dump as the PostgreSQL superuser, which requires your user to have sudo privileges.
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:
| Field | Value | Meaning |
|---|---|---|
| Minute | 0 | At minute 0 |
| Hour | 2 | At 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:
| Layer | Tool | Protects Against | Recovery Time |
|---|---|---|---|
| Application backup | cron + rsync + tar | Accidental deletion, corrupted data, bad deploy | Minutes (restore specific files) |
| Database backup | cron + pg_dump/mysqldump | Data corruption, dropped tables, logical errors | Minutes (restore database) |
| Off-server copy | rsync to remote server | Disk failure, server compromise | 10-30 minutes (copy back + restore) |
| VM snapshot | Raff automated snapshots | OS failure, kernel panic, total compromise | 2-5 minutes (full server restore) |
Configure Raff automated backups through the dashboard:
- Navigate to your VM in the Raff control panel
- Go to Backups & Snapshots
- Enable automated backups with your preferred schedule (daily, weekly, or monthly)
- 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
Warning
Schedule a restore test at least once per month. We do this on our own infrastructure on the first Monday of every month — it takes 10 minutes and has caught two issues that would have meant data loss if we had waited until an actual emergency.
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
mailutilsor a webhook to Slack - Encrypt backup archives with
gpgbefore 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.

