HostEurope's backup system stores 14 days of snapshots that can be selectively restored via their KIS console. This guide covers navigating the restore interface, choosing backup dates, and handling the restored files once recovered.

Introduction

Our data center, Hosting Provider, creates daily backups of the entire file system of a VPS (Virtual Private Server), 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 for each virtual host (i.e. Moodle website) that creates a database dump and compresses it in one go.

The advantage of this is that a database dump, together with the moodledata directory and Moodle’ s codebase, allows you to restore the complete Moodle installation on any system which meets the minimum requirements for the Moodle version, not just the original server.

Please see the document Installing a New Moodle Website on a VPS for more details on how the backup script works exactly.

1. Create A Restore Directory

HostEurope has the option to restore a specific directory of the file system in the backup (up to 13 days) to a directory of your choice. For instance, you can choose to restore /home/mensen/ to /restore. Either in full, or as a tar.gz file.

This means that performing a restore will take up additional disk space. But it’s also extremely easy to execute.

To create a restore directory, login to the VPS using ssh and issue a command like:

mkdir /restore

2. Select The Backup Files

First login to https://kis.hosting-provider.de/.

Navigate to The Correct VPS

Now click on Product Admin:

  • Choose Virtual Server:
  • Now pick the system on which you want to perform the restore, by clicking on the Konfigurieren button (the KIS website is originally in German, which pops up in odd places in the English version).

Finding The Right VPS

If you don’t know which system (VPS) has the Moodle installation you’re looking for, use the command nslookup with the domain name as its argument. For instance, if I want to know the IP address of academy.civic-rights.example:

engineer@opsbox:~$ nslookup academy.civic-rights.example
Server:		127.0.0.53
Address:	127.0.0.53#53

Non-authoritative answer:
Name:	academy.civic-rights.example
Address: 198.51.100.42

Select The Source for The Backup

Next to Datum des Backups (which means backup date), click on the select box and pick the right date. Please keep in mind that the backup is usually created around midnight CET.

Next, click on the Browse button next to Quell-Verzeichnis (source directory).

In the following screen, you’ll get to see the file system as it was at the time the backup was made. On your VPS systems, we always store the files for the Moodle installations in their own home directories. So click on home first:

Find the home directory of the Moodle installation you want to restore, which typically has a part of its domain name as the directory name.

Now click on the auswählen (choose) link next to the home directory’s name, mensen in this example:

3. Perform The Restore

Once you have selected the source of the backup, you’ll be presented with the following screen, on the KIS website:

Here’s a translation and clarification for the fields in the form:

  • Datum des Backups: backup date
  • Quell-Verzeichnis: source directory – this is the directory you have created on the VPS in the first step.
  • Ziel-Verzeichnis (muss existieren): target directory (must exist)
  • Benachrichtigungen an E-mail: e-mail notification – the system will send you an email once the restore is done.
  • Restore-Type: restore type – do you want to restore the target directory in its entirety (Direkt) or as a compressed file?

If you have filled out the form click Restore starten (Start restore).

The restore process will take some time (since it’s presumably copying the files from a slow backup medium).

Depending on the purpose of the restore (e.g. disaster recovery vs. restoring a single Moodle course based on a user’s request), you might want to check other SOPs on how to get the Moodle backup up and running. If the Moodle site needs to be installed on a new system, please see the document SOP Migrating a Moodle install to a new server and domain.

Once the restore is complete, don’t forget to inform your users of the results.

Considerations

Don’t look at Alpine Vista’s backup solution – it’s too complicated.

Instead, rely on a rotation scheme (e.g. as provided by /etc/logrotate.d) to remove old backups out of the window. Although this probably can’t be done on the storage server if it’s really only a file server you can only access through sftp. In that case we need something that cycles through the numbers 1 – 14. E.g.:

$backupday = $weekday;
if ($weeknumber % 2 == 0) {
  // even week number
  $backupday = $backupday + 7;
}

(The script should probably also remove yesterday’s backup before creating a new one to be copied to the remote system).

Also consider encrypting the backup file on the fly:

$ tar -zcpf - folder | gpg -c --passphrase thesecrectpassphrase > folder.tar.gz.gpg

And don’t forget to skip .git from the actual backup (when including public_html)

There are also existing scripts:

https://wiki.archlinux.org/title/duplicity

But these tend to be too complicated for our use case.

Incremental Backups

20230601

Large moodledata directories typically contain files that are added once but never changed afterward, such as video files. It doesn’t make much sense to store a complete copy of moodledata every day.

Instead, we need to create a full backup on day 1 (level 0, in tar terminology) and incrementally add to that until day 14 (or whatever the backup window is).

Notes from https://www.gnu.org/software/tar/manual/html_section/Incremental-Dumps.html and https://www.theurbanpenguin.com/incremental-backups-with-tar/:

  • You can force ‘level 0’ backups either by removing the snapshot file before running tar, or by supplying the ‘–level=0’ option
  • It is common practice to name meta files with the extension .snar, think of this as an amalgamation of snapshot and tar.

It is also possible to create differential backups: these take the full backup as the base and then subsequently store all the mutations since the full backup was made. The difference is this: incremental backups store only the mutations since the last backup, whereas differential backups may store the same file multiple times (a file that was newly added since the full backup).

This is explained quite well here: https://linuxconfig.org/how-to-create-incremental-and-differential-backups-with-tar

There seems to be a way to create incremental backups with mysqldump, but it looks cumbersome: https://www.guguweb.com/2020/01/30/mysql-incremental-backup-with-mysqldump-and-rdiff-backup/

Besides, most databases compress to a relatively small size so it doesn’t matter if they’re added 14 times in a row.

Differential Backups

20230605

We now have the following script in operation on the La Forge server (82.165.1.29):

https://bitbucket.org/exampleorg/serverbackupscripts/src/master/

This script is executed using a cron job started at different times for each vhost:

/usr/bin/php7.2 /home/backupvhost/scripts/backupvhost.php --config="/home/inm/public_html/config.php" >/dev/null

This script creates differential backup and sends the encrypted tar ball to:

 solin@scp.hidrive.strato.com:/users/solin/{$hostname}_server_backups/{$CFG->dbname}/{$CFG->dbname}.backup_day{$backupday}.tar.gz.gpg

There is a user backupvhost on the La Forge server who has an ssh key pair. The public key is stored on the Strato server to enable authentication without the need of specifying a password (there is no passphrase).

Please note: there will always be one additional full backup to make sure the incremental parts from the previous period can also be restored.

Troubleshooting

20230602

Not working:

  • INM – destination folder on Hidrive was incorrectly named
  • Leerhuis – was no longer present in backupvhost cron job
  • Civic Rights Academy – first no cron job and then option --hostname="your-vps" was missing

20230619

Not working:

  • Kwil – backup script must run under php 5.6 since it’s going to include config.php, which in turn reads some of the Moodle files. This is Moodle 2.6, so php 5.6 is needed, not php 7.2
  • Alpine Vista – not clear why backup wasn’t created overnight. Test run works fine. I’ll have to keep an eye on it.

20230620

  • Alpine Vista may still not be working: there was a fresh full backup made at 1:51 am, but today is day 2 (Tuesday, not Monday) so we would have expected a day2 backup… Let’s keep an eye on it.

20230621

  • Alpine Vista is one day behind. Other than that, everything seems to be working fine. The cron job was set to work at 1:50 am, so maybe some date in the system is still at the previous day. I did another run, resulting in a ‘backup_day03’ file. I’ve also changed the cron job to run at 3:50 am.

Mailgun handles the infrastructure complexity of transactional email: reputation management, DKIM/SPF verification, and delivery tracking. This guide covers account setup, DNS configuration, SMTP integration with Moodle, and testing delivery from both the CLI and within Moodle.

Notes

  • Through this document we will use example-training.com as a test case, however any username or credentials shown will have been modified to be intentionally invalid for obvious reasons.
  • Mailgun is no longer free. Consider using something like Mailjet.

Pre-flight check.

We use a new mailgun account for each environment. Create a new account based on the current project and make sure to save and forward the credentials to the lead engineer. Best to use a custom email address for future transfer possibilities, e.g. example-training@your-domain.com.

Verify you have the right credentials, and that you are able to login to the following services:

  • Mailgun account.
  • Moodle site administration.
  • Domain DNS settings, these might live inside the Domain Registrar’s administration panel, or they might reside in an external service (Cloudflare, for example).

Specifically, it is common for services like Mailgun and DNS settings to be protected by 2 Factor Authentication. If that is the case, you will need to coordinate with the lead engineer to find a change window with the client so they are able to forward the second factor tokens to you.

  • If you use GMail and have enabled login codes through an app or SMS, this is a good example of working 2-Factor Authentication.

It goes without saying that disabling 2-Factor Authentication is something we must not do once we are logged in. If necessary, multiple additional change windows can be requested.

Mailgun

What is Mailgun

Mailgun is an online service that provides a set of APIs for sending, receiving, tracking and storing email. It can be used through an HTTP REST API, or as a traditional SMTP server.

Mailgun handles all the complex email stuff: the software services themselves, and all the email dark arts that surround them (SpamHaus, shared IPs, IP reputation, warming up IPs, the software side of DKIM and friends). We can stick to the explicit action we want to perform: sending emails, and let Mailgun worry about the rest.We are going to use Mailgun in SMTP mode, and we will add their SMTP server to Moodle so transactional emails are sent this way.

Create an account for Mailgun

  • Go to https://signup.mailgun.com/new/signup and create an account. Use your own email address and for “company” a combination of Solin with the company name of your customer.
  • You also have to provide credit card details otherwise you can only send emails to 5 authorized recipients only. In case the account details are shared with your users, make sure that you do not share credit card details from your colleagues!
  • Save the login details of this account in your password wallet and send it also to the lead engineer.

Adding the domain to Mailgun

  • After logging into the account, enter the “Domains” section (navigation pane Sending>Domains) and click on “Add New Domain”.
  • At the “Add your domain” page fill out the following details and click on Add Domain:
  • Domain name: Always use a subdomain to prevent any problems with the root domain. Use in this case mg.example-training.com
  • US/EU: Select the location of the sending server: For all companies residing in countries except from US, select EU. For the United States, select US.
  • Create DKIM Authority: leave this checked and select the longest DKIM key that is possible.

Domain or subdomain? What is the difference?

Explained in simple terms, these domains are going to develop a reputation over time depending on the rate of email delivery and the amount of times our users click on “Mark as Spam”.

Using the root domain is possible when it doesn’t have any MX records yet. However, to prevent any problems, we will always use a subdomain when we implement Mailgun.

A longer, more detailed read on this topic is available on Mailgun’s documentation.

Now that we have added the domain, we need to prove our ownership of it to Mailgun.

Proving to Mailgun that we own the domain.

Mailgun allows us to send email from any address and domain, as long as we can verify them. To do so, we will add specific TXT records through the DNS server of the domain. These records are generated and shown as soon as you click on the “Add Domain” button. You can also find these records by navigating to Sending>Domain Settings and tab DNS records. Make sure you select the correct domain on top of the page.

The DNS settings might live inside the domain registrar’s interface, or they might reside in an external service if this guide uses other services to manage the DNS (Cloudflare, for example).

Finding out who manages the DNS servers for the domain

Linux: Use the dig command.

$ dig +short NS example-training.com

clint.ns.cloudflare.com.

dorthy.ns.cloudflare.com.

Windows: Use nslookup

nslookup -type=ns example-training.com

Server:4.2.2.3Address:4.2.2.3#53Non-authoritative answer:example-training.comnameserver = clint.ns.cloudflare.com.example-training.comnameserver = dorthy.ns.cloudflare.com.

You can also use an online tool to find the hosting details: https://hostingchecker.com/

The DNS server for example-training.com is managed by Cloudflare, which means we will need credentials to access your users’s Cloudflare account.

Adding TXT records to the domain’s DNS settings

In this particular case, we are going to modify Cloudflare’s DNS settings. The process will be similar with other providers, and Mailgun offers helpful manuals for GoDaddy, NameCheap, Network Solutions, Rackspace Email & Apps, Rackspace Cloud DNS, Amazon Route 53 and Digital Ocean. Consult each provider’s help section if you have doubts about this.

For more general instructions, go to https://help.mailgun.com/hc/en-us/articles/360026833053-Domain-Verification-Walkthrough

  • Login to your hosting provider. This account might be applicable for several domains.
  • Select the domain of the Moodle website.
  • Navigate to the DNS section. Look for a section called Advanced domain settings or Manage DNS.
  • Add all records with the type, hostname and value that Mailgun has provided:
  • Add two separate sending TXT records, one for SPF and one for DKIM. These are required to send and receive email with Mailgun.
  • Add two separate receiving MX records. Use for priority 10.
  • Add a tracking CNAME record. The CNAME record is what enables Open and Click tracking. This is highly recommended to reap the full benefit of Mailgun and gain insights into your email performance.
  • Once you have added all records, you can click in Mailgun at the DNS records page on the button “Check DNS Records Now” to verify the records. If they have been verified, you see a green check sign in front of the record.
  • You will also receive an email from Mailgun once the DNS records have been verified.

If you are not familiar with DNS’ innerworkings, it might surprise you to read that this change might take up to 48 hours to propagate. Basically, TTL is the amount of time that records will be kept in cache throughout the whole DNS hierarchy; from the root servers to the servers that you use as DNS servers (such as 8.8.8.8).

GoDaddy “Helps” by Adding Your Full Domain in Name Part

Please note that GoDaddy doesn't clearly tell you that it automatically appends the domain name to the "Name" field — which leads to lots of confusion, especially when you're used to entering full FQDNs (Fully Qualified Domain Names) elsewhere.In short, when using GoDaddy’s interface, do not enter the full domain name, just the subdomain you’re editing.For example, if you have this DNS record:

  • then you should enter this as “mail”.

Integrating Mailgun with Moodle.

Now that we can send emails as whatever@example-training.com, it’s time to configure Moodle to send email through Mailgun.

Obtaining the credentials from Mailgun

Beware: Anyone with the password and SMTP login will be able to send email as <anyone>@domain.com. Treat these credentials with utmost care.

Configuring Moodle

  • Go to the Moodle website and log in as an Administrator.
  • Click on Site Administration> Server> Outgoing mail configuration
  • Fill in the fields with the information obtained in the previous step, and click save.

If you want to have SMTP security enabled, please select the appropriate security mode.

For secure connections, port 465 is usually used with SSL, port 587 is usually used with TLS.

Done! Moodle is now configured to send email through Mailgun. Let’s test this setup, to confirm everything is working.

Send a test email.

From the shell.

Using swaks (available in the Debian/Ubuntu repositories) we are going to send a test email that will come from onno@example-training.com.

$ swaks --auth --server smtp.mailgun.org --au postmaster@example-training.com --ap <smtp password> --from onno@example-training.com --to email@yourinbox.com --h-Subject: "Hello" Test
=== Trying smtp.mailgun.org:25...
=== Connected to smtp.mailgun.org.
<-  220 ak47 ESMTP ready
 -> EHLO MailDemo
<-  250-ak47
<-  250-AUTH PLAIN LOGIN
<-  250-SIZE 52428800
<-  250-8BITMIME
<-  250-ENHANCEDSTNorthwind UniversitySCODES
<-  250-SMTPUTF8
<-  250 STARTTLS
 -> AUTH LOGIN
[..]
<-  235 2.0.0 OK
 -> MAIL FROM:<onno@example-training.com>
<-  250 Sender address accepted
 -> RCPT TO:<email@yourinbox.com>
<-  250 Recipient address accepted
 -> DATA
<-  354 Continue
 -> Date: Sun, 04 Feb 2018 20:53:12 +0100
 -> To: email@yourinbox.com
 -> From: onno@example-training.com
 -> Subject: Hello
 -> X-Mailer: swaks v20130209.0 jetmore.org/john/code/swaks/
 -> 
 -> This is a test mailing
 -> 
 -> .
<-  250 Great success
 -> QUIT
<-  221 See you later. Yours truly, Mailgun
=== Connection closed with remote host.

From Moodle.

(Note: if the plugin is not installed, you can install it from https://moodle.org/plugins/local_mailtest. However, be mindful of your users’s environment! If you have any doubts about this step, please check with the lead engineer before proceeding.)

Go to Server/eMail Test and fill in the fields.

Moodle will show you exactly what happened, and if the email was sent successfully or not. Debug mode settings do not affect the rest of the website, and detailed output is always shown on error regardless of the debug settings specified in Moodle’s config.php.

Troubleshooting

SMTP ERROR: Failed to connect to server.

Either:

  • You have selected SSL in the SMTP settings, and it should be disabled since this guide uses plaintext auth.
  • The service provider is blocking port 25, 465 or 487.

If it’s due to port blocking, Mailgun has an extensive article on this issue and how to work around it.

  • TL;DR: Use port 2525 instead.

To Do: Add Section on DMARC

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.

Testing that Moodle plugins correctly post data to remote endpoints requires a local test server to capture requests. This guide shows how to set up a simple PHP endpoint, configure the plugin, and inspect the actual HTTP payloads being sent.

SOP: Testing Moodle Plugins That Transmit Data to a Remote Endpoint

Purpose

To verify that a Moodle plugin correctly sends data (e.g., grades) to a remote web service (typically via HTTP POST) when specific events are triggered, such as grading an assignment.

Prerequisites

  • Moodle instance with the plugin installed and enabled
  • Access to Moodle server (for grading & logs)
  • PHP CLI available (7.4+)
  • A terminal:
  • Linux/macOS: Bash or Zsh
  • Windows: PowerShell or CMD
  • Optional: curl or Postman for manual POSTs
  • Step-by-Step Procedure
  • Step 1: Set Up a Fake Local Endpoint
  • UNIX-like systems (Linux/macOS)
  • Create a test directory:mkdir -p ~/php/wstest
  • cd ~/php/wstest
  • Create a file called index.php:<?php
  • ob_start();
  • echo "=== REQUEST METHOD: " . $_SERVER['REQUEST_METHOD'] . " ===\n";
  • foreach (getallheaders() as $name => $value) {
  • echo "$name: $value\n";
  • }
  • echo "\n";
  • $input = file_get_contents('php://input');
  • echo "=== BODY ===\n";
  • echo $input . "\n";
  • $json = json_decode($input, true);
  • if ($json !== null) {
  • echo "\n=== PARSED JSON ===\n";
  • print_r($json);
  • }
  • file_put_contents('php://stdout', "=== LOG FROM index.php ===\n" . ob_get_contents() . "\n");
  • ob_end_flush();
  • Start the PHP test server:php -S localhost:8000
  • Keep this terminal open — you'll see incoming requests printed here.
  • 🪟 Windows (for testers)
  • Install PHP
  • Download from https://windows.php.net/
  • Extract to C:\php
  • Add C:\php to your PATH
  • Create a test script
  • Create folder: C:\php\wstest
  • Create file: index.php (same contents as above)
  • Start server in CMD or PowerShell:cd C:\php\wstest
  • php -S localhost:8000
  • Step 2: Point Moodle to the Local Endpoint
  • In the plugin settings, set the endpoint to your local test server:http://localhost:8000
  • Ensure your plugin's event observer is correctly registered. Example (your plugin may differ):'callback' => '\local_yourplugin\observer::graded'
  • Confirm that your observer method exists and is properly namespaced. Example method signature:public static function graded($eventdata) {
  • // your handling code
  • }
  • Step 3: Trigger the Event
  • Grade an assignment or perform any action that triggers the event
  • Watch your terminal window

Example terminal output:

  • === LOG FROM index.php ===
  • === REQUEST METHOD: POST ===
  • Content-Type: application/json
  • === BODY ===
  • {"Cijfer":"77.00000","Cijfertype":"point","OpdrachtID":"15030","GebruikerID":"145404"}
  • === PARSED JSON ===
  • Array
  • (
  • [Cijfer] => 77.00000
  • [Cijfertype] => point
  • [OpdrachtID] => 15030
  • [GebruikerID] => 145404
  • )
  • Step 4: Debug If Nothing Appears
  • Check that:
  • The plugin is enabled
  • The correct Moodle event is triggered
  • The callback matches your observer class and method
  • Add debug logging with: Example:error_log("Triggered observer::graded()");
  • Check web server logs
  • Ensure your curl call includes: Example:CURLOPT_RETURNTRANSFER => true
  • Success Criteria
  • Endpoint receives a POST request
  • Data includes expected keys and values
  • No errors are logged in Moodle or PHP logs
  • Cleanup
  • Press Ctrl+C to stop the test server
  • Revert endpoint configuration if needed

Building AMD modules in Moodle plugins using esbuild avoids Grunt's complexity while maintaining full compatibility. This guide covers setup, configuration, and the complete build-to-production workflow.

Standard Operating Procedure: Using esbuild to Build AMD Modules in a Moodle Plugin

Purpose

To compile modern JavaScript source files in amd/src/ into AMD-compatible .min.js files in amd/build/ using esbuild, in a way that's fully Moodle-compliant but avoids Grunt.

Directory Structure

Your Moodle plugin should contain:

mod/yourplugin/
 amd/
    src/
       editor.js
       runtime.js
    build/      (created automatically by esbuild)

Each source file should begin with a valid AMD define() block:

// amd/src/editor.js
define(['jquery'], function($) {
   return {
        init: function(cfg) {
            console.log('Hello from editor.js', cfg);
        }
    };
});

Step 1: Initialize NPM (once)

From your plugin root:

npm init -y

This creates a package.json file.

Step 2: Install esbuild

npm install --save-dev esbuild

This installs esbuild locally into your plugin.

Step 3: Create build.js file

Create a file named build.js in the plugin root:

touch build.js

Paste in:

// build.js
const esbuild = require('esbuild');
esbuild.build({
  entryPoints: ['amd/src/editor.js', 'amd/src/runtime.js'],
  outdir: 'amd/build',
  outExtension: { '.js': '.min.js' },format: 'iife', // Moodle-compatible format
  bundle: false,
  minify: true,target: ['es2015']}).then) => {
  console.log(' JS build complete.');
}).catcherr) => {
  console.error(' Build failed:', err);
  process.exit(1);
});

Step 4: Run the build

From your plugin root:

node build.js

You should see:

JS build complete.

Then verify:

ls amd/build/

You should see:

editor.min.js
runtime.min.js

Optional: Add NPM build script

Edit package.json to add:

"scripts": {
  "build": "node build.js"
}

Now you can build with:

npm run build

TL;DR Summary

  • npm init -y
  • npm install --save-dev esbuild
  • Create build.js with AMD-compatible config
  • Run node build.js or npm run build
  • Moodle will load your AMD modules from amd/build/*.min.js

Setting up a new Moodle instance on a VPS requires careful configuration of DNS, virtual hosts, and web server settings. This guide covers the complete workflow from domain setup through initial Moodle installation.

1. Create a DNS record

There are two options here (described in more detail below):

  • The customer has access to their own DNS server or provides you with access data: link their preferred (sub)domain to the ip address of your VPS server.
  • Create a DNS Record for a Subdomain under Our Domain: this means that you have to adjust the DNS settings for one of your own domains, to link a subdomain to the VPS.

Link the Customer’s Preferred (sub)domain to the IP Address of your VPS Server

If your users already has a (sub)domain name for their new Moodle installation, you have to go into their own DNS server and create the necessary changes. Since there’s no telling what DNS server or provider they’re using, the exact steps vary by DNS provider.

The outcome should usually be an A record that links their (sub)domain to the ip address of your server:

lms.westwood.com. IN A 123.456.789.101

In short: their (sub)domain should be link to our ip address.

Create a DNS Record for a Subdomain under Our Domain

2. Create a VirtualHost

Log in to Virtualmin on the your-vps server: https://198.51.100.42:10000/?virtualmin.

Then select “Create Virtual Server”:

Input the details for the new virtualhost:

Please note:

  • Use the subdomain as the Administration username (use the “Custom username” feature). E.g. if the domain is westwood.example-lms.com, then ‘westwood’ becomes the Administration username.
  • Create a strong, random password and store it in a very secure password wallet (e.g. KeePassX).
  • If you use Virtualmin, be sure to adapt the quota (usually 1GB by default) – otherwise larger Moodle installations will run into the limit. (MySQL server will even crash entirely if you try to import data beyond the quota limit. I once had to manually delete the database directory from /var/lib/mysql/ to get it working again, because the MySQL server wouldn’t even start in recovery mode.) To double check whether a quota limit applies:
  • root@blueyondercoaching:~# quota -u example-db
  • Disk quotas for user example-db (uid 1020): none
  • If the database creation fails for some reason, simply use: create database mydatabasename character set utf8mb4 collate utf8mb4_unicode_ci;

3. Set the Database Password

After completing the previous step, click “Create Server” (see previous screenshot).

If the virtual server has been set up, click on “Webmin” in the top left corner of the window:

In the blue panel on the left, select “Servers” > “MySQL Database Server. Then scroll down and click “User Permissions” (Under “Global Options”)::

Then click on the database user with the name of the new subdomain (i.e. the Administration username), Westwood in this example:

Now type in a strong, random password and store that password in your password wallet (e.g. KeePassX):

Click “Save” to complete this step.

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.

For Totara: Grant Permissions to Create Tables & Indexes

If you’re installing Totara, please make sure the database user has adequate permissions to create caching tables and indexes. (For the report builder.)

Using The MySQL Client To Create Database And DB User

Instead of using Webmin, you can also use the commandline tool mysql:

mysql -u root -p
CREATE DATABASE moodle_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'moodle_user'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT ALL PRIVILEGES ON moodle_db.* TO 'moodle_user'@'localhost';
FLUSH PRIVILEGES;

4. Create a Git Repository

Before we install or deploy the code for the new Moodle website, we need to set up a Git repository. In this guide this guide uses Bitbucket to store the remote repository, but any other system is fine too (e.g. Gitlab or Github).

New Bitbucket Repository

On Bitbucket.com, use the ‘Create’ button to add a new repository:

Bitbucket will also ask you to create a Project. Lately, I’ve made it a habit to use your users’s name for the project, and your users’s name for their platform (Moodle or Totara) as the name for the repository.

Clone Repo and Get the Moodle Code

On your local machine, clone the repo. Then add the Moodle repo as the upstream repo:

git remote add upstream https://github.com/moodle/moodle.git

Get a specific branch. Here, we’re pulling the branch containing Moodle 4.1 because it is the current LTS version (the upstream branch name can be found in the upstream repo):

git pull upstream MOODLE_401_STABLE

Create a local branch based on the upstream branch:

git checkout upstream/MOODLE_401_STABLE
git switch -c moodle41

Go to master branch:

git checkout master

(Bitbucket uses main by default nowadays.)

Then copy all the files from moodle41 to the current branch:

git checkout moodle41 .

Yes, this looks like we’re switching to another branch altogether, but notice the dot. This apparently means: get the files from branch moodle41 and put them in the currently checked out branch (which is the master branch in this example).

After you’re done, commit and push the changes to remote.

Please note that we can’t add the Totara source code in this way because Totara is not really open source: there is no publicly available Git repository for it.

Caveat: 3rd Party Plugins

Please note: the method described above also works if you allow users to install their plugins (through the standard Moodle interface). But you wouldn’t have the complete code base in your git repo of course.

It’s probably a better idea to use git submodules if you want to be able to upgrade 3rd party (“additional”) plugins through git as well. See the explanation here: https://docs.moodle.org/401/en/Git_for_Administrators#Maintaining_Git_submodules.

However, this still needs to be executed on your local machine since it requires a commit afterwards. From the link above:

$ cd /path/to/your/moodle
$ git submodule foreach git pull

The command git submodule foreach [another command] walks through all submodule repositiories and executes what is specified by [another command]. In this case it is git pull. Therefore, the module mylittleextension was updated and the main repository isn't clean anymore until changes are committed:

$ git commit -a -m "Plugin updates"

Since the git users on the live servers do not have write access to the repository, we can’t do the above on the live server itself. (We don’t want to give them write access because their private ssh keys are on the web server, accessible for everyone on that server).

Add Public Key of Vhost’s Unix User to Bitbucket

We are going to use Git to deploy the new code (and also later, e.g. for minor and major Moodle upgrades). Login on the web server and:

  • su to the unix user for the website
  • Execute the following command: ssh-keygen
  • Skip the passphrase – we don’t want to use a passphrase (but only in this case)
  • Put the public key in Bitbucket repo through “Repository settings > Access keys”
  • Provide a clear label, e.g. “mbcc user on your-vps”
  • Clone the repository: ‘git clone’. Make sure the directory containing the git repository is the webroot (usually called public_html or htdocs)
  • Set the permissions, e.g.: chmod -R 755 public_html
  • Secure the .git folder by doing a chmod -R 700 .git (we’ve had complaints from security officers that the .git folder was public, even though what’s in it is typically public anyway since it’s open source software).

5. Install & Configure Moodle

  • Make sure the (sub)domain can be reached, e.g.:

You will get an error message ‘Forbidden’, but that’s okay since there’s no actual website yet. If something went wrong, you’ll see a different message, e.g.: “This site can’t be reached”.

  • Login to the VPS, e.g.:
engineer@opsbox-computer14:~$ ssh sysadmin@198.51.100.42
Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-042stab123.9 x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage
Last login: Sun Aug 27 08:34:07 2017 from 83.162.44.210
sysadmin@yourserver:~$ 
  • Move to your users’s home directory:
sysadmin@yourserver:~$ cd /home/westwood
sysadmin@yourserver:/home/westwood$ 
  • Create a moodledata directory:
sysadmin@yourserver:/home/westwood$ sudo mkdir moodledata
  • If you have deployed the code through Git, which is the preferred way, you can skip ahead to step 11: Make a copy of public_html/config-dist.php. Otherwise, go to https://download.moodle.org/releases/latest/ and click on the download button for the package that is built weekly:
  • (Cancel the download to your local machine.) Copy the download link:
  • Use wget to download Moodle: sudo wget [the link]
sysadmin@yourserver:/home/westwood$ sudo wget https://download.moodle.org/download.php/direct/stable33/moodle-latest-33.tgz
--2017-08-27 08:58:10--  https://download.moodle.org/download.php/direct/stable33/moodle-latest-33.tgz
Resolving download.moodle.org (download.moodle.org)... 2400:cb00:2048:1::6814:ef, 2400:cb00:2048:1::6814:1ef, 104.20.0.239, ...
Connecting to download.moodle.org (download.moodle.org)|2400:cb00:2048:1::6814:ef|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 44163286 (42M) [application/g-zip]
Saving to: 'moodle-latest-33.tgz'

moodle-latest-33.tgz                                 100%[======================================================================================================================>]  42.12M  39.8MB/s    in 1.1s    

2017-08-27 08:58:11 (39.8 MB/s) - 'moodle-latest-33.tgz' saved [44163286/44163286]

sysadmin@yourserver:/home/westwood$ 
  • Rename the public_html to public_html.bak:
sysadmin@yourserver:/home/westwood$ sudo mv public_html public_html.bak
  • Extract the moodle code base:
sysadmin@yourserver:/home/westwood$ sudo tar -xf moodle-latest-33.tgz
  • Rename to public_html:
sysadmin@yourserver:/home/westwood$ sudo mv moodle public_html
  • Make a copy of public_html/config-dist.php:
sudo cp public_html/config-dist.php public_html/config.php
  • Edit the contents of public_html/config.php with your text editor, e.g. vim:
$CFG->dbtype    = 'mysqli';      // 'pgsql', 'mariadb', 'mysqli', 'mssql', 'sqlsrv' or 
$CFG->dblibrary = 'native';     // 'native' only at the moment
$CFG->dbhost    = 'localhost';  // eg 'localhost' or 'db.isp.com' or IP
$CFG->dbname    = 'westwood';     // database name, eg moodle
$CFG->dbuser    = 'westwood';   // your database username
$CFG->dbpass    = 'the_secret_password';   // your database password
$CFG->prefix    = 'mdl_';       // prefix to use for all table names
$CFG->dboptions = array(
    'dbpersist' => false,       // should persistent database connections be
    'dbsocket'  => false,       // should connection via UNIX socket be used?
    'dbport'    => '',          // the TCP port number to use when connecting
    'dbhandlesoptions' => false,// On PostgreSQL poolers like pgbouncer don't
    'dbcollation' => 'utf8mb4_unicode_ci', // MySQL has partial and full UTF-8
);
$CFG->wwwroot   = 'http://westwood.example-lms.com';
$CFG->dataroot  = '/home/westwood/moodledata';
  • The lines that need changing are highlighted. This is an excerpt from a config.php file, not a complete config.php file.. Do not replace the code of your config.php file with this code, but make the changes in the file itself (new code may have been added by the Moodle core team).

Secure the Moodle Installation

  • Make sure the directories have the right permissions and ownership (for the username, we’re following the ‘westwood’ example – change this to match the client’s username):
  • sudo chown -R westwood:www-data public_html
  • sudo chmod -R 0755 public_html
  • sudo chown -R westwood:www-data moodledata
  • sudo chmod -R 0750 moodledata
  • Also consider closing public access to the .git folder if it’s in your webroot. Even though most of the code is open source, some clients are still paranoid about it.
  • Go to the website to actually install Moodle. It should redirect you to /admin/index.php:
  • Just follow the installation wizard from here on (ignore any notifications about https).
  • In the installation form (see screenshot below):
  • The username should be either siteadmin, yoursiteadmin or example-siteadmin (again, whatever is appropriate).
  • Use admin@example-lms.com, admin@your-domain.com or admin@example.com as the admin’s email address (whatever is appropriate, e.g. if the site is Frooxl’s then it should be admin@example-lms.com).
  • Set Email display to “Hide my email address from everyone”.
  • For the Front page settings, use the subdomain name (or your users’s chosen name for the LMS) as the “Full site name” and the “Short name for site”. Set the “Default timezone” to whatever timezone the site is in.

Create an Admin Account

  • In the panel (menu) on the left, click on “Site adminstration”, then select the “Users” tab. Click the “Add a new user” link.
  • Fill out the minimally required fields (see screenshot below):
  • Username: your users’s company name, followed by ‘admin’. E.g.: westwoodadmin.
  • Generate a strong, random password and store it securely (e.g. in KeePassX), together with the url and the username. Also fill in the password in the current Moodle form.
  • Use ‘Admin’ as the first name and the company name as the last name (e.g. “Westwood”).
  • If you’ve got a role based email address, use that, preferably the admin address – e.g. admin@westwood.com.
  • Add the new customer’s account as a site administrator. Select “Site administration” > “Users” > “Site administrators” (see screenshot below):
  • Select your users’s user and click “Add”:
  • Afterwards, the form should look like this:
  • (The customer’s account should appear under “Current site administrators”).

Add Outgoing Mail Configuration

Go to admin/settings.php?section=outgoingmailconfig and either:

  • Add your users’s SMTP settings
  • Or create a Mailgun account for your users (see Mailgun and Moodle Integration).

Change Upload Limits

By default, Moodle's upload limits are just one or two measly MBs. You need to change this upload limit to 1000 MB in a number of places:

  • On the server: in the php.ini file, you need to set post_max_size and upload_max_filesize to 1000M (this is typically already done, you can check this under /admin/phpinfo.php – or Site administration > Server > PHP info)
  • Under Site security settings (Site administration > Site security settings), where you need to select 1000 MB for "Maximum uploaded file size".
  • And finally for individual modules, by default at least the assignment module (mod assign), under /admin/settings.php?section=assignsubmission_file (go to Site administration > Plugins, select "File submissions"). This setting, again, needs to be 1000 MB.

Turn Off Update Notifications

Out of the box, Moodle will send out update notifications by email whenever a new version is available. This is alarming for some customers because they tend to think the software is outdated. Turn this feature off:

Site administration > Server > Update notifications

Disable the checkbox “Automatically check for available updates”.

Enable Apache X-Sendfile for large file delivery

Large Moodle sites usually serve videos, H5P files, SCORM packages, PDFs, and large images. By default these files are streamed through PHP-FPM, which reduces performance and limits concurrency. Enabling Apache’s X-Sendfile offloads file delivery to Apache, which is significantly faster.

Step 1: Install and enable mod_xsendfile

apt update
apt install libapache2-mod-xsendfile
a2enmod xsendfile
service apache2 restart

Step 2: Add X-Sendfile directives to the VirtualHost

Inside the VirtualHost configuration:

<IfModule mod_xsendfile.c>
    XSendFile on
    XSendFilePath /home/USERNAME/moodledata
    XSendFilePath /home/USERNAME/public_html
</IfModule>

Reload:

service apache2 reload

Step 3: Configure Moodle

Add this to config.php before the setup include:

$CFG->xsendfile = 'X-Sendfile';
$CFG->xsendfilealiases = array();
  • Default for standard Apache Moodle sites: use X-Sendfile and leave $CFG->xsendfilealiases = [];
  • If logs or an X-Sendfile probe show aliased paths such as /dataroot/..., add the required alias mapping for that site.

Step 4: Basic Apache test

Create:

xsendtest.php:
<?php
header("X-Sendfile: /home/USERNAME/moodledata/xsend-test.txt");
header("Content-Type: text/plain");
header("Content-Disposition: inline; filename=\"xsend-test.txt\"");
exit;

Create the file:

echo "hello xsendfile" > /home/USERNAME/moodledata/xsend-test.txt

Test:

curl -i https://domain/xsendtest.php

If the file downloads correctly, Apache X-Sendfile works.

Aside: If you later observe slow requests

If the site suffers performance issues when users view large files, verify X-Sendfile is actually in use by running the Moodle-level probe described in the troubleshooting guide.

When X-Sendfile is active, PHP-FPM usage for large files drops dramatically and site responsiveness improves.

6. Create a Cron Job

On your users’s Moodle website, select: “Site administration” > “Site administration” (tab) > “Site policies”:

On the next page, scroll down and:

  • Disable “Cron execution via command line only”.
  • Set a strong, random password for “Cron password for remote access”. But do not use any special characters in the cron password, because this password is used in a querystring parameter (i.e. in a url). Store the password in secure location (e.g. KeePassX).

Now test the url in your browser. In this example, the url is: http://westwood.example-lms.com/admin/cron.php?password=your_cron_password

Go to Webmin and select “System” > “Scheduled Cron Jobs”. Click on one of the existing cron jobs, e.g. the one for example.com:

wget -q -O /dev/null http://www.example.com/admin/cron.php

In the “Command” field, replace the original url (example.com… in this example) with the new customer’s url. Then click on “Create” at the bottom of the screen.

7. Configure backupvhost.php Script for Backups

A simple solution for creating differential backups. Here is the script we are using:

https://bitbucket.org/exampleorg/serverbackupscripts/src/master/

This script is executed using a cron job started at different times for each vhost:

/usr/bin/php7.2 /home/backupvhost/scripts/backupvhost.php --config="/home/inm/public_html/config.php" >/dev/null

The script creates differential backups and sends the encrypted tar ball to:

 solin@scp.hidrive.strato.com:/users/solin/{$hostname}_server_backups/{$CFG->dbname}/{$CFG->dbname}.backup_day{$backupday}.tar.gz.gpg

On each server, there is a user backupvhost who has an ssh key pair. The public key is stored on the Strato server to enable authentication without the need of specifying a password (there is no passphrase).

Please note: there will always be one additional full backup to make sure the incremental parts from the previous period can also be restored.

Installation of Backup Script on Server

To get the backup script working on a new server, follow the steps below (see the Git repo for the most recent version of these instructions). If you already have the backup script installed on the server, skip to the next section.

  • Create a 'backupvhost' unix user on the web server.
  • Add the 'backupvhost' user to the user group of the vhost's unix user (add vhost unix user to backupvhost’s secondary group in Webmin)
  • Generate a public / private ssh key pair for the backupvhost user.
  • Add the public key to the git repo for the backup script.
  • Install this script in a 'scripts' folder in the home directory of the backupvhost user using git
  • Create a backup directory in the home directory of the backupvhost user. You should have the following files and folders now:
  • /home/backupvhost/scripts/backupvhost.php/home/backupvhost/backup/
  • Add the public key of the backupvhost user to the destination server.
  • Initiate a very small scp copy to the destination server and answer yes on the following question:The authenticity of host 'scp.hidrive.strato.com (85.214.3.70)' can't be established. ED25519 key fingerprint is SHA256:RZwN5ypH59Jssd3qVhuAnCn3fEtveYCrF24igD8QDBs. This key is not known by any other names Are you sure you want to continue connecting (yes/no/[fingerprint])?
  • Add a folder {source hostname}_server_backups on the destination server.
  • On the source server, grant the right permissions to the script:
  • chmod 770 scripts/backupvhost.php
  • chown backupvhost scripts/backupvhost.php
  • Create a folder /users/solin/{source hostname}_server_backups/{databasename}/ on the destination server for each vhost that you want to backup.
  • Test the script manually. Remove the results afterwards from the destination server. Calling the script should look something like this:
  • /usr/bin/php7.2 /home/backupvhost/scripts/backupvhost.php --config="/home/{unix user for vhost}/public_html/config.php" --hostname="{source hostname}" >/dev/null
  • Run this script as user 'backupvhost', typically as a daily cron job (choose a time around midnight, but pay attention to other backups jobs so as not to tax the system too much at any given time).
  • Check the backup results the following day on the destination server.

Creating a Cron Job to Call the Backup Script for a New Moodle Installation

To get the backup script working for a new Moodle installation, follow these steps:

  • Create a folder /users/solin/{source hostname}_server_backups/{databasename}/ on the destination server (currently my.hidrive.com) for the vhost that you want to backup. (source hostname is typically yyour server’s name).
  • Add vhost unix user to backupvhost’s secondary group in Webmin.
  • Do the same for www-data and set the correct permissions e.g.:usermod -a -G www-data backupvhostchmod -R g+rX /home/example-db/moodledata
  • Test the script manually (through a screen session). Remove the results from the destination server afterwards. Calling the script should look something like this:
  • su backupvhost;
  • /usr/bin/php7.2 /home/backupvhost/scripts/backupvhost.php --config="/home/{unix user for vhost}/public_html/config.php" --hostname="{source hostname}" >/dev/null
  • Run this script as user 'backupvhost', typically as a daily cron job (choose a time around midnight, but pay attention to other backups jobs so as not to tax the system too much at the same time).
  • Check the backup results the following day on the destination server.

Previously: Install a database backup script [Out of Date]

Our data center (Hosting Provider) used to create daily backups of the entire file system, for a window of fourteen days. They don’t do that anymore, but here is how we used the daily backups in the past.

The daily backups also include 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. Please see the document Restoring a Moodle Backup from HostEurope for more details on how to restore a backup.

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

8. Add Monitoring

To check that the website is up at all times, we add a “Remote HTTP Service” monitor to a Webmin installation. This should be done on another web server than the one hosting the current Moodle site.

Go to Tools > System and Server Status and click on the button Add monitor of type, while HTTP Request is selected. This should take you to the following screen:

Set the field “Connection timeout” to 10 seconds. This should also notify you if the loading times for the Moodle website get unacceptable (i.e. more than 10 seconds).

9. Share Access Credentials

If the Moodle installation is completely ready, and no further content editing is required, notify your users directly. In your email, include:

  • Url for the new Moodle installation
  • Username
  • Password

At any rate, send the company’s Moodle admin account (username and password) for this installation to the lead developer (the lead engineer, at the time of writing), including MySQL's password, the Unix password and the url. Be sure to do that in a secure way.

Large Moodle clones can consume enormous amounts of disk space and time. This guide identifies which tables and directories can safely be skipped for development or testing, and shows how to recreate missing tables afterward.

What to Skip When Cloning Large Sites

Some customers have very large Moodle installations, with literally hundreds of Gigabytes worth of data. In that case, you can skip:

  • The moodledata directory (you won’t be able to see any uploaded files such as scorms, videos or pictures though)
  • The mdl_files table when creating a database dump, as well as
  • the mdl_logstore_standard_log table

Recreating the Omitted Tables

If you’ve got a database dump without the mdl_files and mdl_logstore_standard_log tables, you need to recreate them in your own copy of the database. Use these SQL statements to do so:

CREATE TABLE `mdl_files` (
  `id` bigint(10) NOT NULL,
  `contenthash` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `pathnamehash` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `contextid` bigint(10) NOT NULL,
  `component` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `filearea` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `itemid` bigint(10) NOT NULL,
  `filepath` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `userid` bigint(10) DEFAULT NULL,
  `filesize` bigint(10) NOT NULL,
  `mimetype` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `status` bigint(10) NOT NULL DEFAULT '0',
  `source` longtext COLLATE utf8mb4_unicode_ci,
  `author` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `license` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `timecreated` bigint(10) NOT NULL,
  `timemodified` bigint(10) NOT NULL,
  `sortorder` bigint(10) NOT NULL DEFAULT '0',
  `referencefileid` bigint(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='description of files, content is stored in sha1 file pool' ROW_FORMAT=COMPRESSED;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `mdl_files`
--
ALTER TABLE `mdl_files`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `mdl_file_pat_uix` (`pathnamehash`),
  ADD KEY `mdl_file_comfilconite_ix` (`component`,`filearea`,`contextid`,`itemid`),
  ADD KEY `mdl_file_con_ix` (`contenthash`),
  ADD KEY `mdl_file_lic_ix` (`license`),
  ADD KEY `mdl_file_con2_ix` (`contextid`),
  ADD KEY `mdl_file_use_ix` (`userid`),
  ADD KEY `mdl_file_ref_ix` (`referencefileid`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `mdl_files`
--
ALTER TABLE `mdl_files`
  MODIFY `id` bigint(10) NOT NULL AUTO_INCREMENT;




CREATE TABLE `mdl_logstore_standard_log` (
  `id` bigint(10) NOT NULL,
  `eventname` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `component` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `action` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `target` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `objecttable` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `objectid` bigint(10) DEFAULT NULL,
  `crud` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `edulevel` tinyint(1) NOT NULL,
  `contextid` bigint(10) NOT NULL,
  `contextlevel` bigint(10) NOT NULL,
  `contextinstanceid` bigint(10) NOT NULL,
  `userid` bigint(10) NOT NULL,
  `courseid` bigint(10) DEFAULT NULL,
  `relateduserid` bigint(10) DEFAULT NULL,
  `anonymous` tinyint(1) NOT NULL DEFAULT '0',
  `other` longtext COLLATE utf8mb4_unicode_ci,
  `timecreated` bigint(10) NOT NULL,
  `origin` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `ip` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `realuserid` bigint(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Standard log table' ROW_FORMAT=COMPRESSED;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `mdl_logstore_standard_log`
--
ALTER TABLE `mdl_logstore_standard_log`
  ADD PRIMARY KEY (`id`),
  ADD KEY `mdl_logsstanlog_tim_ix` (`timecreated`),
  ADD KEY `mdl_logsstanlog_couanotim_ix` (`courseid`,`anonymous`,`timecreated`),
  ADD KEY `mdl_logsstanlog_useconconcr_ix` (`userid`,`contextlevel`,`contextinstanceid`,`crud`,`edulevel`,`timecreated`),
  ADD KEY `mdl_logsstanlog_con_ix` (`contextid`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `mdl_logstore_standard_log`
--
ALTER TABLE `mdl_logstore_standard_log`
  MODIFY `id` bigint(10) NOT NULL AUTO_INCREMENT;

LDAP and SSO are fundamentally different approaches to user authentication in Moodle. LDAP performs credential lookups directly against a directory service; SSO delegates authentication to a centralized identity provider. Understanding the distinction helps you choose the right approach for your setup.

LDAP Authentication

With Moodle’s LDAP plugin, each Moodle instance authenticates users itself by querying an LDAP directory — typically an on-premises Active Directory server accessible over the local network. When a user logs in:

  1. Moodle connects to the LDAP server and queries the credentials.
  2. If the credentials match, Moodle checks whether the user account exists locally; if not, it creates one.
  3. Moodle creates a session.

This works well when your LDAP server is on the same network as Moodle. It is not SSO — each Moodle instance authenticates independently, so a user with access to three instances must log in to each one separately.

Single Sign-On (SSO)

An SSO solution centralizes authentication in a dedicated identity provider (IdP) — such as Azure AD, Okta, or any SAML2-compatible service. When a user logs in to a connected application:

  1. The application redirects the user to the IdP.
  2. The user authenticates once at the IdP — or is recognized as already authenticated.
  3. The IdP redirects back with the outcome and the user’s profile data.
  4. Moodle checks whether the user account exists locally; if not, it creates one.
  5. Moodle creates a session.

Moodle never sees the user’s credentials — it only receives the result from the IdP. If the same user then accesses a second Moodle instance, they are already authenticated at the IdP and pass through without logging in again.

Azure AD: LDAP Is Not Available by Default

Azure AD (Entra ID) is a cloud service and does not expose a traditional LDAP endpoint. To use LDAP with Azure AD, you would need to set up Azure AD Domain Services (AD DS) — a managed domain add-on that does expose LDAP. This is a complex configuration and carries meaningful security risk: exposing LDAP over the internet is roughly equivalent to opening a database port publicly.

For Azure AD environments, SSO via SAML2 or OIDC is the straightforward and recommended path. If you do need to evaluate AD DS, these references cover the setup:

Which to Use

Use LDAP if:

  • You have an on-premises Active Directory server on the same network as Moodle.
  • You have a single Moodle instance, or separate logins per instance are acceptable.

Use SSO if:

  • Your identity provider is cloud-based (Azure AD, Okta, Google Workspace, etc.).
  • You have multiple Moodle instances or other applications that should share a single login.
  • You want to decouple Moodle from the specifics of where credentials are stored.