On-premise Installation

This is a description on how to install Autotech Development ABs Docker-based Moodle distribution on your own servers. On-premise in this instance doesn’t necessarily mean your local servers, you are free to host it in a cloud of your choice. On-premise is more “not in our cloud” than anything else.

Prerequisites

The following steps assumes that the following prerequisites are fullfilled:

See also

This guide assumes you have direct access to the registry. It’s also possible to set this up on server without direct access to the registry. More about that in the On-premise Secure Environments chapter.

Warning

Installation on other distributions than Ubuntu LTS is not supported by our normal installation fee. To help with any troubles that arises we will have to charge extra.

Note

It should be possible to use cloud managed databases, at least PostgreSQL, e.g. Digital Ocean, Azure, AWS, etc. It is verified to work with Digital Ocean and Azure.

System Preparation

User & Group

The container uses a less privileged user to run PHP-FPM and access files. It has the following UID & GID:

UID:

2020

GID:

2020

Start by creating the group:

sudo groupadd -g 2020 sparkfore

Then create the user:

sudo useradd -s /sbin/nologin -u 2020 -g sparkfore sparkfore

Folders

Moodle needs to have access to persistent storage which we can mount into the container so our next step is to prepare for these folders.

sudo mkdir -p /data/moodledata

Set the correct ownership

sudo chown 2020:2020 /data/moodledata

Make shure that the permissions are correct

sudo chmod 755 /data/moodledata

Tip

This is our prefered method of organizing the data. It is possible to use a custom layout if the paths in docker-compose.yml is change to match the custom layout.

SystemD Services

We are using a couple of SystemD services to mange the startup process of our Docker Compose configuration. A required one that pulls the latest images and starts (and stops) the Docker Compose and an optional one that manages scheduled restarts (which will automatically update to the latest version).

Preparations

If you are running this in a virtualized environment we recommend that you install haveged. We have discovered that the start and stop times of Docker can be severely effected if it is missing.

sudo apt-get install haveged

Managing Docker Compose (required)

We will start by creating the service file and populating it with content. Our first step is to open the file in an editor,

sudo nano /etc/systemd/system/docker-compose.service

Tip

Nano is our prefered editor due to its simplicity but feel free to substitute it with your preference, e.g. vi, vim or whatever you like.

Next step is to populate the content of the file. The required content of the file is provided below but you will need to change <registry-user> and <registry-password> to the credentials you’ve received from us.

/etc/systemd/system/docker-compose.service
[Unit]
Description=Docker Compose container starter
After=docker.service network-online.target
Requires=docker.service network-online.target

[Service]
WorkingDirectory=/etc/docker-compose
Type=oneshot
RemainAfterExit=yes

ExecStartPre=/usr/bin/docker login -u <registry-user> -p <registry-password> https://registry.autotech.se
ExecStartPre=/usr/bin/docker compose pull --quiet --ignore-pull-failures
ExecStart=/usr/bin/docker compose up -d --remove-orphans

ExecStop=/usr/bin/docker compose down

ExecReload=/usr/bin/docker compose pull --quiet --ignore-pull-failures
ExecReload=/usr/bin/docker compose up -d --remove-orphans

[Install]
WantedBy=docker.service

Now that we’ve created the file let’s make sure that it has the correct ownership and permissions:

sudo chown root:root /etc/systemd/system/docker-compose.service
sudo chmod 644 /etc/systemd/system/docker-compose.service

Reload SystemD to make it aware of the new service

sudo systemctl daemon-reload

Our last step is now to enable the service so that it autostarts after reboots.

sudo systemctl enable /etc/systemd/system/docker-compose.service

Caution

Do not try to start the service yet. It is still missing some requirements, mainly the Docker Compose file.

Scheduled Restarts (optional)

We are using the timer fucntionality of SystemD to schedule automatic updates of the images. If you want to implement it this way or do it manually is entirely up to you, this is not a requirement in any way.

We will begin with creating a service-file for the timer so open it up in an editor.

sudo nano /etc/systemd/system/docker-compose-reload.service

The contents of this requires no adaptation and can be used as is since it’s only reloading the docker-compose.service we created earlier.

/etc/systemd/system/docker-compose-reload.service
[Unit]
Description=Refresh images and update containers

[Service]
Type=oneshot

ExecStart=/bin/systemctl reload docker-compose.service

Make sure it has the correct owner and permissions

sudo chown root:root /etc/systemd/system/docker-compose-reload.service
sudo chmod 644 /etc/systemd/system/docker-compose-reload.service

Now it is time to create the actual timer that’s responsible for running our newly created docker-compose-reload.service. To create a timer for docker-compose-reload.service we will create docker-compose-reload.timer.

sudo nano /etc/systemd/system/docker-compose-reload.service

You can use the provided timer example as it is if you want to. If there is something you might want to change it is the actual schedule. In our example it is configured to reload on every third tuesday of the month (this is to match up with our release schedule).

/etc/systemd/system/docker-compose-reload.timer
[Unit]
Description=Refresh images and update containers

[Timer]
OnCalendar=Tue *-*-15..21 03:00:00
RandomizedDelaySec=0

[Install]
WantedBy=timers.target

As always make sure the file as the correcet ownership and permissions.

sudo chown root:root /etc/systemd/system/docker-compose-reload.timer
sudo chmod 644 /etc/systemd/system/docker-compose-reload.timer

Reload SystemD to make it aware of the new service and timer

sudo systemctl daemon-reload

The next step is to enable the timer (just the timer and NOT the service)

sudo systemctl enable /etc/systemd/system/docker-compose-reload.timer

Docker Compose file

It is now time to create the Docker Compose file that will launch all the containers that are required. Our first step is to create a folder we will store docker-compose.yml and the related env-files for settings.

sudo mkdir -p /etc/docker-compose/settings
sudo chown -R root:root /etc/docker-compose
sudo chmod -R 700 /etc/docker-compose

Tip

Since we’ve now changed the permissions of /etc/docker-compose to only allow root access just using sudo can be a bit awkward. While it is possible to run all the commands listed below using sudo tab-completion won’t work and it’s not possble to list the contents of the directory. To switch to running as root user instead run this:

sudo su

Start by creating docker-compose.yml

sudo nano /etc/docker-compose/docker-compose.yml

You can use this template to create the docker-compose.yml file but you will need to change <your-moodle-image> and <your-moodle-cron-image> to the information you’ve received from us.

/etc/docker-compose/docker-compose.yml
version: '3'

services:
  moodle:
    image: <your-moodle-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
    networks:
      - autotech

  cron:
    image: <your-moodle-cron-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
    networks:
      - autotech

networks:
  autotech:

Caution

You will still not be able to access Moodle with this docker-compose.yml. No ports are exposed yet. See the section Ports, Proxy & Varnish

Caution

If you’ve used another layout for organizing your data you need to change the paths under volumes for both the moodle and cron service.

Ports, Proxy & Varnish

The following section is a bit fuzzt around the edges when it comes to hard recommendations, mostly because it’s very much up to what fits in your environment. It is possible to run things a multiple different ways with very small changes to the configuration.

If you want to run without SSL or if you want to use a non-docker reverse proxy (e.g. reverse proxy installed on host, external load balancer or similar) the steps are almost the same. What needs to be done is to expose the port for the Moodle container.

In the example below you can see how it’s done. The first value is the host port (8080) and the second value is the container port (80). The container port will always be 80 but you can change the host port to suite your needs. This might be an example for running the reverse proxy directly on the host.

/etc/docker-compose/docker-compose.yml
version: '3'

services:
  moodle:
    image: <your-moodle-image>
    restart: unless-stopped
    ports:
      - "8080:80"
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
    networks:
      - autotech

  cron:
    image: <your-moodle-cron-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
    networks:
      - autotech

networks:
  autotech:

Reverse Proxy

If you want to deploy a all-in-one solution where everything is handled by Docker and docker-compose.yml we provide a preconfigured reverse proxy that can be used. It is based on nginx and prepared for SSL termination (mainly certificates from Let’s Encrypt but can be used with customer provided certificates also). When using a docker-compose.yml based solution there is no need to expose the port for the Moodle container. This is an example using our reverse proxy and Certbot for Let’s Encrypt certificates (using Cloudflare DNS validation).

/etc/docker-compose/docker-compose.yml
version: '3'

services:
  reverseproxy:
    image: registry.autotech.se/tools/generic-moodle-reverseproxy:latest
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - ENVIRONMENT=production
      - HOSTNAME=lms.example.com
      - MOODLE_HOST=moodle
    volumes:
      - /data/ssl/conf:/etc/letsencrypt
      - /data/ssl/data:/var/www/certbot
    networks:
      - autotech
    depends_on:
      - moodle

  moodle:
    image: <your-moodle-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
    networks:
      - autotech

  cron:
    image: <your-moodle-cron-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
    networks:
      - autotech

  certbot:
    image: certbot/dns-cloudflare
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --dns-cloudflare --dns-cloudflare-credentials /cloudflare.ini ; sleep 12h & wait $${!}; done;'"
    restart: unless-stopped
    volumes:
      - /data/ssl/conf:/etc/letsencrypt
      - /data/ssl/data:/var/www/certbot
      - ./settings/cloudflare.ini:/cloudflare.ini:ro

# docker-compose run --entrypoint certbot --rm certbot certonly --non-interactive --agree-tos --dns-cloudflare --dns-cloudflare-credentials /cloudflare.ini -m <admin email> --cert-name sparkfore.com -d <your domain name>

networks:
  autotech:

Note

During the initial creation of the certificate we give the certificate the name sparkfore.com, this name is what the reverse proxy image is looking for. Mounting a non-Let’s Encrypt certificate and giving it the same name will also work.

Varnish

We also have a customzied Varnish image that can be used for HTTP caching. It is pre-configured for Moodle and basically plug-n-play. As long as the service for Moodle is named moodle all that is needed is to add the Varnish service and change the MOODLE_HOST variable for the reverse proxy. Here is a complete example using our reverse proxy, varnish image and certbot for Let’s Encrypt certificates.

/etc/docker-compose/docker-compose.yml
version: '3'

services:
  reverseproxy:
    image: registry.autotech.se/tools/generic-moodle-reverseproxy:latest
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - ENVIRONMENT=production
      - HOSTNAME=lms.example.com
      - MOODLE_HOST=varnish:7777
    volumes:
      - /data/ssl/conf:/etc/letsencrypt
      - /data/ssl/data:/var/www/certbot
    networks:
      - autotech
    depends_on:
      - moodle
      - varnish

  varnish:
    image: registry.autotech.se/tools/moodle-varnish:latest
    restart: unless-stopped
    environment:
      - VARNISH_SIZE=512M
      - VARNISH_HTTP_PORT=7777
    tmpfs:
      - /var/lib/varnish/varnishd:exec
    networks:
      - autotech
    depends_on:
      - moodle

  moodle:
    image: <your-moodle-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
    networks:
      - autotech

  cron:
    image: <your-moodle-cron-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
    networks:
      - autotech

  certbot:
    image: certbot/dns-cloudflare
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --dns-cloudflare --dns-cloudflare-credentials /cloudflare.ini ; sleep 12h & wait $${!}; done;'"
    restart: unless-stopped
    volumes:
      - /data/ssl/conf:/etc/letsencrypt
      - /data/ssl/data:/var/www/certbot
      - ./settings/cloudflare.ini:/cloudflare.ini:ro

# docker-compose run --entrypoint certbot --rm certbot certonly --non-interactive --agree-tos --dns-cloudflare --dns-cloudflare-credentials /cloudflare.ini -m <admin email> --cert-name sparkfore.com -d <your domain name>

networks:
  autotech:

Settings

Our prefered method is to use multiple env-files grouped by context (as seen in docker-compose.yml above). The reasoning behind this is that almost all values must be shared between the moodle and cron container and this prevents us from having to write every setting twice.

The format of the settings file can be found in detail here: https://docs.docker.com/compose/environment-variables/env-file/ but the basics are as this:

VAR=value

app-moodle.env

This file contains settings for configuring the behavior of Moodle (except the database connection).

Required (always)

These variables always needs be defined for Moodle to work correctly (or at all).

MOODLE_APP_URL:

The full base URL for the LMS, including protocol, but no trailing slash. For example: https://lms.example.com

Note: Moodle actually saves this value in the database when making some actions. In practice this means that if you change this value afterwards the saved values will still point to the old URL. Our recommendation is to use the correct URL from the begining.

Warning: Moodle behaves a bit odd when running on a non-standard port (not 80 or 443). So if you want to test Moodle locally you might try to run it by mapping the host port 8080 to the container port 80. This will however put you in an eternal redirect loop. To fix this loop you need to set the APP_URL to http://localhost:8080 and NGINX_PORT to 8080. This will cause the container port to be 8080 so you would then need to map host port 8080 to container port 8080.

MOODLE_SSL_PROXY:

As described in the On-premise System Architecture chapter the Moodle container itself only responds to HTTP on port 80, no builtin SSL termination. HTTPS must be handled by a reverse proxy or load balancer. When running Moodle behind a reverse proxy or load balancer that terminates SSL it must be made aware of this. Setting this variable to 1 will make Moodle aware of the SSL termination. For now the default is 0 and therefore this variable must be defined and set to 1 in most scenarios.

Valid options:

  • 0 (default)

  • 1

Required (install)

These variables are only required during the installation of Moodle, meaning most often just the first start. During the installation Moodle creates a Super Administrator account using this information. When Moodle has been installed it’s possible to remove this information if it’s desired. Part of the reasoning behind only giving root access to this folder is to protect information of this kind.

MOODLE_ADMIN_EMAIL:

The email to use for the default Super Administrator account

MOODLE_ADMIN_USER:

The username to use for the default Super Administrator account

MOODLE_ADMIN_PASS:

The password to use for the default Super Administrator account

Optional

The variables in this section are generally not defined by default but they have functionality that, in some cases, might be useful.

MOODLE_NO_EMAILS:

If you define this variable and set it to 1 Moodle will not send any emails at all. This feature is almost exclusively used in troubleshooting scenarios.

Valid options:

  • 0 (default)

  • 1

database-moodle.env

This file is for specifying the database connection options. While these options still are for Moodle we have separated them to a separate file for easier overview.

Common Settings (required)
MOODLE_DB_TYPE:

What type of database is Moodle trying to connect to?

Valid options:

  • pgsql (default)

  • mariadb

  • mysql

  • sqlsrv

MOODLE_DB_USER:

Username for connecting to the database

MOODLE_DB_PASS:

Password for connecting to the database

MOODLE_DB_NAME:

Name of the database that is to be used

MOODLE_DB_HOST:

Hostname, eg ‘localhost’ or ‘db.isp.com’, or IP

MOODLE_DB_PORT:

Port used for connecting to the database server. Defaults to 5432

Common Settings (optional)
MOODLE_DB_PREFIX:

This is the prefix to use for all table names. Defaults to mdl_

PostgreSQL Specific (optional)
MOODLE_DB_FETCHBUFFERSIZE:

Set the limit on the number of rows that are fetched into
memory when doing a large recordset query (e.g. search
indexing). Set to 0 to turn it off. You need to set to 0 if you
are using pg_bouncer in ‘transaction’ mode (it is fine in ‘session’
mode). Default is 100000.

MOODLE_DB_PGBOUNCER:

When set to true it enables the “dbhandlesoptions” setting to
true.

Valid options:

  • true

  • false (default)

MOODLE_DB_SSL:

Set this to true to enable support for connect to the database
over SSL.

Valid options:

  • true

  • false (default)

MOODLE_DB_SSLMODE:

Set what SSL mode PostgreSQL will be using. Verified to be
working with require

Valid options:

  • allow

  • prefer

  • require

  • verify-ca

  • verify-full

MS SQL Server Specific (optional)

These options are only valid from Moodle 4.2 an upwards.
Microsoft ODBC 18 by default tries to encrypt the database connection and these options are to help connect to databases that doesn’t encrypt the connection.

MOODLE_DB_SQLSRV_ENCRYPT:

Should the connection be encrypted?

Valid options:

  • true

  • false

MOODLE_DB_SQLSRV_TRUSTSERVERCERTIFICATE:

Should the SQL Server certificate be trusted
(e.g. self-signed certificates)?

Valid options:

  • true

  • false

shared-debug.env (optional)

This file is entirely optional to create. Its purpose is to enable debuging options in both Moodle and PHP. In some cases this might help with troubleshooting but is rarely used in production systems.

Moodle
MOODLE_DEBUG:

When set to force it force Moodle to enable all debugging options and disables caching.

PHP
PHP_DISPLAY_ERRORS:

When set to On PHP will display the errors that occur

PHP_ERROR_REPORTING:

When set to E_ALL PHP will report all errors that occur

PHP_OPCACHE_ENABLE:

When set to 0 the opcache is disabled and code is always executed. Speeds up testing of new code instead of having to wait on cache invalidation.

PHP_OPCACHE_ENABLE_CLI:

The sames as above but for CLI processes.

/etc/docker-compose/settings/shared-debug.env
MOODLE_DEBUG=force

PHP_DISPLAY_ERRORS=On
PHP_ERROR_REPORTING=E_ALL
PHP_OPCACHE_ENABLE=0
PHP_OPCACHE_ENABLE_CLI=0

Custom Root Certificates

Sometimes, mainly when connecting to Active Directory/LDAP for synchronizing users, there’s a need to add custom root certificates. Our images support this and all you really need to do is to make sure that the certificate is in PEM-format and then mount it into the container on the correct location. The path you need to mount the certificate to is /usr/local/share/ca-certificates/ (in both moodle and cron) and when the container starts it will then add the mounted certificate(s) to its CA-store. Here is an example of a docker-compose.yml with a custom root certificate mounted.

/etc/docker-compose/docker-compose.yml
version: '3'

services:
  moodle:
    image: <your-moodle-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      - ./settings/shared-email.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
      - ./custom-root-ca.pem:/usr/local/share/ca-certificates/custom-root-ca.crt
    networks:
      - autotech

  cron:
    image: <your-moodle-cron-image>
    restart: unless-stopped
    env_file:
      - ./settings/database-moodle.env
      - ./settings/app-moodle.env
      - ./settings/shared-email.env
      #- ./settings/shared-debug.env
    volumes:
      - /data/moodledata:/moodledata/
      - ./custom-root-ca.pem:/usr/local/share/ca-certificates/custom-root-ca.crt
    networks:
      - autotech

networks:
  autotech:

Performance Tunings

Depending on the type of load the installation is under it might be required to tweak the settings of PHP or PHP-FPM. Therefore various settings for PHP and PHP-FPM are exposed as environmen variables. The following is a list of the available variables and their default values.

FPM_PM_MAX_SPARE_SERVICE:

3

FPM_PM_MAX_REQUESTS:

0

FPM_PM_MAX_CHILDREN:

5

FPM_PM_START_SERVICE:

2

FPM_PM:

dynamic

FPM_PM_MIN_SPARE_SERVICE:

1

FPM_PM_PROCESS_IDLE_TIMEOUT:

10s

PHP_OPCACHE_ENABLE_FILE_OVERRIDE:

0

PHP_MEMORY_LIMIT:

128M

PHP_MAX_FILE_UPLOADS:

20

PHP_OPCACHE_SAVE_COMMENTS:

1

PHP_OPCACHE_MAX_ACCELERATED_FILES:

8000

PHP_OPCACHE_REVALIDATE_FREQ:

60

PHP_OPCACHE_MEMORY_CONSUMPTION:

128

PHP_MAX_INPUT_TIME:

60

PHP_MAX_EXECUTION_TIME:

30

More Environtment Variables

The default value is listed along them.

LANG:

C.UTF-8

TZ:

Europe/Stockholm

NGINX_HOST:

_

NGINX_PORT:

80

MOODLE_REVERSEPROXY:

false

MOODLE_CLUSTERED:

false