How to Upgrade Moodle Safely on a Self-Hosted Server
This guide walks through a production-safe Moodle upgrade, including major-versus-minor planning, platform checks, plugin inventory, test upgrades, live cutover, cron updates, and rollback preparation.
Preamble: Minor vs. Major Upgrades
What is the difference between a minor upgrade and a major upgrade? A minor upgrade is usually a security patch which can be deployed by overwriting the core code base. By contrast, a major upgrade typically also requires you to upgrade parts of the LAMP stack Moodle is running on, as well as the theme (in case you have customized the theme for the client).
The following is mostly about performing a major upgrade. We will include a separate section on executing a minor upgrade at the end of the SOP. If you just need to perform a minor upgrade, you can skip to that section right away.
Familiarize yourself with the Moodle upgrade procedure
Before you get started, check out the Upgrading page on Moodle’s website.
Upgrading in a nutshell
-
Backup everything beforehand, specifically:
-
The database
-
The moodledata directory
-
The entire code base (can usually be found in public_html, htdocs, or a subdirectory called moodle)
-
-
Replace the code base with the new version
-
Put the config.php file of the old version in the code base of the new version
-
Add the new versions of the 3rd party plugins
Get access to Moodle and server
Ask the customer for:
-
Moodle url
-
Moodle admin username
-
Moodle admin password
-
Web server url or ip address
-
Web server username
-
Web server password
If the Moodle website is hosted on one of our own servers, we should already have this information – don’t bother the customer in that case, but check with your lead engineer instead.
Check if server meets requirements
As stated in the moodle.org section on upgrading, make sure that the customer’s server meets the minimum requirements for the new Moodle version. The most important software to check is:
-
MySQL (or whatever database they’re currently running Moodle on)
-
In the case of upgrades from 2.9 to 3.1 or later, check whether the MySQL / MariaDB database can be converted from Antelope to Barracuda and the new character set & collation for.
-
-
PHP – including the required extensions
If the server does not meet the requirements, make sure to check with the sysop whether the system can be updated. Do not perform the actual system updates yet, because that may disrupt the current Moodle version.
Installing a Separate PHP Version Using PHP-FPM under Ubuntu
If you are the sysop, or devop, you can also install the correct version of PHP yourself. (See also this site.)
-
Install Ondřej Surý’s package for Apache 2:
sudo add-apt-repository ppa:ondrej/apache2 sudo apt-get update -
Then get the fastcgi mod for php:
sudo apt install software-properties-common sudo apt install libapache2-mod-fcgid -
Add Ondřej’s PHP package:
sudo add-apt-repository ppa:ondrej/php && sudo apt update -
Then install the required php version, e.g.:
-
sudo apt-get install -y php7.3-{bcmath,bz2,intl,gd,mbstring,mysql,zip,common,fpm}
-
-
Now add a directive to the vhost configuration file of the Moodle website, right before the ‘Directory’ part, e.g:
<FilesMatch \.php> # Apache 2.4.10+ can proxy to unix
socket
SetHandler
"proxy:unix:/var/run/php/php7.4-fpm.sock|fcgi://localhost/"
</FilesMatch>
<Directory /home/example-site/public_html>In some cases, it may be necessary to activivate the proxy_fcgi mod at this point:
sudo a2enmod proxy_fcgi proxyAlso don’t forget to restart the webserver gracefully:
/etc/init.d/apache2 gracefulAnd if you’re upgrading to a new php-fpm version, check the php.ini for that version, e.g.:
/etc/php/7.3/fpm/php.iniAnd restart after changing any values (like max_post_size etc.) there:
/etc/init.d/php7.3-fpm restartPlease note: php-fpm also requires performance tweaking. This is outside the scope of this SOP.
Upgrade Cron Job
If the php version has changed and you’re using multiple php versions on the same server, make sure that the command for the cron job execution is updated too:
E.g. if your php version was 7.0 for the previous version of Moodle, and your cron command is this:
/usr/bin/php7.0
/home/example-site/public_html/admin/cli/cron.php >/dev/nullThen you may need to change this to the latest php version:
/usr/bin/php7.4
/home/example-site/public_html/admin/cli/cron.php >/dev/nullUpgrade Backup Script
For most vhosts, we have a custom backup script running: Backupvhost Script. This script relies on the php version Moodle is using, so if the php version has changed for the Moodle upgrade, make sure to call the backup script with the new php version too. In addition, the location of the config.php may change from one version of Totara to another.
Take inventory of all ‘Additional’ (i.e. 3rd party) plugins
Visit /admin/plugins.php and note all ‘Additional’ plugins. These are 3rd party plugins for which you need to get the new source code separately, usually through https://moodle.org/plugins/.
Here’s an example:
| Plugin | 3.1 | 3.4 | Source | Comments |
|---|---|---|---|---|
| Theme Lambda | Yes | Yes | https://themeforest.net/item/lambda-responsive-moodle-theme/9442816 | Only commercially available.Check with the design contact to see if they can download version 3.1 and 3.4. Otherwise ask if it’s alright to buy 3.4. |
| Grid course format | Yes | Yes | https://moodle.org/plugins/format_grid | |
| Local scorm log | No | No | The current version might just work in 3.4. It looks pretty simple – just a bunch of DB functions in the lib.php file. | |
| Mod Questionnaire | Yes | Yes | https://moodle.org/plugins/mod_questionnaire | |
| Block Progress Bar | Yes | No | https://moodle.org/plugins/block_progress | Block completion_progress should be used in 3.4. https://moodle.org/plugins/block_completion_progress |
| auth_mcae | Not actually used | |||
| auth_emailadmin | Not actually used |
(Since the customer’s Moodle version was very old, in this example, we had to perform two subsequent upgrades: first to 3.1 and then to 3.4.)
Pay special attention to the theme
The theme is usually the only part that’s really been customized. In many cases, the theme will not survive any major upgrades, unless it’s a standard Moodle theme that has only had some configuration for an uploaded logo, some color customizations, and maybe a favicon added.
If the site looks very bad after the upgrade, it may be necessary to ask a Moodle theme developer to fix it. This must go through sales first (ask the lead engineer).
Look for any non-Moodle content which is stored in the Moodle directory
Anything that is not part of the Moodle code base (including plugins), such as external images, Flash files, movies, etc., should obviously not be stored in the Moodle directory (the wwwroot folder). But customers do this nonetheless. So be sure to copy that content and put it back in place after an upgrade.
Copy All Additional Plugins
For testing & development it may be convenient to simply copy over all ‘additional’ plugins from the old code base to the new. Here’s a script to do this:
#!/bin/bash
clean="/home/the lead engineer/temp/moodle41-clean"
modified="/home/the lead engineer/temp/moodle41-modified"
target="/home/the lead engineer/php/icm/public_html"# Find directories in modified that are missing in clean, excluding hidden directories
find "$modified" -mindepth 1 -type d ! -path "*/.*" | while read dir;
do
relative_path="${dir#$modified/}" # Get relative path
if [ ! -d "$clean/$relative_path" ]; then
echo "Copying: $relative_path"
rsync -av "$dir" "$target/${relative_path%/*}/"
fi
doneCheck if there are any core hacks
It’s very unusual to find any core hacks on a customer’s Moodle website. Just ask them and if there’s any doubt, diff against the original code base for the exact same version, e.g.:
diff -qr moodle1/ moodle2This compares two directories recursively (-r) and only reports the differences (-q). For example:
diff -qr multitenant-example-a.20240213/
multitenant-example-b.20240213/Files multitenant-example-a.20240213/multitenant/lib.php and multitenant-example-b.20240213/multitenant/lib.php differ
Or, ignoring the .git directory:
diff -qr -exclude='.git' moodle1/ moodle2How to Find The Exact Moodle Build
If you want to compare the client’s Moodle code base with the ‘official’ code base, you need the exact same build. Here’s how to find it.
Switch to the stable branch and update it
In your local clone of the ‘official’ Moodle code base:
git checkout MOODLE_401_STABLE
git pull origin MOODLE_401_STABLEThis ensures your local MOODLE_401_STABLE branch contains every version bump and bugfix up to the latest commit.
Search for the version bump in version.php
git log -S "20250131" -- version.phpThe -S “20250131” flag looks for commits that add or remove the exact string 20250131 in version.php. The top result is the commit where Moodle’s maintainers changed:
$release = '4.1.15+ (Build: 20250131)';
$version = 2022112815.06;Check out that specific commit
Suppose the git log output shows a commit hash of 59a1f3e4a2b8c5d7e8f9a0123b4c5d6e7f8a9b0c. You can then do:
git checkout 59a1f3e4a2b8c5d7e8f9a0123b4c5d6e7f8a9b0cor, if you want a named branch at that point:
git checkout -b moodle-4.1.15-build20250131
59a1f3e4a2b8c5d7e8f9a0123b4c5d6e7f8a9b0cVerify that version.php matches the desired build
grep ‘\$version\|\$release’ version.php
You should see exactly:
$version = 2022112815.06;
$release = '4.1.15+ (Build: 20250131)';At this point, your working copy is pinned to the precise Moodle build (4.1.15+ (Build: 20250131 that matches the client’s code base.
Perform a test upgrade for the customer to approve
Put the test upgrade on a website under our domain. For instance, if the customer’s Moodle site is lms.example.com, create a domain lms.example.com.
But instead of creating a new Moodle website, upgrade the customer’s.
Set Right Expectations for Test Version
In previous upgrade projects, we’ve encountered customers who mistakenly believed that the configuration changes they made to the test-version would be retained in the live-upgrade.
To prevent this, inform the customer that any changes they make to the test-version will not be transferred to the live version of the upgrade.
Also, advise customers to test the customizations primarily. There is no real reason to test Moodle core or any of the much used 3rd party plugins since these will have been tested by the larger user community.
Get The Upgraded Code In The Repository
We deploy the upgrade using a Git branch, let’s call it upgrade. What’s more, the upgrade branch is going to be based on the upstream Moodle repository. In your local repo, do the following. Add the official Moodle repo as the upstream repo:
git remote add upstream https://github.com/moodle/moodle.gitGet a specific branch. Here, we’re pulling the branch containing Moodle 4.1 (the upstream branch name can be found in the upstream repo through git remote show upstream):
git fetch upstream MOODLE_401_STABLECreate a local branch based on the upstream branch:
git checkout -b moodle41 upstream/MOODLE_401_STABLEOptionally, if you want to store the 4.1 version in your remote repo:
git push -u origin moodle41Create the ‘upgrade’ branch and check it out:
git checkout -b upgrade moodle41| During a major upgrade, Moodle does not tolerate the presence of any old files: leftovers from the previous version that are no longer in use. In fact, the upgrade process explicitly checks for the presence of quite a number of old files that should no longer be present in the code base. If it finds any old files, the upgrade process will halt with an error. That’s why we start the upgrade with a completely new branch and don’t branch off the master branch. |
|---|
Finish by adding, committing and pushing the new code:
git commit -a -m "Adding code for Moodle 4.1"
git push --set-upstream origin upgradeUpgrading the website
Put the following items on the test website:
-
The original database
-
The original moodledata directory
-
Checkout the git branch storing the source code for the upgrade (i.e. the new Moodle version)
-
Add the new versions of the 3rd party plugins
-
Copy the config-dist.php file from the source code to config.php and make the adjustments for the proper directory references and database access details.
-
Check if there are any additional language packs that need upgrading
Go to the /admin/ url of the test website to trigger the actual upgrade process. Take a good look at the information on the ‘Current release information’ page that comes up just before you start the upgrade. Do not click on continue.
Are the Server Checks and Other Checks okay? (You can ignore any warnings about the site not being https if you’re on a test site).
Pay attention to collation issues: It is not enough to just change the collation in config.php! You should always run the following cli script if there are any collation issues:
php admin/cli/mysql_collation.php --collation=utf8mb4_unicode_ci(Check Moodle documentation linked to in the /admin page.)
Once everything is ready for the actual upgrade, go to the command line interface. Then:
-
Check that the cli version of php is the same as the ‘web version’
-
Create a Screen session (this means the upgrade will proceed even if your connection to the server is suddenly broken).
-
Navigate to /admin/cli/ and perform the following command:
php upgrade.php (please use the correct php version here, e.g.
php5.6)We don’t want to use the web interface for upgrading, because the upgrade can take a long time. You might run into timeout issues somewhere along the way.
Basic Tests
After the upgrade, perform a few basic tests:
-
Test if Moodle is still sending out email
-
Make sure the cron job is still running properly
Contact the customer
Once the test upgrade is done, contact the customer with the details of the test website, specifically the url.
Ask the customer to approve the upgrade.
Also send the details of the test installation, such as the Moodle admin account (username and password) to our lead developer (the lead engineer, at the time of writing), including the MySQL password, the Unix password and the url. Be sure to do that in a secure way.
Perform the live upgrade
Prepare the Git repository for the deployment of the upgraded code. In your local repo, do the following:
git checkout upgrade
git pull
git checkout master
git pull
git reset --hard upgrade
git push --force origin masterRe-add the new versions of all the missing plugins if necessary. (And then commit and push in Git).
Then:
-
Put the Moodle website in maintenance mode: visit /admin/settings.php?section=maintenancemode to do that.
-
Create a complete backup of:
-
The moodledata directory
-
The database
-
The entire code base (can usually be found in public_html, htdocs, or a subdirectory called moodle)
-
-
Alternatively, if the hosting environment supports it, you may instead create a snapshot of the entire system. (the hosting provider supports this through their KIS panel.)
-
Update the system (i.e. the web server) if necessary – this may disrupt the current (old) Moodle installation, so make absolutely sure Moodle is in maintenance mode first.
-
Also make sure there are no other websites that depend on the old version of php or the database (or whatever component you need to update on the system)
-
Make sure that the settings of the current stack are retained. For example, if php.ini for php 7.1 has upload_max_filesize = 500M, then make sure that your new php 7.2 version has the same value in the new php.ini file. Idem ditto for post_max_size and max_execution_time.
-
-
Replace the source code with the new Moodle version by replacing the local master branch with the newly updated remote master branch:
git checkout master
git fetch origin
git reset --hard origin/master
-
Copy the config.php file from the backup of the code base
-
Go to the /admin/ url of the Moodle website to check if there are any issues with the plugins
-
Go to the command line and start a Screen session
-
Perform the actual upgrade through php admin/cli/upgrade.php
-
Do some basic tests:
-
Test if Moodle is still sending out email
-
Make sure the cron job is still running properly (this can probably be done only after completing the next step)
-
-
In some cases, it may be necessary to copy backend settings from the test server (e.g. if a theme was upgraded, make sure to copy any custom css). Use the Site admin presets tool for this: /admin/tool/admin_presets/index.php. However, beware that this will take the site out of maintenance mode if you import from a site that has the setting disabled.
-
Take the Moodle website out of maintenance mode on the /admin/settings.php?section=maintenancemode page.
Once the upgrade is done successfully, contact the customer.
Performing a Minor Upgrade
Minor upgrades are typically performed when Moodle HQ issues a security alert. The email has the subject “Moodle Security Alerts” and the sender is admin@lms.example.com. The upgrade will typically be a security patch.
In such a case, it is good practice to do a minor upgrade. For some customers, there is even a contractual obligation to perform minor upgrades.
Preparing The Git Repository
Manual minor-upgrade Git operations are no longer the standard process.
For websites that should receive automatic Moodle 4.5 minor upgrades, onboarding is now done by configuration on the automation hosts:
-
Add the site to runner mapping:
-
File: /opt/solin-deploy/config/sites.json
-
Include: site, git_root, webroot, deploy_user, php_binary, systemd_service_base, backup_log_path, optional healthcheck_url
-
-
Add or update per-repo Git bot config:
-
Files: /opt/solin-deploy/config/gitbot-<repo>.config.json and /opt/solin-deploy/config/gitbot-<repo>.env
-
Use tag_glob: “v4.5.*”
-
Include the correct site repo path and branches (for example live, staging, master, or main depending on the repo)
-
-
Ensure repository token is loaded on coordinator host:
-
Runtime token file pattern: /run/solin-deploy/<repo>.token
-
Validate with: scripts/check-bitbucket-token.sh
-
-
Configure Bitbucket webhook on the managed repo:
-
URL: https://deploy-coordinator.example.internal/webhook/bitbucket
-
Event: push
-
Secret: must match SOLIN_WEBHOOK_SECRET in coordinator env
-
-
Enable timer if not already enabled:
-
solin-deploy-gitbot-<repo>-nightly.timer
-
Steps To Do The Minor Upgrade
Once a site is onboarded, the recurring minor upgrade is handled automatically by the coordinator + runner stack:
-
Nightly Git bot fetches upstream Moodle tags and selects latest eligible patch tag (v4.5.*).
-
Git bot merges that upstream tag into the configured site branch(es) and pushes.
-
Bitbucket webhook creates a deploy job in the coordinator.
-
Runner pulls the target commit, enables maintenance mode, runs admin/cli/upgrade.php –non-interactive, disables maintenance mode, and performs health checks.
-
On deployment failure, automatic rollback is executed with backup + optional Git commit restore, and an immediate rollback alert email is sent.
Why third-party plugins are not overwritten in minor upgrades
The automated minor upgrade uses a Git merge of upstream Moodle tag into the existing site branch. It does not replace the branch with a clean upstream tree.
As long as third-party plugins are committed in the site repository branch, they remain in place after the merge. In other words: plugin loss risk comes from branch history/content mistakes (for example, force-rewriting a branch without carrying custom plugin directories), not from the normal automated minor-upgrade merge process itself.
Troubleshooting
SSL / HTTPS Issue
During an upgrade I ran into the following issue: a test php script was running just fine, but whenever I tried to access Moodle itself, I was redirected to the server’s default site. In addition, https was enforced.
It turned out that the site had this setting:
This was a Moodle 3.1 site, from back when only the login pages had https.
This situation may arise when you run Moodle without an SSL certificate, for example because you’re using a temporary domain name and you want to add the certificate once you have attached the actual domain name to the same vhost.
If you find yourself in a similar situation, go to the script lib/setuplib.php and look for the initialise_fullme() function.
In this code block:
if (!empty($CFG->overridetossl {
if (strpos($CFG->wwwroot, 'http://') === 0) {
$CFG->wwwroot = str_replace(‘http:’, ‘https:’, $CFG->wwwroot);
} else {
unset_config('overridetossl');
}
}
Copy unset_config(‘overridetossl’); to the top:
unset_config('overridetossl');
if (!empty($CFG->overridetossl {
if (strpos($CFG->wwwroot, 'http://') === 0) {
$CFG->wwwroot = str_replace(‘http:’, ‘https:’, $CFG->wwwroot);
} else {
unset_config('overridetossl');
}
}
And now there won’t be a forced https redirect anymore.
Solin plans and executes Moodle upgrades with staging, rollback strategy, plugin validation, and post-release checks. Need help? Contact us.
Contact us