
I remember a particularly brutal Moodle upgrade for a university in Manila. We had a 12-hour maintenance window for a Moodle 3.9 to 4.1 migration, impacting over 15,000 active students. Midway through, a manually executed SQL script for a custom plugin failed, rolling back half the database and corrupting several activity modules. The rollback took 4 hours, and we ended up extending downtime by another 6, drawing ire from the academic registrar. That incident, which personally cost me 18 hours of continuous debugging fueled by instant coffee, cemented my resolve: manual deployments and upgrades are a ticking time bomb. This is exactly why I’ve dedicated significant effort to mastering moodle ansible automation for every subsequent deployment.
For nearly a decade, I’ve architected EdTech infrastructure across Asia-Pacific, deploying everything from Moodle and Canvas to Open edX. My experience consistently shows that automation, specifically with Ansible, isn’t just a convenience; it’s a critical reliability and security component for any educational institution running Moodle in production.
When I design an Ansible playbook for Moodle, I approach it as a modular blueprint. A robust automate moodle deployment strategy requires more than just copying files; it’s about orchestrating servers, databases, and application-level configurations harmoniously. My standard Moodle playbook structure breaks down into several key roles, ensuring idempotent operations and clear separation of concerns:
webserver: Configures Nginx (my web server of choice for performance with PHP-FPM) or Apache, including SSL/TLS setup via Let’s Encrypt or institutional certificates.php: Installs PHP-FPM with necessary extensions (e.g., php-cli, php-curl, php-gd, php-intl, php-mbstring, php-xml, php-zip, php-soap, php-pgsql or php-mysql). Crucially, this role also tunes php.ini and www.conf settings.database: Provisions PostgreSQL (my strong preference over MySQL for Moodle’s long-term scalability and data integrity) or MySQL, creates the Moodle database and user, and sets up appropriate permissions.moodle_app: Handles Moodle core file deployment (cloning from Git or extracting archives), config.php generation, data directory setup, and initial installation via admin/cli/install.php or admin/cli/upgrade.php.moodle_plugins: Manages the deployment of custom and third-party Moodle plugins, themes, and language packs.caching: Configures Redis or Memcached for Moodle’s session and MUC (Moodle Universal Cache) stores, significantly improving responsiveness for high user loads.cron: Sets up the Moodle cron job, vital for background tasks like sending notifications, processing activity logs, and synchronizing user data.This structured approach allows me to iterate on specific components without fear of breaking others. I routinely deploy a base Moodle instance capable of handling 5,000 concurrent users with this methodology, targeting 4GB RAM, 2vCPU servers for the application layer.
For any serious Moodle deployment, I wholeheartedly recommend PostgreSQL. Its advanced features, superior concurrency handling, and robust data integrity checks make it a far better choice than MySQL, especially for large datasets and heavy read/write operations common in an LMS. My database role, when deploying PostgreSQL, takes care of the following:
# roles/database/tasks/main.yml
- name: Install PostgreSQL server
ansible.builtin.apt:
name: "postgresql-{{ postgresql_version }}"
state: present
update_cache: yes
become: yes
- name: Ensure PostgreSQL service is running and enabled
ansible.builtin.service:
name: postgresql
state: started
enabled: yes
become: yes
- name: Create Moodle database user
community.postgresql.postgresql_user:
db: postgres
name: "{{ moodle_db_user }}"
password: "{{ moodle_db_password }}"
role_attr_flags: CREATEDB,LOGIN
state: present
become: yes
become_user: postgres
- name: Create Moodle database
community.postgresql.postgresql_db:
name: "{{ moodle_db_name }}"
owner: "{{ moodle_db_user }}"
encoding: UTF8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
template: template0
state: present
become: yes
become_user: postgres
- name: Configure pg_hba.conf for secure access
ansible.builtin.copy:
src: pg_hba.conf.j2
dest: /etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf
owner: postgres
group: postgres
mode: '0640'
become: yes
notify: Restart postgresql
- name: Configure postgresql.conf for performance
ansible.builtin.copy:
src: postgresql.conf.j2
dest: /etc/postgresql/{{ postgresql_version }}/main/postgresql.conf
owner: postgres
group: postgres
mode: '0644'
become: yes
notify: Restart postgresql
The pg_hba.conf.j2 template usually restricts access to host all all 127.0.0.1/32 trust (for local access) or specifies the IP range of the Moodle application servers with md5 authentication for production environments. For postgresql.conf, I tune shared_buffers, work_mem, maintenance_work_mem, and max_connections based on server resources and expected user load, often setting shared_buffers to 25% of system RAM and max_connections to 200-300 for a server handling 10,000 student enrollments.
Nginx is my go-to web server for Moodle. Its asynchronous event-driven architecture makes it incredibly efficient at handling static assets and proxying requests to PHP-FPM. Within my webserver and php roles, I ensure PHP-FPM is configured with adequate child processes and memory.
One thing I wish someone had told me before I started my journey: always prioritize PHP memory. Moodle, especially with numerous plugins, can be quite memory hungry. I’ve spent frustrating hours debugging “blank page” errors only to trace them back to an out of memory error hidden deep in PHP-FPM logs, often caused by the default memory_limit = 128M. For any production Moodle instance, I immediately set memory_limit = 512M (or even 1G for very large or complex installations) in /etc/php/{{ php_version }}/fpm/php.ini and /etc/php/{{ php_version }}/cli/php.ini.
Another crucial setting is max_execution_time. While CLI scripts handle their own limits, web requests often hit a default 30-second timeout, which isn’t enough for some Moodle processes, especially during upgrades or extensive course backups. I typically set max_execution_time = 300 seconds.
The moodle_app role is where the magic of deployment truly happens. I usually pull Moodle directly from its Git repository, allowing for easy version control and patch application.
# roles/moodle_app/tasks/main.yml
- name: Clone Moodle from Git repository
ansible.builtin.git:
repo: "{{ moodle_git_repo }}"
dest: "{{ moodle_install_dir }}"
version: "{{ moodle_version_tag }}"
force: yes
become: yes
become_user: "{{ moodle_app_user }}"
- name: Create Moodle data directory
ansible.builtin.file:
path: "{{ moodle_data_dir }}"
state: directory
owner: "{{ moodle_app_user }}"
group: "{{ moodle_app_group }}"
mode: '0770'
become: yes
- name: Copy Moodle config.php
ansible.builtin.template:
src: config.php.j2
dest: "{{ moodle_install_dir }}/config.php"
owner: "{{ moodle_app_user }}"
group: "{{ moodle_app_group }}"
mode: '0644'
become: yes
- name: Install Moodle via CLI if not installed (or upgrade)
ansible.builtin.command:
cmd: "php {{ moodle_install_dir }}/admin/cli/install.php --non-interactive --wwwroot={{ moodle_wwwroot }} --dataroot={{ moodle_data_dir }} --dbtype={{ moodle_db_type }} --dbhost={{ moodle_db_host }} --dbname={{ moodle_db_name }} --dbuser={{ moodle_db_user }} --dbpass={{ moodle_db_password }} --fullname={{ moodle_site_name }} --shortname={{ moodle_site_shortname }} --adminuser={{ moodle_admin_user }} --adminpass={{ moodle_admin_password }} --adminemail={{ moodle_admin_email }}"
creates: "{{ moodle_install_dir }}/version.php" # Prevents re-running if Moodle is already installed
become: yes
become_user: "{{ moodle_app_user }}"
when: not moodle_is_installed.stdout
- name: Run Moodle upgrade via CLI (for subsequent runs)
ansible.builtin.command:
cmd: "php {{ moodle_install_dir }}/admin/cli/upgrade.php --non-interactive"
become: yes
become_user: "{{ moodle_app_user }}"
when: moodle_is_installed.stdout # Run only if Moodle was already installed (e.g., during an upgrade)
# ... tasks for installing plugins, themes etc.
The install.php command is incredibly powerful, allowing a completely headless Moodle setup. For plugins, I typically use git clone or unarchive into the appropriate plugin directory (mod, block, theme, auth, etc.) followed by another admin/cli/upgrade.php to register them with Moodle. This ensures a consistent plugin set across all environments.
After the core Moodle files are in place, several post-deployment steps are critical for a functional and performant LMS:
cron role using Ansible’s ansible.builtin.cron module, running every minute under the www-data user (or whichever user PHP-FPM runs as).- name: Ensure Moodle cron job is configured
ansible.builtin.cron:
name: "Moodle cron job"
user: "{{ moodle_app_user }}" # e.g., www-data
minute: "*"
job: "/usr/bin/php {{ moodle_install_dir }}/admin/cli/cron.php > /dev/null 2>&1"
state: present
become: yes
moodledata directory needs to be writable by the web server user but not directly accessible via the web. Permissions are frequently a source of deployment headaches. I enforce 0770 on moodledata owned by www-data:www-data.After years of deploying Moodle with Ansible, I’ve hit more than my share of obscure issues. Here are a few common automate moodle deployment pitfalls and their fixes:
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20971520 bytes). This usually happens during install.php or upgrade.php, especially when dealing with many plugins or a large data directory migration./var/log/php-fpm/www-error.log (or equivalent) for memory_limit errors.memory_limit in /etc/php/{{ php_version }}/fpm/php.ini and /etc/php/{{ php_version }}/cli/php.ini to 512M or 1G. Don’t forget to restart PHP-FPM (systemctl restart php{{ php_version }}-fpm).www-data) doesn’t have write access to /var/www/moodledata.ansible.builtin.file: path: /var/www/moodledata state: directory owner: www-data group: www-data mode: '0770'. Crucially, if you create the directory manually, ensure the Ansible task re-applies these, and sometimes a chown -R www-data:www-data /var/www/moodledata is needed if previous manual steps messed it up.pg_hba.conf Connection Denied:/var/log/postgresql/postgresql-{{ postgresql_version }}-main.log) show FATAL: no pg_hba.conf entry for host "192.168.1.10" user "moodleuser" database "moodledb".pg_hba.conf./etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf to add an entry that matches your Moodle application server’s IP address, user, and database. For example: host moodledb moodleuser 192.168.1.10/32 md5. Then, restart PostgreSQL (systemctl restart postgresql).I have a strong, unequivocal stance: for any university deploying or managing an LMS, whether it’s Moodle, Canvas, or Open edX, automation with tools like Ansible is not optional. It is a fundamental requirement for operational excellence, security, and scalability.
My recommendation is always to move beyond the manual clicks or ad-hoc shell scripts. Implementing moodle ansible playbooks drastically reduces human error, provides consistent deployments across development, staging, and production environments, and enables rapid disaster recovery. If you’re building a campus-wide Single Sign-On (SSO) with Keycloak, as described in my article Building a Campus-Wide Single Sign-On (SSO) with Keycloak, you’ll find Ansible indispensable for configuring Moodle’s authentication plugins consistently. Furthermore, managing the underlying infrastructure for a student performance dashboard powered by Moodle data and Grafana, as detailed in Building a Student Performance Dashboard with Grafana and Moodle Data, becomes trivial with automation.
When evaluating database options, my recommendation remains PostgreSQL for Moodle, coupled with Redis for caching. While MySQL works, PostgreSQL scales better, handles heavy transaction loads with greater resilience, and offers more advanced features beneficial for data analysis or reporting integrations.
Automating Moodle deployment with Ansible playbooks transforms a typically complex, error-prone process into a repeatable, auditable, and reliable workflow. I’ve personally seen it slash deployment times from half a day to under 15 minutes, freeing up critical infrastructure architects like myself to focus on more strategic initiatives. From provisioning the database and tuning PHP-FPM to deploying core Moodle and its plugins, Ansible provides the framework for robust EdTech infrastructure.
By investing in moodle ansible automation, you’re not just deploying software; you’re building a foundation for a resilient, performant, and secure learning environment that can evolve with your institution’s needs. Remember, the goal isn’t just to make things work, it’s to make them work reliably, repeatedly, and with minimal fuss. For further insights into automating other aspects of EdTech, consider my experiences in Automating Canvas LMS Enrollments Using Python and REST APIs. Happy automating!