Moving a Moodle install to a new server and domain requires careful pre-flight checks, code and database migration, DNS updates, and configuration adjustments. This guide covers the complete procedure with emphasis on avoiding downtime.

Pre-flight check.

Before proceeding, please double check that you have proper answers to the following questions at hand. Triple-check with the lead engineer if you have any doubts because a mistake throughout this process will result in publicly visible downtime at the very least.

  • Is the Moodle website using recaptcha? Unless the Recaptcha settings are modified the DESTINATION website will not have functioning captchas thus blocking users from registering and/or logging in.
  • Do you have root access to both servers?
  • Is the SOURCE website in maintenance mode? What window of time do you have for this process? If everything goes as expected you should be done in an hour or two, but if you encounter any hiccups it will take longer.
  • Do you have admin credentials for Moodle? What about root credentials for the database?
  • Does the DESTINATION server have the same post_max_size and upload_max_filesize php settings (usually in php.ini) as the SOURCE server? Otherwise users may run into upload limits of 2 MB.
  • Does the DESTINATION server have the same locales installed? Check this with locale -a (under Ubuntu Linux).
  • Are there any logging processes on the DESTINATION server that tend to eat up a lot of disk space, such as log_bin on MySQL? If so, turn them off if possible.
  • Does the DESTINATION server have enough free disk space? Check with df -h
  • apachectl configtest to test whether the config files are properly written.
  • service apache2 reload to process changes to the configuration.
  • The web server software: Apache (version 2.2 or 2.4, depending on the host OS version). This is important because some of the syntax has changed between those releases, but we will address that later in this document.
  • The database: MySQL.
  • The Moodle website folders, located on /home/<website name>/public_html. As you can guess, each website gets their own user. This will be relevant shortly.
  • The moodledata folder, usually located on /home/<website name/moodledata, contains files that are uploaded or created by the Moodle interface. The location of this folder will be specified in /home/<website name>/public_html/config.php.
  • First of all, we need to create the user and folder structure. Let’s get the uid of the SOURCE user:
  • As we can see, the uid is 1025. We will create a user with this same uid on the new system.
  • After creating it we are going to switch users, create the folder structure and assign the proper permissions so we can transfer files later.
  • If the UID were to be already in use, don’t worry. Create the user with a different UID and remember this when we are transferring the website files over to the destination server.
  • To create a backup of the database, first we need to know which database Moodle is using. This information can be obtained from the config.php file, like so:
  • And to back it up we use the following command:
  • This folder now contains the Moodle website, the moodledata folder and the database backup we just made. To transfer it to the new server, we are going to use Rsync and ask rsync to preserve all extra attributes (owner, permissions and so forth) with the “-a” switch.
  • “-a” will take care of assigning the proper permissions to the folder, so the files are accessible for Apache too.
  • SOURCE --: rsync -ar --progress --partial /home/<user>/* root@198.51.100.42:/home/<user>/
  • This will take a while, and Rsync will keep us informed of the progress.
  • If earlier we had to create the user with a different UID, we can manually fix permissions in the DESTINATION server at this moment with the following set of commands.
  • Now that the website folder is in place, we need to add the site to Apache’s configuration. To do that, we will copy the original website from SOURCE:/etc/apache2/sites-available/<website>.conf into DESTINATION:/etc/apache2/sites-available/<website.conf>. We need to tweak some of the configuration parameters on DESTINATION, namely:
  • ServerName must match the new domain, if it changes. Same with ServerAlias (i.e. if the original website contained any ServerAlias instructions, they will have been copied over through the .conf file).
  • ErrorLog and CustomLog must have their paths modified to include the new domain name, if relevant.
  • To check for this, run apache2 -v in both the source and the destination. This will present a problem, as Apache made significant changes to the configuration directives between those two versions. You might encounter this scenario when the source server is Debian and the destination server is Ubuntu, due to their different packaging policies.
  • In this case, we are only concerned about one change: The way directory permissions are specified. We need to turn “allow from all” into “Require all granted”. The following is a snippet of an actual configuration file, before and after the change-

Before:

<Directory /home/12mprove/public_html>
Options -Indexes +IncludesNOEXEC +SymLinksIfOwnerMatch
allow from all
AllowOverride All Options=ExecCGI,Includes,IncludesNOEXEC,Indexes,MultiViews,SymLinksIfOwnerMatch
</Directory>

After:

<Directory /home/website/public_html>
Options -Indexes +IncludesNOEXEC +SymLinksIfOwnerMatch
Require all granted
AllowOverride All Options=ExecCGI,Includes,IncludesNOEXEC,Indexes,MultiViews,SymLinksIfOwnerMatch
</Directory>

If you do not check for this properly, Apache would error out when loading the new website configuration leading to downtime for every website on that server.

A full list of changes between versions is available on the website.

Creating Moodle’s cron job
  • Moodle has a scheduled task that runs every minute, and takes care of running a variety of scheduled tasks at regular intervals (like sending mail, updating Moodle reports, RSS feeds, activity completions or posting forum messages).This scheduled task needs to be added to Apache’s crontab. To do so, we run the following command:
DESTINATION --: crontab -u www-data -e
  • And this is the line we need to add (careful, it is just one line with no breaks):
*/1 * * * * /usr/bin/php  /home/12mprove/public_html/admin/cli/cron.php >/dev/null

If you’re using php-fpm (to run multiple php versions on the same server), then you’ll want to specify the exact php version:

*/1 * * * * /usr/bin/php7.2  /home/12mprove/public_html/admin/cli/cron.php >/dev/null

(It’s not necessary to use cgi-fcgi in order to run a specific php version on the command line.)

Running the “replace” script to update references to the domain name.

If we have changed the domain name, we need to run this tool so Moodle can update it’s internal references to the new domain. To do so, we run the following command on the DESTINATION server.

  • php /home/<website>/public_html/admin/tool/replace/cli/replace.php --search="<old domain>" --replace="<new domain>"
Updating the domain name in Moodle’s configuration

If the domain name has changed, you need to update /home/<website>/public_html/config.php so wwwroot points to the new domain name.

$CFG->wwwroot   = 'https://<URL>';

In addition, you will also need to clear the caches (‘purge all caches’), especially for Totara, which caches the domain name for menu items like the gear icon (or cog wheel, in British English).

Adjust Quota

If you’re using something like Virtualmin to create the vhost, adjust the quota (usually 1GB or 2GB by default) – otherwise you will soon run into the limit.

Solving Database Connection Errors

If you run into any database connection problems, they may be due to using a recent MySQL version and an older Moodle version. Here are the known issues we have run into:

  • Authentication issue when using ‘traditional’ native MySQL native password
  • Using Moodle 2.x with MySQL 8
Authentication Issue
Add default-authentication-plugin=mysql_native_password to the [mysqld] section of /etc/mysql/mysql.conf.d.

Change the authentication method for the database user:

ALTER USER example-db@localhost IDENTIFIED WITH mysql_native_password BY 'thepassword';
Using Moodle 2.x with MySQL 8

If you are using a very old Moodle version, such as 2.x, together with a newer version of MySQL, e.g. version 8, then you will need to make some changes to the source code. You will also need to address the authentication mentioned above.

Change the code in lib/dml/mysqli_native_moodle_database.php, line 523:

$sql = "SELECT column_name as `column_name`, data_type as `data_type`, character_maximum_length as `character_maximum_length`, numeric_precision as `numeric_precision`, numeric_scale as `numeric_scale`, is_nullable as `is_nullable`, column_type as `column_type`, column_default as `column_default`, column_key as `column_key`, extra as `extra`
 FROM information_schema.columns

To summarize: add aliases for every single column name.

It may also be necessary to make an additional change if you get the error message Unknown system variable 'storage_engine'.

Fix this issue by editing lib/dml/mysqli_native_moodle_database.php. Replace:

@@storage_engine

with

@@default_storage_engine

(In two places, in this case.)

There will still be a notice:

Notice: Undefined index: engine in /home/example-client/public_html/lib/dml/mysqli_native_moodle_database.php on line 173

But that will be ignored if you turn off full debugging mode.

Install a database backup script

The following is superseded by the section Configure backupvhost.php Script for Backups from “Installing a New Moodle Website on a VPS”.

Our data center (Hosting Provider) creates daily backups of the entire file system, for a window of fourteen days. This also includes the binary database files. But these can be hard to restore on another system. Therefore, we install a Bash script that creates a database dump and compresses it in one go. Here’s an example script:

#!/bin/bash
## location: /home/example-db/db-backup/example-db-backup.sh
mysqldump --single-transaction -uexample-db -p[secret_password] example-db | gzip -c > /home/example-db/db-backup/example-db.sql.gz

This script creates a zipped database dump which will be included in the data center’s daily backup. Together with the public_html and the moodledata directories, this file can be used to completely restore a working Moodle installation on another system, should the need arise.

  • “The –single-transaction flag will start a transaction before running. Rather than lock the entire database, this will let mysqldump read the database in the current state at the time of the transaction, making for a consistent data dump.”
  • https://serversforhackers.com/c/mysqldump-with-modern-mysql

Steps to get the Bash script working

  • Create a db-backup directory in the home directory.
  • Use vim or another text editor to create the [customer]-backup.sh script inside the db-backup directory.
  • Set the owner to your users: chown -R [customer]:[customer] /home/[customer]/db-backup
  • Set the permissions to 770: chmod -R 770 db-backup
  • Give the database user (example-db in this example above) the proper permissions to user mysqldump: PROCESS (‘Manage processes’ in Webmin)
  • Check that the script is actually working properly: su (change user) to [customer] then run the script manually and check the contents of the zip file.
  • If you run into a disk quota issue: set the quota to unlimited for both the user and the group.
  • Create a cron job to execute the script on a daily basis, e.g. one hour before midnight (at that moment the data center starts their own backups). Use Webmin to create the cron job, but you can also use crontab:
  • @daily /home/[customer]/db-backup/[customer]-backup.sh #Creates a daily backup of the [customer] database (as a gz file)
  • The next day, check that the script has run properly (the timestamp should be shortly before midnight).

Here’s the Bash script without a specific customer’s name:

#!/bin/bash
## location: /home/[customer]/db-backup/[customer]-backup.sh
mysqldump --single-transaction -u[customer] -p [secret_password] [customer] | gzip -c > /home/[customer]/db-backup/[customer].sql.gz

Reinstall the SSL Certificates

Typically, we have a Let’s Encrypt certificate installed for the domain, as well as the ‘www.’ version of the domain.

After migrating the site, run the following command to make sure the certificates are still in place and get renewed automatically:

certbot -d [thedomainname] -d www.[thedomainname]

(leave out the square brackets while typing in the command)

If there is no www.[thedomainname] version, skip that part.

You can test the results by visiting the website and checking that the certificate’s validity date starts today.

If you have certbot running on the old server, make sure you disable it for the old domain (unless you still need it).

Enabling the new website and testing that everything works.

  • To enable the new website, we need to run the following command on the DESTINATION server:
  • a2ensite <website>
  • service apache2 reload
  • You should be able to visit the domain now, and see the Moodle “maintenance” page. Log in with your admin credentials at https://websitedomain.com/login/ and browse through the courses. You should be able to access both courses and files, if the server is working correctly.

Test Email

Don’t forget to test the email delivery. To prevent the system from sending out emails to everyone before you’re ready, use this directive in config.php:

$CFG->divertallemailto = "youremail@yourdomain.com";
  • Now use the email test plugin to see if the system is still sending out email properly.

Disable Maintenance Mode

  • After you have checked that everything is working fine, use Administration > Site administration > Server > Maintenance mode to take the website out of maintenance mode.

Possible issues.

The embedded videos don’t work.

We use Vimeo as a video hosting service, and it is configured to only allow embedding from certain domains. If we are migrating to a new domain, we will need to add it to Vimeo’s settings so we can access the material.

  • Check with the lead engineer for more details.
The upload limits are too low.
  • Moodle allows users to upload files in a number of places. The upload limit (size of uploaded file) may be too low if the php.ini settings for the server have not been changed. You can check this under /admin/phpinfo.php – or Site administration > Server > PHP info – where you need to look for the post_max_size and upload_max_filesize settings. These should be 1000M. If they are not, change the php.ini file (you can't do that in Moodle, this is a server administration task).
Mailgun is no longer working

For Horizon, we had to switch to port 465 and ssl to get it working again, after migrating their websites to your server.

If you want to make sure that the system can send out email at all, use your own email provider (e.g. Gmail through smtp.gmail.com:465) to test.

Mailgun is no longer free. Consider using something like Mailjet.

MySQL Database Import Fails

If you can’t import a large DB dump because you encounter the following error message:

ERROR 2013 (HY000) at line 21770: Lost connection to MySQL server during query
  • Then check if you also have a memory error message somewhere. I had to edit /etc/mysql/mysql.conf.d/mysqld.cnf and change innodb_buffer_pool_size from 8589934592 to 4294967296:
  • innodb_buffer_pool_size = 4294967296

(And then restart MySQL). See also https://dba.stackexchange.com/questions/124964/error-2013-hy000-lost-connection-to-mysql-server-during-query-while-load-of-my

Switching over to the new website.

Once we have taken the new Moodle site out of maintenance, we are going to redirect visitors from the old domain to the new one. To do this, we edit /etc/apache2/sites-available/<website>.conf and add a new Redirect directive.

RedirectMatch ^/(.*)$ https://<new domain>/$1

Streaming a tarred moodledata directory from SOURCE to DESTINATION

Overview

For very large moodledata directories (tens or hundreds of gigabytes), traditional copying methods like scp or rsync can be too slow or can run into memory/disk limits. A more robust approach is to stream a tar archive over SSH, unpacking it immediately on the DESTINATION server without creating a temporary tar file. This method:

  • avoids creating a giant .tar file on disk
  • avoids double I/O (read + write on both ends)
  • is resilient and efficient
  • can be monitored with pv (pipe viewer)
  • works across servers as long as firewall rules permit SSH traffic

This section documents the complete process.

Prerequisites

Before streaming moodledata:

  • You must be logged in on the SOURCE server.
  • The DESTINATION server must allow SSH connections from the SOURCE server’s IP address. (If the firewall blocks it, the stream will freeze at the SSH step.)
  • SSH public-key authentication must be working between SOURCE and DESTINATION.
  • You should use screen so that the transfer continues even if your session disconnects.

Understanding the Components

1. tar -C /path -cf – .

This command creates a tar archive on STDOUT instead of writing to a file.

  • -C /path changes into the moodledata directory before tarring. This ensures we put only the contents into the stream, not the folder itself.
  • -c means create archive
  • -f - means write the archive to stdout
  • . means "archive everything in this directory"

Example:

tar -C /home/webroot/leren/moodledata -cf - .

This produces a byte stream representing the entire moodledata contents.

2. pv (Pipe Viewer)

pv sits between the tar creation and the ssh transmission:

tar ... | pv | ssh ...

It shows:

  • total bytes streamed
  • current throughput
  • ETA

If pv is not installed, you can remove it — the stream will still work, just without progress feedback.

3. SSH agent forwarding (ssh -A)

If you connect to SOURCE from your laptop and then connect from SOURCE DESTINATION, you often need your local SSH keys available on the SOURCE machine. Using ssh -A forwards your SSH agent:

ssh -A source.example.com

This makes your laptop’s SSH key available transparently on SOURCE, so SOURCE can authenticate to DESTINATION without storing private keys on SOURCE.

If authentication fails or the agent doesn’t forward, DESTINATION will reject the connection.

4. Unpacking on DESTINATION (tar -C /path -xf -)

On DESTINATION, we unpack the incoming stream immediately:

  • -C /path = change into the target moodledata directory
  • -x = extract
  • -f - = read the archive from stdin

No temporary file is created.

Example:

ssh solin@destination 'tar -C /var/www/moodle-prd/moodledata -xf -'

Full Command for Streaming moodledata

Below is the production-ready command, including:

  • screen
  • progress meter
  • email notification
  • automatic extraction on DESTINATION

Replace paths accordingly.

Command (run on SOURCE)
ssh -A academy-demo@academy.adventureworks.example
screen -S stream-moodledata

tar -C /home/webroot/academy-demo/academy.adventureworks.example/moodledata -cf - . \
| pv \
| ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=5 solin@149.210.215.9 \
    'tar -C /var/www/moodle-prd/moodledata.20251025 -xf -' \
; echo "moodledata transfer finished on $(hostname) at $(date -Is)" \
| mail -s "moodledata stream done" onno@your-domain.com
Explanation
  • tar -C ... -cf - . Streams the moodledata contents only (not the dir itself).
  • pv Shows progress.
  • SSH options:
  • ServerAliveInterval=60 sends a keep-alive every minute
  • ServerAliveCountMax=5 abort if 5 keep-alives fail These prevent half-open SSH hangs during long transfers.
  • tar -C /target -xf - on DESTINATION Immediately unpacks the data into the target moodledata directory.
  • Email at the end Once the tar command finishes, a small message is piped into mail for notification.

Confirming Firewall Access

If streaming hangs right after pv starts output, SSH is not connecting.

You can test manually:

ssh solin@DESTINATION_IP 'echo ok'

If this times out, firewall access is missing.

Checklist After Transfer

  • Confirm files on DESTINATION:
ls -lh /var/www/moodle-prd/moodledata.20251025
  • Adjust permissions if needed:
chown -R www-data:www-data /var/www/moodle-prd/moodledata.20251025
  • Test Moodle access and course file delivery.

Solin specializes in Moodle migrations and server moves.

Contact us