Skip to content
Skip to content
Editorial 3D illustration of Moodle backup and disaster recovery with protected data snapshots, restore checkpoints, and cloud storage.

Complete Moodle Backup and Disaster Recovery Guide for 2026

The failure mode is common: backups appear to run for years, then a restore test reveals truncated dumps, missing files, or inconsistent database snapshots. By the time ransomware, accidental deletion, or storage corruption makes recovery urgent, an untested backup chain can turn a routine incident into weeks of reconstruction work.

This guide shows you how to build a backup system you can actually trust, and how to verify it before disaster strikes.

Understanding Moodle's Two Backup Systems

Moodle has two completely different backup systems. Confusing them is how most institutions end up unprotected.

  1. Course-Level Backups (MBZ Files) are Moodle's built-in export tool. They capture a single course structure, content, activities, and optionally user data. They're useful for migrating courses between instances or creating pre-change snapshots. They are not a disaster recovery tool. They don't capture user accounts, global configuration, plugins, or server settings.

  2. Site-Level Backups (Your Responsibility) are your responsibility and the only real path to disaster recovery. A complete site backup requires all three of these components -- lose any one and your restore is incomplete:

    • The database: every grade, enrollment, user account, forum post, and configuration setting.
    • The moodledata directory: every uploaded file, student submission, and cached asset.
    • The application code: Moodle core, config.php, plugins, themes, and custom patches.

Building Your Site-Level Backup Strategy

This is where disaster recovery actually happens. Your site-level backup strategy needs to capture all three components reliably, store them safely, and allow you to restore within your target recovery window.

Step 1: Define Your RPO and RTO

Before writing a single script, define two numbers:

  • RPO (Recovery Point Objective): How much data loss can you afford? An RPO of 24 hours means nightly backups suffice. An RPO of 1 hour means you need 24 backups per day.
  • RTO (Recovery Time Objective): How long until the site must be back online?
Institution TypeRecommended RPORecommended RTO
K-12 school24 hours8-12 hours
University (active semester)4-8 hours2-4 hours
Corporate training1-4 hours1-2 hours
Medical/legal education1 hour1 hour
During exam periods1 hour30-60 minutes

Your RPO determines backup frequency. Your RTO determines how fast you need to restore -- and whether you need pre-provisioned standby infrastructure.

Step 2: Database Backup

The database is your most critical component. Use --single-transaction for InnoDB tables -- it takes a consistent snapshot without locking tables or interrupting users.

Shell
#!/bin/bash
# /usr/local/bin/moodle-db-backup.sh

BACKUP_DIR="/var/backups/moodle/database"
DATE=$(date +%Y-%m-%d_%H%M)
DB_NAME="moodle"
DB_USER="moodle_backup"
RETENTION_DAYS=14
LOG_FILE="/var/log/moodle/db-backup.log"

mkdir -p "$BACKUP_DIR"
echo "[$(date)] Starting database backup..." >> "$LOG_FILE"

mysqldump \
  --user="$DB_USER" \
  --single-transaction \
  --routines \
  --triggers \
  --quick \
  "$DB_NAME" | gzip > "$BACKUP_DIR/moodle_db_${DATE}.sql.gz"

BACKUP_FILE="$BACKUP_DIR/moodle_db_${DATE}.sql.gz"
BACKUP_SIZE=$(stat -c%s "$BACKUP_FILE")

# Verify file is not suspiciously small
if [ "$BACKUP_SIZE" -lt 1000 ]; then
  echo "[$(date)] ERROR: Backup too small ($BACKUP_SIZE bytes)" >> "$LOG_FILE"
  echo "Moodle DB backup FAILED" | mail -s "CRITICAL: DB backup failure" admin@yourdomain.com
  exit 1
fi

# Verify gzip integrity
if ! gzip -t "$BACKUP_FILE" 2>/dev/null; then
  echo "[$(date)] ERROR: Backup file corrupted" >> "$LOG_FILE"
  echo "Moodle DB backup CORRUPTED" | mail -s "CRITICAL: DB backup corruption" admin@yourdomain.com
  exit 1
fi

find "$BACKUP_DIR" -name "moodle_db_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
echo "[$(date)] Backup complete: $BACKUP_FILE ($BACKUP_SIZE bytes)" >> "$LOG_FILE"

For databases over 50 GB, switch to Percona XtraBackup. It performs a hot binary copy without table locks -- a 100 GB database takes 15-30 minutes versus 2-4 hours with mysqldump.

For PostgreSQL, use pg_dump --format=custom, which supports parallel restore and built-in compression.

Step 3: Moodledata Backup

The moodledata directory contains every file your users have uploaded, cached, and generated. It grows continuously and can reach hundreds of gigabytes on active sites.

shell
#!/bin/bash
# /usr/local/bin/moodle-data-backup.sh

rsync -az \
  --delete \
  --checksum \
  --exclude='cache/' \
  --exclude='localcache/' \
  --exclude='sessions/' \
  --exclude='temp/' \
  /var/moodledata/ \
  /var/backups/moodle/moodledata/ \
  >> /var/log/moodle/data-backup.log 2>&1 || \
  echo "Moodledata backup FAILED" | mail -s "CRITICAL: Data backup failure" admin@yourdomain.com

Use rsync for incremental syncs. The --checksum flag compares file contents rather than just timestamps -- slower, but it catches corruption that timestamp-based comparison misses.

Excluding cache/, sessions/, and temp/ typically reduces backup size by 10-30% since Moodle regenerates these automatically.

Step 4: Application Code Backup

Code changes far less frequently than data. Weekly backups are sufficient, with additional snapshots before and after upgrades.

Shell
tar -czf /var/backups/moodle/code/moodle_code_$(date +%Y-%m-%d).tar.gz \
  -C /var/www moodle \
  --exclude='moodle/.git' \
  --exclude='moodle/node_modules'

Step 5: Cron Schedule

Combine all three backup scripts into a coordinated schedule:

bash
# /etc/cron.d/moodle-backups

# Database: nightly at 2 AM
0 2 * * * root /usr/local/bin/moodle-db-backup.sh

# Moodledata: nightly at 3 AM (staggered after DB)
0 3 * * * root /usr/local/bin/moodle-data-backup.sh

# Code: weekly on Sunday at 4 AM
0 4 * * 0 root /usr/local/bin/moodle-code-backup.sh

Stagger start times so scripts don't compete for disk I/O.

Offsite Storage: The 3-2-1 Rule

Every production backup strategy must follow the 3-2-1 rule: 3 copies of your data, on 2 different media, with 1 copy offsite.

A backup stored on the same server as Moodle won't survive ransomware or hardware failure. Use rclone to sync backups to cloud object storage with encryption:

Shell
rclone sync /var/backups/moodle b2-encrypted:moodle-backups \
  --transfers=4 --log-file=/var/log/moodle/offsite-sync.log

Cloud Storage Cost Comparison (500 GB)

Note: These May 2026 examples are storage-cost planning figures for 500 GB before taxes, request charges, retrieval charges, and region-specific differences. Real-world costs vary with egress (download), API request volume, minimum storage duration, and restore patterns. AWS and Azure include limited monthly egress allowances, while Backblaze B2 advertises free egress up to a multiple of your monthly storage volume. Always verify regional pricing and retrieval fees, especially for archive tiers, before deployment.

ProviderStorage/MonthNotes
Backblaze B2~$3.00$0.005/GB; egress free up to 3x monthly storage
AWS S3 Standard~$11.50$0.023/GB; egress charged separately (~$0.09/GB)
AWS S3 Glacier Instant~$2.00$0.004/GB; per-retrieval fees apply
Azure Cool Blob~$5.00Good balance of cost and access speed

For disaster recovery specifically, where you rarely access backups, Backblaze B2 or AWS S3 Glacier Instant offer the best value.

Tiered Retention

Keep 7-day backups only, and you risk having every copy contain corrupted data if a problem goes unnoticed for 8+ days. Use tiered retention instead:

  • Daily backups: 14 days
  • Weekly backups: 8 weeks
  • Monthly backups: 12 months
Shell
# Promote Saturday's backup to weekly archive
0 5 * * 0 root cp /var/backups/moodle/daily/moodle_db_$(date -d 'yesterday' +\%Y-\%m-\%d)*.sql.gz \
  /var/backups/moodle/weekly/

# Clean up by tier
0 7 * * * root find /var/backups/moodle/daily/ -mtime +14 -delete
0 7 * * 0 root find /var/backups/moodle/weekly/ -mtime +56 -delete
0 7 1 * * root find /var/backups/moodle/monthly/ -mtime +365 -delete

Testing Your Backups

A backup you haven't restored is a backup you can't trust.

Run a full restore test quarterly on a separate test server. Here's the procedure:

Shell
# 1. Restore the database
# Create the DB with 2026 utf8mb4 standards and import
mysql -e "CREATE DATABASE moodle_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
gunzip < moodle_db_2026-04-01_0200.sql.gz | mysql --default-character-set=utf8mb4 moodle_test

# 2. Restore application code
tar -xzf moodle_code_2026-03-30.tar.gz -C /var/www/

# 3. Restore moodledata
# Use rsync to skip regenerating cache/temp files, saving RTO time
rsync -az --exclude='cache/' --exclude='localcache/' --exclude='temp/' \
  /path/to/backup/moodledata/ /var/moodledata_test/

# 4. Update config.php and Database URLs
# After pointing config.php to the test DB/Paths, fix hardcoded URLs:
sudo -u www-data php /var/www/moodle/admin/tool/replace/cli/replace.php \
  --search="https://moodle.college.edu" \
  --replace="https://moodle-test.local" --non-interactive

# 5. Fix permissions
# Hardening: Webserver should own data, but code should be read-only
chown -R www-data:www-data /var/moodledata_test
chown -R root:root /var/www/moodle
chown -R www-data:www-data /var/www/moodle/cache
chmod 444 /var/www/moodle/config.php

# 6. Run Moodle health & upgrade checks
sudo -u www-data php /var/www/moodle/admin/cli/purge_caches.php
sudo -u www-data php /var/www/moodle/admin/cli/upgrade.php --non-interactive
sudo -u www-data php /var/www/moodle/admin/cli/checks.php

After each test, document: how long the restore took, which backup date you used, any errors encountered, whether you met your RTO target, and what needs fixing.

Common Failures and How to Prevent Them

  1. Silent script failures. Cron jobs fail without anyone noticing because errors go nowhere. Every script must send an immediate alert on failure and a daily success summary. If the summary email stops arriving, your entire cron system may be broken.
  2. Disk space exhaustion. Add a pre-flight disk space check so backups fail loudly before they silently produce truncated files:
Shell
# Pre-flight disk space check (requires 20 GB free)
FREE_KB=$(df --output=avail --no-header /var/backups/moodle)

if [ "$FREE_KB" -lt 20971520 ]; then
  echo "Insufficient disk space: Only ${FREE_KB}KB remaining." | \
  mail -s "CRITICAL: Backup disk low on $(hostname)" admin@yourdomain.com
  exit 1
fi
  1. Inconsistent backups. Your DB backup runs at 2 AM; moodledata runs at 3 AM. A file uploaded in between will be in one backup but not the other. Minimize the gap -- or use maintenance mode for a fully consistent snapshot if your RTO allows the brief downtime.
  2. Backup retention too short. If corruption goes unnoticed for 10 days and you only keep 7 days of backups, every copy is already corrupted. See the tiered retention strategy above.
  3. Unencrypted off-site storage. Your backups contain student PII. Encrypt before transfer using GPG or rclone's built-in crypt backend. Store the passphrase separately from the backups themselves.
  4. Nobody has ever done a real restore. Staff turnover means the person who wrote the runbook is gone. Rotate which team member runs the quarterly restore test so knowledge doesn't concentrate in one person.

For Low RPO: Database Replication

If your RPO target is under 4 hours, daily backups aren't enough. Set up MySQL/MariaDB replication so a read replica receives changes in near-real time.

Bash
# Primary server (mysqld.cnf)
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_do_db = moodle
binlog_format = ROW

Monitor Seconds_Behind_Master on the replica. If it climbs above 60, your effective RPO is growing. For zero-RPO requirements, a Percona XtraDB Cluster with 3 nodes provides synchronous replication every write commit across all nodes before returning success.

Quick Reference: Disaster Scenarios

ScenarioTypical RTOResponse
Single disk failure0-1 hoursReplace disk, rebuild RAID array
Full server failure2-8 hoursRestore to new hardware or cloud instance
Database corruption1-4 hoursRestore last verified database dump
Ransomware attack4-24 hoursIsolate, rebuild on fresh infrastructure from clean backup
Accidental course deletion30-60 minutesRestore from Moodle's course-level backup
Failed upgrade1-2 hoursRestore from pre-upgrade snapshot

Final Checklist

  • Site-level backup covers all three components: database, moodledata, code
  • Backup scripts send alerts on failure and daily success confirmations
  • Offsite copy exists, geographically separate from production
  • Backups are encrypted before offsite transfer
  • Tiered retention: daily (14d), weekly (8wk), monthly (12mo)
  • Quarterly restore test completed and documented
  • Runbook is stored somewhere accessible when production is down
  • At least two people know how to execute a full restore

Research References