How to back up and restore a Moodle site
A Moodle site backup has three parts: a dump of the database, a copy of the moodledata directory, and the Moodle code with its config.php. Dump the database first, then archive moodledata without its cache and temp folders. To restore, load all three on the new server, update wwwroot and dataroot in config.php, purge the caches and run the search and replace tool if the URL changed. Course backups (.mbz files) do not replace any of this.
The three parts of a Moodle site
This guide uses Moodle 5.2 in /var/www/moodle, data in /var/moodledata, a database named moodle and a web server running as www-data. Read your own values from config.php:
grep -E 'CFG->(dbtype|dbhost|dbname|dbuser|prefix|wwwroot|dataroot)' /var/www/moodle/config.php| Part | Where | Back it up? |
|---|---|---|
| Database | $CFG->dbname, PostgreSQL or MySQL/MariaDB | Yes. Users, courses, grades, settings. |
| Uploaded files | moodledata/filedir | Yes. Files are named by a hash of their content, so they are useless without the database. |
| Encryption key | moodledata/secret, or $CFG->secretdataroot | Yes. Without it, encrypted settings cannot be read. |
| Language packs, cache config | moodledata/lang, moodledata/muc | Yes. |
| Caches, sessions, temp files, trash | moodledata/cache, localcache, sessions, temp, trashdir | No. Moodle's migration guide says they need not be copied. |
Code, plugins, config.php | /var/www/moodle | Yes. Custom and third-party plugins cannot always be downloaded again. |
Since Moodle 5.1, the web-accessible files live in a public folder and the web server's document root must point at it. config.php and admin/cli stay one level up, in the Moodle root. Plugin CLI scripts moved under public.
config.php holds the database password and moodledata holds the encryption key. Keep the backups in private storage and encrypt them.
Course backups are not a site backup
Moodle can save each course as a .mbz file, and Site administration > Courses > Backups > Automated backup setup does it on a schedule. Moodle's documentation says course backups "should never be used as a primary backup system". Each file holds one course; the code, plugins, site settings and anything outside courses are not in it. Moodle 5.2's defaults make the gap wider: automated backups are disabled, keep only 1 backup per course, skip hidden courses and courses unchanged for 30 days, and are stored in the course backup filearea, which is inside moodledata on the same server.
Use course backups to copy or reuse courses. If you keep automated ones, a site backup carries them as well, so watch how much they add to moodledata.
Maintenance mode
Moodle's migration guide starts with maintenance mode, to stop new data arriving. Run Moodle's CLI scripts as the web server user, so files they create in moodledata stay writable for it:
sudo -u www-data /usr/bin/php /var/www/moodle/admin/cli/maintenance.php --enableCLI maintenance mode blocks all web access, administrators included. CLI scripts keep working, except admin/cli/cron.php. It works by writing climaintenance.html into moodledata and setting maintenance_enabled in the database, so a backup taken during it restores into maintenance mode. --disable clears both. Use it for a migration. For nightly backups on a busy site, you can instead dump the database first and copy moodledata straight after; a file added in between is then stored but unused, which is harmless.
Dump the database
install -d -m 700 /var/backups/moodleOn PostgreSQL, a custom-format dump is compressed and restores with pg_restore. pg_dump reads from one snapshot, so the dump is consistent while the site runs:
sudo -u postgres pg_dump -Fc moodle > /var/backups/moodle/moodle-db-$(date +%F).dumppg_restore --list /var/backups/moodle/moodle-db-2026-10-04.dump | head--list prints the archive's table of contents, a quick sign the file is a readable pg_dump archive; the exit status of pg_dump itself is what tells you the dump finished. The pg_dump guide covers the other formats.
On MySQL or MariaDB, put the credentials in an option file so the password stays out of ps, then dump:
[client]
user=moodleuser
password="your-password-here"
host=localhostchmod 600 /etc/mysql/moodle-backup.cnfmysqldump --defaults-extra-file=/etc/mysql/moodle-backup.cnf --single-transaction --no-tablespaces --default-character-set=utf8mb4 moodle | gzip > /var/backups/moodle/moodle-db-$(date +%F).sql.gz--single-transactiondumps InnoDB tables from one consistent snapshot. It also means mysqldump does not needLOCK TABLES, which the grants in Moodle's MySQL guide leave out.--no-tablespacesavoids thePROCESSprivilege, also missing from those grants.--default-character-set=utf8mb4matches the character set Moodle's guide creates the database with, so emoji and other 4-byte characters survive.- On MariaDB 11 and later the program is also called
mariadb-dump.
Archive moodledata and the code
Archive moodledata after the dump, leaving out the folders Moodle's migration guide lists as unnecessary. Exclude patterns match the stored names and must come before the directory:
tar -czf /var/backups/moodle/moodledata-$(date +%F).tar.gz --exclude='moodledata/cache' --exclude='moodledata/localcache' --exclude='moodledata/sessions' --exclude='moodledata/temp' --exclude='moodledata/trashdir' -C /var moodledatatar -czf /var/backups/moodle/moodle-code-$(date +%F).tar.gz -C /var/www moodleIf config.php sets $CFG->secretdataroot, $CFG->tempdir or $CFG->cachedir elsewhere, add the secret directory to the archive; the others can be skipped. A large filedir changes little from day to day, so rsync or restic copy it faster than a nightly full archive.
Restore on a new server
Install the web server, the PHP version and extensions your Moodle release requires, and the database server. Create an empty database and user as Moodle's guides do. PostgreSQL:
CREATE USER moodleuser WITH PASSWORD 'yourpassword';
CREATE DATABASE moodle WITH OWNER moodleuser;sudo -u postgres pg_restore --dbname=moodle --no-owner --role=moodleuser --exit-on-error < /var/backups/moodle/moodle-db-2026-10-04.dump--no-owner drops the original ownership commands and --role creates every object as moodleuser, so the restore works even if the old owner had a different name. Reading from < lets root's shell open the private backup file. On MySQL or MariaDB, create the database with Moodle's settings, recreate the option file from above on this server, and load the dump:
CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER moodleuser@localhost IDENTIFIED BY 'yourpassword';
GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,CREATE TEMPORARY TABLES,DROP,INDEX,ALTER ON moodle.* TO moodleuser@localhost;gunzip -c /var/backups/moodle/moodle-db-2026-10-04.sql.gz | mysql --defaults-extra-file=/etc/mysql/moodle-backup.cnf moodleExtract moodledata and the code, then give moodledata to the web server user:
tar -xzf /var/backups/moodle/moodledata-2026-10-04.tar.gz -C /vartar -xzf /var/backups/moodle/moodle-code-2026-10-04.tar.gz -C /var/wwwchown -R www-data:www-data /var/moodledataThe web server needs read and write access to moodledata and read access to the code; Moodle's install guide leaves the code owned by root. Edit config.php for this server. wwwroot takes no trailing slash:
$CFG->dbhost = 'localhost';
$CFG->dbname = 'moodle';
$CFG->dbuser = 'moodleuser';
$CFG->dbpass = 'yourpassword';
$CFG->wwwroot = 'https://moodle.example.com';
$CFG->dataroot = '/var/moodledata';Point the web server's document root at /var/www/moodle/public (Moodle 5.1 and later). If the address changed, links stored in course content still point at the old one. Moodle's search and replace tool fixes them; its own help says changes cannot be reverted, so run it on a site you have just backed up. --shorten is needed when the new string is longer than the old:
sudo -u www-data /usr/bin/php /var/www/moodle/public/admin/tool/replace/cli/replace.php --search=//old.example.com --replace=//moodle.example.com --shorten --non-interactiveOn Moodle 5.0 and earlier, drop public/ from that path. Then clear the caches built on the old server and take the site out of maintenance mode:
sudo -u www-data /usr/bin/php /var/www/moodle/admin/cli/purge_caches.phpsudo -u www-data /usr/bin/php /var/www/moodle/admin/cli/maintenance.php --disableFinally install Moodle's cron for the web server user with crontab -u www-data -e, running every minute:
* * * * * /usr/bin/php /var/www/moodle/admin/cli/cron.php >/dev/nullUpgrades versus restores
The database records the Moodle version that last upgraded it, and Moodle refuses to run older code against it. Restore the code from the same backup set as the database. Newer code is allowed: Moodle then runs an upgrade, which is how you move an old backup onto a current release. Moodle 5.2 only upgrades from 4.4 or later, so older sites need an intermediate step.
An upgrade cannot be undone in place. Moodle's upgrade guide says to back up the code, moodledata and the database first; to go back, you restore all three from that moment. Practice the upgrade on a restored copy, where admin/cli/upgrade.php --non-interactive runs it from the command line.
Verify and automate
sudo -u www-data /usr/bin/php /var/www/moodle/admin/cli/checks.phpThat runs Moodle's status checks and reports the ones that fail; -v shows all of them. Then fetch the login page and expect 200, log in, open a course and download a file from it, a quick check that moodledata matches the database:
curl -sS -o /dev/null -w '%{http_code}\n' https://moodle.example.com/login/index.phpFor nightly backups on PostgreSQL, this script gives each file its real name only after the command that wrote it succeeded, and treats tar's exit code 1 (a file changed while being read) as success:
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/var/backups/moodle"
KEEP_DAYS=14
STAMP="$(date +%Y-%m-%d_%H%M)"
DB="$BACKUP_DIR/moodle-db-$STAMP.dump"
DATA="$BACKUP_DIR/moodledata-$STAMP.tar.gz"
CODE="$BACKUP_DIR/moodle-code-$STAMP.tar.gz"
mkdir -p "$BACKUP_DIR"
trap 'rm -f "$DB.partial" "$DATA.partial" "$CODE.partial"' EXIT
sudo -u postgres pg_dump -Fc moodle > "$DB.partial"
pg_restore --list "$DB.partial" > /dev/null
mv "$DB.partial" "$DB"
tar -czf "$DATA.partial" --exclude='moodledata/cache' --exclude='moodledata/localcache' \
--exclude='moodledata/sessions' --exclude='moodledata/temp' --exclude='moodledata/trashdir' \
-C /var moodledata || [ $? -eq 1 ]
mv "$DATA.partial" "$DATA"
tar -czf "$CODE.partial" -C /var/www moodle
mv "$CODE.partial" "$CODE"
find "$BACKUP_DIR" -type f -name 'moodle*' -mtime +"$KEEP_DAYS" -deletePATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 2 * * * root flock -n /run/lock/moodle-backup.lock /usr/local/bin/moodle-backup.sh >> /var/log/moodle-backup.log 2>&1Make it executable with chmod 755, copy the results off the server, and test a restore on a spare machine before term starts.
Common errors
| Error | Fix |
|---|---|
Error: The code you are using is older than the version recorded in the database. | Restore the code from the same backup set as the database, or newer code. |
Error: Database connection failed | Check dbtype, dbhost, dbname, dbuser and dbpass in config.php, and the user's grants. |
Fatal error: $CFG->dataroot is not configured properly, directory does not exist or is not accessible! Exiting. | Set $CFG->dataroot to the restored path. |
Fatal error: $CFG->dataroot is not writable, admin has to fix directory permissions! Exiting. | chown -R www-data:www-data /var/moodledata. |
Incorrect access detected, this server may be accessed only through "..." address, sorry. | $CFG->wwwroot does not match the address you used. |
| Every page shows the maintenance message | The backup was taken in maintenance mode. Run maintenance.php --disable. |
The replacement is longer than the original and shortening is not allowed; cannot continue. | Add --shorten to replace.php. |
Moving to another provider? See moving a server to a new provider. Restoring a PostgreSQL dump in more depth: restoring a PostgreSQL dump.
Frequently asked questions
- Are Moodle course backups enough?
- No. Moodle's documentation says course backups should never be your primary backup. They hold one course each, without the code, plugins or site settings, and by default they are stored inside moodledata on the same server.
- Which moodledata folders can I skip?
- Moodle's migration guide lists cache, localcache, sessions, temp and trashdir as unnecessary. Keep filedir, lang, muc and secret.
- Do I need maintenance mode to back up Moodle?
- For a migration, yes; Moodle's guide starts with it. For nightly backups, pg_dump and mysqldump with --single-transaction are consistent on their own; dump the database before copying moodledata.
- How do I change the Moodle URL after a restore?
- Set $CFG->wwwroot in config.php, then run admin/tool/replace/cli/replace.php (under public/ on Moodle 5.1 and later) with the old and new addresses, and purge the caches.
How this was checked
Commands, limits and prices were checked against these official pages, on October 4, 2026:
- Moodle docs (5.2): Site backup
- Moodle docs (5.2): Moodle migration
- Moodle docs (5.2): Upgrading
- Moodle docs (5.2): Maintenance mode
- Moodle docs (5.2): Administration via command line
- Moodle docs (5.2): Search and replace tool
- Moodle docs (5.2): Automated course backup
- Moodle docs (5.2): Course backup
- Moodle docs (5.2): Installing Moodle
- Moodle docs (5.2): PostgreSQL
- Moodle docs (5.2): MySQL
- Moodle docs (5.2): Cron
- Moodle 5.2 source: admin/cli scripts (maintenance, purge_caches, checks, upgrade)
- Moodle 5.2 source: config-dist.php and lib/setup.php (dataroot, secret key, cache directories)
- Moodle 5.2 source: automated backup defaults (admin/settings/courses.php)
- Moodle 5.2 source: error strings (lang/en/error.php)
- PostgreSQL 18 docs: pg_dump
- PostgreSQL 18 docs: pg_restore
- MySQL 8.4 Reference Manual: mysqldump