This guide provides a full Ansible playbook to deploy Moodle on Debian 12+, Ubuntu 22.04+, and RHEL 9+ compatible hosts with Linux DevOps best practices.
---
- name: Deploy Moodle LMS
hosts: moodle
become: true
vars:
moodle_version: "4.5.1" # Specify version for reproducible deployments
moodle_db_name: "moodle"
moodle_db_user: "moodleuser"
moodle_db_password: "{{ vault_moodle_db_password }}" # Store in Ansible Vault
moodle_admin_user: "admin"
moodle_admin_password: "{{ vault_moodle_admin_password }}" # Store in Ansible Vault
moodle_datadir: "/var/moodledata"
moodle_webdir: "/var/www/moodle"
moodle_domain: "moodle.example.com"
pre_tasks:
- name: Update apt cache if needed
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_pkg_mgr == "apt"
tasks:
- name: Install system dependencies
package:
name:
- apache2
- php8.3
- php8.3-{cli,curl,mbstring,xml,zip,intl,gd,soap,xmlrpc,bcmath,ldap,mysql,pgsql,sqlite3}
- libapache2-mod-php8.3
- mariadb-server
- mariadb-client
- curl
- wget
- unzip
- git
state: present
notify: restart apache2
- name: Enable Apache modules
apache2_module:
name: "{{ item }}"
state: present
loop:
- rewrite
- headers
- ssl
notify: restart apache2
- name: Start and enable MariaDB
systemd:
name: mariadb
state: started
enabled: true
- name: Create Moodle database
mysql_db:
name: "{{ moodle_db_name }}"
encoding: utf8mb4
collation: utf8mb4_unicode_ci
state: present
notify: restart mariadb
- name: Create Moodle database user
mysql_user:
name: "{{ moodle_db_user }}"
password: "{{ moodle_db_password }}"
priv: "{{ moodle_db_name }}.*:SELECT,INSERT,UPDATE,DELETE,CREATE,CREATE TEMPORARY TABLES,DROP,INDEX,ALTER"
state: present
host: localhost
notify: restart mariadb
- name: Download Moodle
unarchive:
src: "https://download.moodle.org/download.php/direct/stable405/moodle-{{ moodle_version }}.tgz"
dest: /tmp
remote_src: yes
register: moodle_download
- name: Copy Moodle to web directory
copy:
src: /tmp/moodle/
dest: "{{ moodle_webdir }}"
remote_src: yes
owner: www-data
group: www-data
mode: preserve
when: moodle_download is succeeded
- name: Create Moodle data directory
file:
path: "{{ moodle_datadir }}"
state: directory
owner: www-data
group: www-data
mode: '0755'
- name: Set proper permissions for Moodle
file:
path: "{{ moodle_webdir }}"
owner: www-data
group: www-data
mode: '0644'
recurse: yes
notify: restart apache2
- name: Set write permissions for specific directories
file:
path: "{{ moodle_webdir }}/{{ item }}"
mode: '0755'
state: directory
recurse: yes
loop:
- "admin"
- "cache"
- "files"
- "local"
- "sessions"
- "temp"
- "trashdir"
- "backup"
- "draft"
- "temp"
- "repository"
- "question"
- "pix"
- "moddata"
notify: restart apache2
- name: Create Apache virtual host for Moodle
template:
src: moodle-vhost.conf.j2
dest: /etc/apache2/sites-available/moodle.conf
owner: root
group: root
mode: '0644'
notify: restart apache2
- name: Enable Moodle virtual host
apache2_site:
name: moodle.conf
state: enabled
notify: restart apache2
- name: Disable default Apache site
apache2_site:
name: 000-default.conf
state: disabled
notify: restart apache2
- name: Configure PHP settings for Moodle
lineinfile:
dest: /etc/php/8.3/apache2/php.ini
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
backup: yes
loop:
- { regexp: '^;?memory_limit =', line: 'memory_limit = 512M' }
- { regexp: '^;?post_max_size =', line: 'post_max_size = 64M' }
- { regexp: '^;?upload_max_filesize =', line: 'upload_max_filesize = 64M' }
- { regexp: '^;?max_execution_time =', line: 'max_execution_time = 180' }
- { regexp: '^;?max_input_vars =', line: 'max_input_vars = 5000' }
notify: restart apache2
- name: Configure MariaDB for Moodle
lineinfile:
dest: /etc/mysql/mariadb.conf.d/50-server.cnf
insertafter: '[mysqld]'
line: "{{ item }}"
backup: yes
loop:
- "innodb_buffer_pool_size = 512M"
- "innodb_log_file_size = 128M"
- "query_cache_type = 1"
- "query_cache_size = 64M"
notify: restart mariadb
- name: Create Moodle config.php
template:
src: config.php.j2
dest: "{{ moodle_webdir }}/config.php"
owner: root
group: www-data
mode: '0640'
notify: restart apache2
- name: Set up Moodle cron job
cron:
name: "Moodle cron"
minute: "*/5"
user: www-data
job: "/usr/bin/php {{ moodle_webdir }}/admin/cli/cron.php >/dev/null 2>&1"
state: present
handlers:
- name: restart apache2
systemd:
name: apache2
state: restarted
- name: restart mariadb
systemd:
name: mariadb
state: restarted
Create the following templates in your Ansible roles directory:
<VirtualHost *:80>
ServerName {{ moodle_domain }}
DocumentRoot {{ moodle_webdir }}
<Directory {{ moodle_webdir }}>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Security headers
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# PHP settings for Moodle
php_value max_input_vars 5000
php_value memory_limit 512M
php_value post_max_size 64M
php_value upload_max_filesize 64M
php_value max_execution_time 180
php_value max_input_time 180
ErrorLog ${APACHE_LOG_DIR}/moodle_error.log
CustomLog ${APACHE_LOG_DIR}/moodle_access.log combined
</VirtualHost>
<?php
unset($CFG);
global $CFG;
$CFG = new stdClass();
$CFG->dbtype = 'mariadb';
$CFG->dblibrary = 'native';
$CFG->dbhost = 'localhost';
$CFG->dbname = '{{ moodle_db_name }}';
$CFG->dbuser = '{{ moodle_db_user }}';
$CFG->dbpass = '{{ moodle_db_password }}';
$CFG->prefix = 'mdl_';
$CFG->dboptions = array(
'dbpersist' => true,
'dbsocket' => false,
'dbport' => '',
);
$CFG->wwwroot = 'https://{{ moodle_domain }}';
$CFG->dataroot = '{{ moodle_datadir }}';
$CFG->admin = 'admin';
$CFG->directorypermissions = 0755;
$CFG->filepermissions = 0644;
// Security settings
$CFG->passwordpolicy = true;
$CFG->opentogoogle = false;
$CFG->protectuserpics = true;
$CFG->registerauth = '';
$CFG->loginlockoutwindow = 300;
$CFG->loginattemptdelay = 5;
// Performance settings
$CFG->cachejs = true;
$CFG->cachetemplates = true;
$CFG->langstringcache = true;
$CFG->proxybypasslocal = true;
// Email settings
$CFG->smtphosts = 'localhost';
$CFG->noreplyaddress = 'noreply@{{ moodle_domain }}';
// File upload settings
$CFG->maxbytes = 104857600; // 100MB
$CFG->maxfiles = 100;
$CFG->enableplagiarism = true;
// Session settings
$CFG->sessiontimeout = 7200; // 2 hours
$CFG->sessioncookie = 'MoodleSession';
$CFG->sessioncookiedomain = '.{{ moodle_domain }}';
$CFG->sessioncookiepath = '/';
$CFG->sessioncookiesecure = true;
$CFG->sessioncookiehttponly = true;
// Advanced features (for Moodle 5.0+)
$CFG->enablemobilewebservice = true;
$CFG->tool_generator_users_per_course = 1000;
// AI Integration settings (for Moodle 5.0+)
$CFG->enableaiconnect = true; // Enable AI features if available
require_once(__DIR__ . '/lib/setup.php');
Create an inventory file to define your Moodle server:
[moodle]
moodle-server ansible_host=192.168.1.100 ansible_user=ubuntu
[all:vars]
ansible_python_interpreter=/usr/bin/python3
# Run the playbook
ansible-playbook -i inventory moodle-playbook.yml
# Using vault for secrets
ansible-playbook -i inventory moodle-playbook.yml --ask-vault-pass
Any questions?
Feel free to contact us. Find all contact information on our contact page.