Moodle scales horizontally well, but only if three things are shared between the web servers: sessions, the cache and the moodledata file store. Miss one and students are logged out, see stale course pages or lose uploaded assignments. This post follows a DevOps engineer preparing a college Moodle for exam week on MassiveGRID PaaS, which is built on Virtuozzo Application Platform, formerly Jelastic, and getting all three right.

The environment is the Moodle package at a university department we will call Whitlock Faculty of Engineering, extended over a year into a PHP layer behind an NGINX load balancer, a MariaDB node, a Redis node and a shared storage node. Exam week means timed quizzes with 4,000 students submitting inside narrow windows.

Why a single big server is the wrong shape for Moodle

Quiz submission is bursty: nothing for 50 minutes, then thousands of POST requests in the last two. A single server sized for the burst idles the rest of the term, and vertical scaling alone hits PHP-FPM's limits on one node before it hits the CPU. Horizontal scaling adds whole application servers behind a balancer, and the platform can do it automatically. The engineer's job is to make Moodle indifferent to which server a request lands on.

Shared thing one: sessions in Redis

By default Moodle stores sessions in the database or on local disk. On local disk a student whose next request hits another server is logged out mid-quiz. The engineer sets Moodle's session handler to Redis in config.php:

$CFG->session_handler_class = '\core\session\redis';
$CFG->session_redis_host = 'redis-node.internal';
$CFG->session_redis_port = 6379;
$CFG->session_redis_auth = 'the-password';
$CFG->session_redis_prefix = 'mdl_sess_';
$CFG->session_redis_acquire_lock_timeout = 120;
$CFG->session_redis_lock_expire = 7200;

The Redis node's hostname is its internal platform address; traffic between the nodes stays on the internal network. The engineer gives the Redis node a cloudlet limit that comfortably exceeds the session set (4,000 sessions is a few tens of megabytes) and confirms its memory policy will not evict live sessions.

Shared thing two: the cache in Redis

Moodle's Universal Cache (MUC) stores compiled language strings, course structures and configuration. If each server keeps its own file cache, a course edit on one server is invisible on the others until their caches expire, and students see different pages depending on which server they hit. In Moodle's cache administration the engineer adds a Redis store pointing at the same Redis node (a different key prefix from the sessions) and maps the application cache mode to it. Every server now reads the same cache, and a purge on one purges for all.

Shared thing three: moodledata on shared storage

Uploaded assignments, course files and Moodle's temp and local directories live in moodledata, which must be outside the web root and identical on every server. The engineer adds a shared storage container to the environment and uses the platform's mount points to mount its moodledata export at the same path on every PHP node over NFSv4. New nodes created by scaling inherit the mount, because the mount is defined on the layer. Two exceptions are worth knowing: Moodle's localcachedir should stay on each server's local disk for speed, which config.php allows, and the storage node's disk limit is set with exam-week uploads in mind and a disk load alert at 80%.

Shared stateWhereConfigured inIf you skip it
SessionsRedis nodeconfig.php session_handler_classStudents logged out when balanced to another node
MUC application cacheRedis node, separate prefixSite admin, Caching, ConfigurationStale course pages on some nodes
moodledataShared storage container, NFSv4 mountPlatform mount points on the PHP layerUploads visible on one node only
Local cacheEach node's diskconfig.php localcachedirSlower page loads (not a correctness issue)

One cron, not seven

Moodle's cron runs scheduled tasks: grade calculations, forum digests, quiz clean-up. It must run every minute, and it must not run on every application server at once, or tasks collide and email is sent twice. The engineer configures the platform's cron on the master PHP node only, calling php admin/cli/cron.php each minute, and leaves the nodes added by scaling without one. Moodle's own task locking (which can also use Redis) is the second line of defence.

The triggers, and a rehearsal

On the PHP layer the engineer sets horizontal scaling triggers: add one node when average CPU exceeds 60% for two minutes, up to eight nodes; remove one when it is below 25% for fifteen minutes, down to two. The layer runs in stateful mode so new nodes are copies of the master with the Moodle code and configuration in place; they join the NGINX balancer automatically. MariaDB scales vertically with a raised cloudlet limit for the quiz-attempt writes. Before the first exam the engineer clones the whole environment, runs a load test of 4,000 simulated quiz submissions against the clone, watches the layer scale to seven, checks that no session was lost and that every upload appears on every node, and deletes the clone. The rehearsal cost a few dollars.

During the real exam week the layer peaked at seven nodes for two hours a day and spent the nights at two. The faculty's exam office reported nothing, which is the report the engineer wanted.

Frequently Asked Questions

Should Redis be a cluster for Moodle?

For a single institution a single Redis node with vertical scaling is usually enough: sessions and cache are small and the node restarts quickly. If a Redis restart during an exam is unacceptable, the Redis Cluster package gives replica failover; Moodle's session and cache stores work with it through a cluster-aware configuration.

Can students be pinned to one server instead of sharing sessions?

Sticky sessions on the balancer would avoid the Redis session store, but they defeat scaling: a node added mid-exam receives no existing students, and a node that fails logs out everyone it held. Shared sessions are the correct fix and are Moodle's recommended configuration for multiple web servers.

How do I keep the PHP layer's code identical across nodes?

Deploy Moodle code through the platform's Deployment Manager to the layer, which applies it to every node, sequentially if you choose. Stateful scaling copies the master node's file system to new nodes. Never edit code on one node by hand.

Seven servers for the exam, two for the term

Run Moodle on MassiveGRID PaaS with an auto-scaling PHP layer, Redis for sessions and cache, shared storage for moodledata and per-cloudlet-hour billing that drops when the exam ends. Free 14-day trial, no credit card.

Moodle on MassiveGRID PaaS

Further Reading