Skip to Content
MoodlePartie 4 — Installation et environnement de développementInstallation manuelle en production (Ubuntu, Nginx, PHP-FPM, PostgreSQL)

Chapitre 4.3 — Installation manuelle en production (Ubuntu, Nginx, PHP-FPM, PostgreSQL)

Dans les chapitres précédents, vous avez monté un environnement Moodle de développement en quelques minutes grâce à Docker. C’était confortable : les images faisaient le travail à votre place, les versions de PHP et de PostgreSQL étaient choisies pour vous, et le serveur web était déjà configuré. Ce chapitre prend le contre-pied : nous allons installer Moodle 5.2 à la main, pièce par pièce, sur un serveur Ubuntu 24.04 LTS nu, comme vous le feriez sur un VPS de production.

Pourquoi s’infliger cela alors que Docker existe ? Parce qu’un jour ou l’autre, vous devrez déboguer une instance de production qui ne tourne pas dans vos conteneurs, comprendre pourquoi le cron d’un client ne s’exécute plus, ou expliquer à un administrateur système pourquoi le vhost doit pointer sur public/ depuis Moodle 5.1. Une installation manuelle est le meilleur moyen de comprendre ce que les images Docker cachent : la frontière entre le code, la configuration et les données ; le dialogue entre Nginx et PHP-FPM ; le rôle exact de config.php et de moodledata.

💡 Pour un dev React : pensez à la différence entre next dev et un vrai déploiement de production avec next build + un reverse proxy + un process manager. Docker en dev, c’est next dev : tout est magique. Ce chapitre, c’est apprendre à déployer sans Vercel — et c’est ce qui vous rendra autonome le jour où « ça marche sur ma machine » ne suffira plus.

La stack cible de ce chapitre :

ComposantChoixPourquoi
OSUbuntu 24.04 LTS (Noble)LTS supportée jusqu’en 2029, PHP 8.3 dans les dépôts officiels
Serveur webNginxLéger, performant, config déclarative lisible
PHPPHP-FPM 8.3Minimum requis par Moodle 5.2 (8.4 également supporté)
Base de donnéesPostgreSQL 16Minimum requis par Moodle 5.2, excellent choix par défaut
Moodle5.2 (branche MOODLE_502_STABLE)Version cible de cette documentation

Mini-sommaire

  1. Pourquoi une installation manuelle
  2. Préparation du serveur Ubuntu 24.04
  3. PHP-FPM 8.3 et ses extensions
  4. PostgreSQL 16
  5. Récupération du code Moodle (git vs archive)
  6. moodledata : le répertoire de données
  7. Le vhost Nginx complet pour Moodle 5.1+/5.2
  8. Installation : installeur web et CLI
  9. config.php décortiqué ligne par ligne
  10. HTTPS avec Let’s Encrypt
  11. Le cron Moodle
  12. Maintenance et mise à jour en CLI
  13. Checklist sécurité production

1. Pourquoi savoir faire une installation manuelle

1.1 Docker cache, la prod expose

Quand vous lancez docker compose up avec une image Moodle préfabriquée, une trentaine de décisions sont prises pour vous : version de PHP, extensions compilées, valeurs de php.ini, utilisateur qui exécute le serveur web, emplacement de moodledata, configuration du cron. Tant que tout fonctionne, c’est parfait. Le jour où quelque chose casse — un upload de 500 Mo qui échoue, des sessions qui expirent bizarrement, un cron qui ne tourne plus — vous devez savoir où regarder, et pour cela il faut avoir assemblé la machine au moins une fois soi-même.

1.2 Trois raisons concrètes

  1. Comprendre la production. La majorité des Moodle de production dans le monde tournent sur des VM ou des serveurs dédiés administrés à la main (ou via Ansible), pas dans Kubernetes. Si vous développez des plugins pour des clients, leurs environnements ressembleront à ce que nous construisons ici.
  2. Déboguer efficacement. Un 502 Bad Gateway, c’est Nginx qui n’arrive pas à joindre PHP-FPM. Un White screen of death, c’est souvent une extension PHP manquante ou un memory_limit trop bas. Une page sans CSS, c’est presque toujours un problème de slash arguments (nous y reviendrons en détail dans la section Nginx). Ces diagnostics deviennent évidents une fois qu’on a monté la stack soi-même.
  3. Déployer sur un VPS à moindre coût. Pour un site de recette, une démo client ou un petit Moodle associatif, un VPS à 5 €/mois avec cette stack fait parfaitement l’affaire — pas besoin d’orchestrateur.

💡 Pour un dev React : l’architecture que nous allons monter est l’équivalent PHP d’un déploiement Node.js classique : Nginx joue le rôle du reverse proxy devant votre app, PHP-FPM celui du process manager (comme PM2 avec ses workers), et Moodle celui de votre application Next.js en mode standalone. La grande différence : PHP est sans état entre les requêtes — chaque requête HTTP recharge l’application de zéro (atténué par OPcache, l’équivalent d’un cache de compilation persistant).

1.3 Prérequis de ce chapitre

  • Un serveur Ubuntu 24.04 LTS (VPS, VM locale, ou WSL2 pour s’entraîner) avec accès root ou sudo.
  • Un nom de domaine pointant vers ce serveur (nous utiliserons moodle.example.com — remplacez-le partout).
  • Un accès SSH. Depuis Windows 11, le client OpenSSH est intégré à PowerShell :
# Depuis PowerShell sur votre poste Windows 11 ssh admin@moodle.example.com # ou avec une clé spécifique : ssh -i $env:USERPROFILE\.ssh\id_ed25519 admin@moodle.example.com

Toutes les commandes qui suivent s’exécutent sur le serveur Ubuntu, en bash.


2. Préparation du serveur

2.1 Mise à jour du système

Toujours commencer par mettre le système à jour. Sur une machine fraîche, cela peut inclure un nouveau noyau, donc prévoyez un redémarrage.

sudo apt update && sudo apt full-upgrade -y sudo apt autoremove -y # Redémarrer si un nouveau noyau a été installé [ -f /var/run/reboot-required ] && sudo reboot

2.2 Création d’un utilisateur d’administration (si vous êtes en root)

Si votre hébergeur vous a livré un accès root direct, créez immédiatement un utilisateur sudo dédié et travaillez avec lui. Ne restez jamais connecté en root pour l’administration quotidienne.

# En tant que root : adduser admin # choisissez un mot de passe fort usermod -aG sudo admin # Copier votre clé SSH publique vers ce nouvel utilisateur rsync --archive --chown=admin:admin ~/.ssh /home/admin # Tester dans un NOUVEAU terminal avant de fermer la session root : # ssh admin@moodle.example.com

Une fois l’accès admin validé, durcissez SSH :

sudo nano /etc/ssh/sshd_config

Modifiez (ou ajoutez) ces directives :

PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes

Puis rechargez le service :

sudo systemctl restart ssh

⚠️ Piège : ne désactivez PasswordAuthentication qu’après avoir vérifié que la connexion par clé fonctionne dans une session séparée. Sinon vous vous enfermez dehors, et selon l’hébergeur, la seule issue est la console de secours ou la réinstallation.

2.3 Pare-feu ufw

Ubuntu embarque ufw (Uncomplicated Firewall). Nous n’ouvrons que le strict nécessaire : SSH, HTTP et HTTPS.

sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' # ouvre 80 et 443 (le profil existe après l'installation de Nginx) sudo ufw enable sudo ufw status verbose

Si Nginx n’est pas encore installé au moment où vous configurez le pare-feu, ouvrez les ports numériquement :

sudo ufw allow 80/tcp sudo ufw allow 443/tcp

Notez ce que nous n’ouvrons pas : le port 5432 de PostgreSQL (la base reste locale) et le port 9000 / la socket de PHP-FPM (communication interne uniquement).

⚠️ Piège : activez la règle SSH avant ufw enable. Un ufw enable sans règle SSH sur un serveur distant coupe instantanément votre session et toute reconnexion.

2.4 Fuseau horaire et NTP

Moodle manipule énormément de dates : échéances de devoirs, ouvertures de quiz, logs, planification du cron. Un serveur à l’heure UTC alors que vos utilisateurs sont à Paris n’est pas un problème en soi (Moodle gère les fuseaux par utilisateur), mais un serveur dont l’horloge dérive est un vrai problème (sessions, jetons OAuth, planifications).

# Définir le fuseau horaire (au choix : UTC est aussi un choix valable et courant en prod) sudo timedatectl set-timezone Europe/Paris # Vérifier que la synchronisation NTP est active timedatectl status # "NTP service: active" doit apparaître ; sinon : sudo timedatectl set-ntp true

2.5 Paquets utilitaires

sudo apt install -y git curl unzip aspell aspell-fr ghostscript
  • git : pour cloner Moodle (méthode recommandée, voir section 5).
  • aspell + aspell-fr : correcteur orthographique utilisé par certains éditeurs de texte de Moodle.
  • ghostscript : requis par l’annotation PDF du module Devoir (assignfeedback_editpdf).

3. Installation de PHP-FPM 8.3 et des extensions

3.1 Rappel des exigences de Moodle 5.2

Moodle 5.2 exige au minimum PHP 8.3 (PHP 8.4 est également supporté), en 64 bits obligatoirement, avec l’extension sodium requise et max_input_vars à 5000 minimum. Ubuntu 24.04 fournit PHP 8.3 dans ses dépôts officiels : pas besoin du PPA ondrej/php ici (vous en aurez besoin si vous visez PHP 8.4 sur cette version d’Ubuntu, ou PHP 8.3 sur une Ubuntu plus ancienne).

3.2 La commande apt complète

Voici la commande qui installe PHP-FPM et toutes les extensions dont Moodle a besoin (obligatoires et fortement recommandées) :

sudo apt install -y \ php8.3-fpm \ php8.3-cli \ php8.3-pgsql \ php8.3-gd \ php8.3-intl \ php8.3-xml \ php8.3-mbstring \ php8.3-curl \ php8.3-zip \ php8.3-soap \ php8.3-opcache \ php8.3-ldap \ php8.3-redis \ php8.3-xmlrpc \ php8.3-bcmath \ php8.3-exif

Rôle de chaque extension :

ExtensionRôle dans MoodleStatut
php8.3-fpmLe gestionnaire de processus FastCGI qui exécute PHP pour NginxObligatoire
php8.3-cliPHP en ligne de commande — indispensable pour le cron et les scripts adminObligatoire
php8.3-pgsqlDriver PostgreSQL (remplacer par php8.3-mysql pour MySQL/MariaDB)Obligatoire (pour notre stack)
php8.3-gdManipulation d’images (avatars, redimensionnements, graphiques)Obligatoire
php8.3-intlInternationalisation ICU (tris, formats de dates localisés)Obligatoire
php8.3-xmlParsing XML (backups, web services, SCORM) — fournit dom, simplexml, xmlreaderObligatoire
php8.3-mbstringChaînes multi-octets (UTF-8 partout dans Moodle)Obligatoire
php8.3-curlRequêtes HTTP sortantes (flux, web services, OAuth, dépôts distants)Obligatoire
php8.3-zipArchives ZIP (backups de cours, import/export, plugins)Obligatoire
php8.3-soapWeb services SOAP (intégrations SIRH/SI legacy)Recommandée
php8.3-opcacheCache d’opcodes — indispensable pour les performancesFortement recommandée
php8.3-ldapAuthentification LDAP/Active DirectoryOptionnelle (très courante en entreprise)
php8.3-redisClient Redis pour sessions et cache MUCOptionnelle (recommandée en prod chargée)
php8.3-xmlrpcProtocole XML-RPC (anciens web services, Mahara)Optionnelle
php8.3-bcmath / php8.3-exifCalculs précis / métadonnées d’imagesOptionnelles mais peu coûteuses

L’extension sodium (chiffrement moderne, requise par Moodle 5.2) est compilée en dur dans le paquet PHP d’Ubuntu — vous n’avez rien à installer, mais vérifiez sa présence :

php -m | grep -E 'sodium|intl|pgsql|gd|opcache' php -v # doit afficher PHP 8.3.x

💡 Pour un dev React : les extensions PHP sont l’équivalent des modules natifs Node.js (comme sharp ou bcrypt) — du code compilé chargé par le runtime. La différence : en PHP elles s’installent au niveau du système via apt, pas dans le projet via npm. Il n’y a pas de package.json qui les déclare ; c’est la page admin/environment.php de Moodle qui joue ce rôle de vérificateur de prérequis.

3.3 Réglages php.ini — FPM et CLI

C’est un piège classique : PHP sur Ubuntu a deux fichiers php.ini distincts :

  • /etc/php/8.3/fpm/php.ini → utilisé par les requêtes web (via PHP-FPM) ;
  • /etc/php/8.3/cli/php.ini → utilisé par la ligne de commande (cron, scripts d’admin, installeur CLI).

Il faut modifier les deux, sinon votre installeur CLI ou votre cron se comportera différemment du site web.

Plutôt que d’éditer les gros php.ini à la main, utilisons le mécanisme conf.d : un fichier de surcharge propre, qui survivra aux mises à jour de paquets.

sudo tee /etc/php/8.3/fpm/conf.d/90-moodle.ini /etc/php/8.3/cli/conf.d/90-moodle.ini > /dev/null <<'EOF' ; ---- Réglages Moodle 5.2 ---- ; OBLIGATOIRE : Moodle exige >= 5000 (formulaires très volumineux : quiz, permissions) max_input_vars = 5000 ; Mémoire par processus PHP. 256M est un bon départ ; certaines restaurations ; de gros cours peuvent demander davantage (le CLI peut monter à 512M). memory_limit = 256M ; Taille maximale des fichiers uploadés (vidéos de cours, backups...) ; Ces deux valeurs vont ensemble : post_max_size doit être >= upload_max_filesize. upload_max_filesize = 512M post_max_size = 512M ; Durée max d'exécution d'un script web (les CLI ne sont pas limités) max_execution_time = 300 ; Encodage et divers default_charset = UTF-8 file_uploads = On EOF

⚠️ Piège : client_max_body_size côté Nginx (section 7) doit être cohérent avec post_max_size côté PHP. Si Nginx limite à 1 Mo (sa valeur par défaut !) alors que PHP accepte 512 Mo, tous les gros uploads échoueront avec une erreur 413 avant même d’atteindre PHP. Trois curseurs à aligner : client_max_body_size (Nginx), post_max_size et upload_max_filesize (PHP), plus le réglage « Taille maximale des fichiers déposés » dans l’administration Moodle.

3.4 OPcache : les réglages recommandés par Moodle

OPcache garde en mémoire le bytecode compilé des scripts PHP, évitant de re-parser les milliers de fichiers de Moodle à chaque requête. Sans OPcache, Moodle est 3 à 5 fois plus lent. La documentation Moodle recommande ces valeurs :

sudo tee /etc/php/8.3/fpm/conf.d/91-moodle-opcache.ini > /dev/null <<'EOF' [opcache] opcache.enable = 1 ; Mémoire allouée au cache d'opcodes. Moodle a BEAUCOUP de fichiers PHP. opcache.memory_consumption = 256 ; Nombre max de fichiers cachés — Moodle en a plus de 10 000 opcache.max_accelerated_files = 10000 ; Cache des chaînes internées (noms de classes, etc.) opcache.interned_strings_buffer = 16 ; Vérifier les modifications de fichiers (laisser à 1 sauf déploiements immuables) opcache.validate_timestamps = 1 opcache.revalidate_freq = 60 ; Recommandé par Moodle opcache.use_cwd = 1 opcache.save_comments = 1 opcache.enable_file_override = 0 EOF

Quelques commentaires :

  • revalidate_freq = 60 : PHP ne revérifie la date de modification d’un fichier qu’une fois par minute. En production, où le code ne change qu’au déploiement, c’est un excellent compromis. Sur un serveur de recette où vous poussez du code souvent, descendez à 0 ou purgez OPcache après chaque déploiement (sudo systemctl reload php8.3-fpm).
  • save_comments = 1 est requis : Moodle lit les annotations dans les docblocks.

💡 Pour un dev React : OPcache ≈ le cache de build de Next.js (.next/cache) combiné au module cache de Node. Sans lui, ce serait comme si next build recompilait toute l’app à chaque requête HTTP. Et comme avec un build Next mal invalidé, un OPcache périmé après déploiement produit des bugs fantômes : le réflexe après un déploiement est systemctl reload php8.3-fpm, l’équivalent de supprimer .next et rebuilder.

3.5 Le pool PHP-FPM : dimensionner pm.max_children

PHP-FPM gère un pool de processus workers. Chaque requête PHP occupe un worker pendant toute sa durée. Le fichier de pool par défaut est /etc/php/8.3/fpm/pool.d/www.conf. Les directives clés :

sudo nano /etc/php/8.3/fpm/pool.d/www.conf
; Utilisateur/groupe des workers — www-data par défaut, on n'y touche pas user = www-data group = www-data ; Socket Unix sur laquelle Nginx enverra les requêtes FastCGI listen = /run/php/php8.3-fpm.sock listen.owner = www-data listen.group = www-data ; --- Dimensionnement du pool --- ; dynamic : le nombre de workers varie entre min et max selon la charge pm = dynamic ; Nombre MAXIMUM de workers simultanés. ; Règle de calcul : (RAM disponible pour PHP) / (mémoire moyenne par worker). ; Un worker Moodle consomme typiquement 60 à 120 Mo. ; Exemple pour une VM de 4 Go dont ~2,5 Go pour PHP : 2500 / 100 ≈ 25. pm.max_children = 25 ; Workers démarrés au boot pm.start_servers = 5 ; Bornes du nombre de workers inactifs maintenus en réserve pm.min_spare_servers = 3 pm.max_spare_servers = 8 ; Recycler chaque worker après N requêtes (contre les fuites mémoire) pm.max_requests = 500 ; Journaliser les requêtes lentes (> 10 s) avec leur stack trace — précieux en prod slowlog = /var/log/php8.3-fpm-slow.log request_slowlog_timeout = 10s

Après toute modification :

sudo systemctl restart php8.3-fpm sudo systemctl enable php8.3-fpm systemctl status php8.3-fpm --no-pager

⚠️ Piège : un pm.max_children trop bas provoque des files d’attente (le site « rame » alors que le CPU est au repos — surveillez le message server reached pm.max_children dans /var/log/php8.3-fpm.log). Trop haut, il provoque du swap puis l’OOM killer, ce qui est bien pire. Mesurez la mémoire réelle de vos workers avec ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$1} END {print s/NR/1024 " Mo/worker"}' et ajustez.


4. PostgreSQL 16

4.1 Installation

Moodle 5.2 requiert PostgreSQL 16 minimum. Bonne nouvelle : c’est exactement la version des dépôts d’Ubuntu 24.04.

sudo apt install -y postgresql postgresql-contrib psql --version # psql (PostgreSQL) 16.x sudo systemctl enable --now postgresql

Par défaut, PostgreSQL n’écoute que sur localhost (listen_addresses = 'localhost') : parfait pour notre architecture mono-serveur, rien à ouvrir dans le pare-feu.

4.2 Création de l’utilisateur et de la base

PostgreSQL utilise l’authentification « peer » pour l’utilisateur système postgres : on passe par sudo -u postgres pour administrer.

sudo -u postgres psql

Dans le shell psql, créez le rôle applicatif puis la base :

-- Un rôle dédié à Moodle, avec droit de connexion et un mot de passe FORT. -- Générez-en un : openssl rand -base64 24 CREATE USER moodleuser WITH PASSWORD 'RemplacezParUnVraiMotDePasseFort'; -- La base, détenue par ce rôle, en UTF-8 obligatoirement. CREATE DATABASE moodle WITH OWNER = moodleuser ENCODING = 'UTF8' LC_COLLATE = 'fr_FR.UTF-8' LC_CTYPE = 'fr_FR.UTF-8' TEMPLATE = template0; -- Vérification \l moodle \q

Notes importantes :

  • ENCODING 'UTF8' est non négociable : Moodle refuse toute base dans un autre encodage.
  • TEMPLATE = template0 est requis dès qu’on spécifie une locale différente de celle du cluster.
  • Si la locale fr_FR.UTF-8 n’existe pas sur le serveur (locale -a | grep fr_FR), générez-la d’abord : sudo locale-gen fr_FR.UTF-8 && sudo systemctl restart postgresql. Sinon, LC_COLLATE = 'C.UTF-8' fonctionne aussi (tris moins « français » mais plus rapides).

Testez la connexion applicative (via TCP, comme le fera Moodle) :

psql "host=localhost dbname=moodle user=moodleuser password=RemplacezParUnVraiMotDePasseFort" -c "SELECT version();"

4.3 pg_hba.conf et scram-sha-256

Le fichier /etc/postgresql/16/main/pg_hba.conf définit qui peut se connecter, d’où, et comment s’authentifier. Sur PostgreSQL 16 / Ubuntu 24.04, la méthode par défaut pour les connexions TCP locales est déjà scram-sha-256 (le hachage moderne, successeur de md5). Vérifiez :

sudo grep -E '^(local|host)' /etc/postgresql/16/main/pg_hba.conf

Vous devez voir des lignes de ce type :

local all all peer host all all 127.0.0.1/32 scram-sha-256 host all all ::1/128 scram-sha-256

Vérifiez aussi que le paramètre de hachage des mots de passe est bien en scram :

sudo -u postgres psql -c "SHOW password_encryption;" # doit renvoyer : scram-sha-256

Si vous migrez depuis un vieux cluster où password_encryption valait md5, repassez-le à scram-sha-256 puis ré-exécutez ALTER USER moodleuser WITH PASSWORD '...'; (le hachage n’est recalculé qu’à la définition du mot de passe). Après toute modification de pg_hba.conf :

sudo systemctl reload postgresql

⚠️ Piège : si Moodle affiche « password authentication failed » alors que le mot de passe est correct, c’est souvent un décalage md5/scram : le mot de passe a été haché en md5 avant le passage à scram-sha-256. Redéfinissez simplement le mot de passe pour régénérer le hash.

4.4 Réglages de base de postgresql.conf

Les valeurs par défaut de PostgreSQL sont volontairement timides (128 Mo de shared_buffers). Pour une VM de 4 Go dédiée à Moodle, un point de départ raisonnable dans /etc/postgresql/16/main/postgresql.conf :

# ~25 % de la RAM totale shared_buffers = 1GB # Estimation du cache disque total (RAM - reste de la stack), utilisée par le planificateur effective_cache_size = 2GB # Mémoire par opération de tri/jointure (par connexion !) work_mem = 16MB # Pour les VACUUM, CREATE INDEX... maintenance_work_mem = 256MB # SSD : coût de lecture aléatoire quasi égal au séquentiel random_page_cost = 1.1 # Connexions max — Moodle via FPM en ouvre une par worker PHP. # pm.max_children (25) + cron + marge => 100 (défaut) est suffisant. max_connections = 100
sudo systemctl restart postgresql

📚 Aller plus loin : le site PGTune  génère une configuration adaptée à votre RAM/CPU/type de disque. Côté Moodle, la page « PostgreSQL » de docs.moodle.org détaille d’autres optimisations (notamment full_page_writes et les checkpoints pour les grosses instances).


5. Récupération du code de Moodle

5.1 Git vs archive : le match

Deux façons officielles d’obtenir le code :

Option A — Archive tgz/zip depuis download.moodle.org  : simple, mais chaque mise à jour mineure (5.2 → 5.2.1) impose de re-télécharger, décompresser, recopier config.php et les plugins tiers. Fastidieux et propice aux erreurs.

Option B — Clone git de la branche stable : c’est la méthode que je recommande sans hésiter. Les mises à jour mineures deviennent un simple git pull (les correctifs de sécurité hebdomadaires arrivent sur la branche MOODLE_502_STABLE), et git vous donne un historique, des diffs, la possibilité de revenir en arrière.

💡 Pour un dev React : c’est la même logique que suivre une branche de release plutôt que de télécharger des zips de GitHub Releases. La branche MOODLE_502_STABLE reçoit uniquement des correctifs (patch releases), jamais de nouvelles fonctionnalités — l’équivalent d’une branche 5.2.x en semver. La branche main de Moodle, elle, est l’équivalent de canary : ne la déployez jamais en production.

5.2 Clonage

Nous installons le code dans /var/www/moodle :

cd /var/www sudo git clone -b MOODLE_502_STABLE --single-branch --depth 10 \ https://github.com/moodle/moodle.git moodle

Explication des options :

  • -b MOODLE_502_STABLE : on se place directement sur la branche stable de la série 5.2.
  • --single-branch : ne télécharge que cette branche (le dépôt Moodle complet avec toutes les branches est volumineux).
  • --depth 10 : historique tronqué — suffisant pour la prod, clone beaucoup plus rapide. Omettez-le si vous voulez l’historique complet (utile pour git log/git blame sur un serveur de dev).

Alternative miroir officiel : https://git.moodle.org/git/moodle.git (le dépôt GitHub moodle/moodle en est le miroir public le plus rapide).

Variante archive, si vous y tenez :

cd /tmp wget https://download.moodle.org/download.php/direct/stable502/moodle-latest-502.tgz sudo mkdir -p /var/www sudo tar -xzf moodle-latest-502.tgz -C /var/www

5.3 La structure du code depuis Moodle 5.1 : le répertoire public/

Point crucial pour Moodle 5.1 et 5.2. Jusqu’à Moodle 5.0, tout le code était servi par le serveur web : index.php, mais aussi config.php, vendor/, composer.json… Depuis Moodle 5.1, le code a été restructuré : tout ce qui doit être accessible en HTTP vit dans le sous-répertoire public/, et le reste est hors de portée du web.

/var/www/moodle/ ← racine du PROJET ($CFG->root, en lecture seule) ├── config.php ← la configuration — reste ICI, à la racine, HORS webroot ├── public/ ← le webroot ($CFG->dirroot) — SEUL répertoire servi par Nginx │ ├── index.php │ ├── admin/ │ │ └── cli/ ← les scripts CLI (cron.php, install.php, upgrade.php...) │ ├── course/, mod/, lib/, theme/, login/ ... ├── vendor/ ← dépendances Composer — hors webroot (si présent) ├── node_modules/ ← outillage front — hors webroot (si présent) ├── composer.json, package.json, Gruntfile.js ...

Trois conséquences pratiques à mémoriser dès maintenant (nous y reviendrons dans les sections Nginx et config.php) :

  1. Le vhost Nginx pointe sur /var/www/moodle/public, jamais sur /var/www/moodle.
  2. $CFG->wwwroot reste https://moodle.example.com — il ne doit jamais se terminer par /public (Moodle lève l’erreur « Incorrect $CFG->wwwroot … it should not end in /public »).
  3. Les scripts CLI se lancent depuis la racine du projet : php public/admin/cli/cron.php (et non plus php admin/cli/cron.php).

Le bénéfice sécurité est majeur : config.php (qui contient le mot de passe de la base !), vendor/, node_modules/ et les fichiers de build ne sont physiquement plus atteignables par une URL, même en cas d’erreur de configuration du serveur web.

💡 Pour un dev React : c’est exactement le pattern du dossier public/ de Next.js ou du dist/ d’un build Vite : on ne sert au navigateur que ce qui est destiné au navigateur. next.config.js, .env.local et node_modules ne sont jamais dans le webroot — Moodle a mis 20 ans à adopter cette hygiène, mais depuis 5.1 c’est fait.

5.4 Propriété et permissions du code

Règle d’or de la documentation Moodle : le serveur web ne doit PAS pouvoir écrire dans le code. Les fichiers appartiennent à root (ou à un utilisateur de déploiement dédié), et www-data (l’utilisateur de Nginx/PHP-FPM) n’a que le droit de lecture.

# Propriétaire : root (ou votre utilisateur de déploiement, ex. "deploy") sudo chown -R root:root /var/www/moodle # Répertoires : traversables et lisibles par tous ; fichiers : lisibles par tous sudo find /var/www/moodle -type d -exec chmod 755 {} \; sudo find /var/www/moodle -type f -exec chmod 644 {} \;

Pourquoi c’est important : si www-data peut écrire dans le code, alors n’importe quelle faille dans un plugin permet à un attaquant de modifier le code de Moodle lui-même (webshell, backdoor). En lecture seule, la surface d’attaque est drastiquement réduite. La contrepartie : l’installation de plugins via l’interface d’administration ne fonctionnera pas (Moodle détecte qu’il ne peut pas écrire) — vous installerez les plugins en CLI ou par git, ce qui est de toute façon la bonne pratique en production.

⚠️ Piège : de nombreux tutoriels obsolètes recommandent chown -R www-data:www-data /var/www/moodle. Ne le faites pas. C’est le réglage de la facilité (l’installeur web peut alors écrire config.php lui-même), mais c’est une faille de sécurité béante en production. Nous verrons dans la section 8 comment gérer proprement la création de config.php.


6. moodledata : le répertoire de données

6.1 Rôle et emplacement

moodledata (le « dataroot ») est le répertoire où Moodle stocke tout ce qui n’est ni du code ni des données relationnelles : fichiers déposés par les utilisateurs, caches, sessions, fichiers temporaires. Il doit être :

  • hors du webroot — jamais dans public/, jamais sous /var/www/moodle ;
  • inscriptible par www-data — c’est le seul endroit (avec les caches) où le serveur web écrit ;
  • sauvegardé — il contient des données irremplaçables (les fichiers des cours).
sudo mkdir -p /var/moodledata sudo chown -R www-data:www-data /var/moodledata sudo chmod -R 770 /var/moodledata

770 : lecture/écriture/exécution pour www-data (propriétaire et groupe), rien pour les autres. Certains guides utilisent 700 (plus strict, très bien aussi) ou 2770 (setgid pour propager le groupe).

Pourquoi jamais dans public/ ? Parce que Moodle contrôle l’accès à chaque fichier via PHP (est-ce que cet utilisateur est inscrit au cours qui contient ce PDF ?). Si moodledata était servi directement par Nginx, n’importe qui pourrait télécharger n’importe quel fichier en devinant son URL — y compris les copies d’examens, les sauvegardes de cours complètes et les données de sessions. C’est l’une des erreurs de configuration les plus graves possibles sur un Moodle.

6.2 Structure interne

Après quelques jours d’utilisation, moodledata ressemble à ceci :

/var/moodledata/ ├── filedir/ ← LE trésor : tous les fichiers, stockés par hash SHA1 de contenu │ ├── 0a/ │ │ └── 3f/ │ │ └── 0a3f5e9b1c... (le fichier lui-même, nommé par son SHA1 complet) ├── cache/ ← cache applicatif partagé (MUC niveau "application") ├── localcache/ ← cache local au serveur (purgeable sans risque) ├── sessions/ ← sessions PHP (si stockées en fichiers — voir Redis, section 9) ├── temp/ ← fichiers temporaires (imports, conversions en cours) ├── trashdir/ ← corbeille : fichiers supprimés en attente de purge par le cron ├── lang/ ← packs de langue téléchargés (dont le français) ├── muc/ ← configuration du Moodle Universal Cache └── lock/ ← verrous de cron et de tâches

Le point le plus intéressant est filedir/ : Moodle utilise un stockage adressé par contenu (content-addressable storage). Chaque fichier est nommé par le SHA1 de son contenu et rangé dans une arborescence aa/bb/ dérivée des premiers caractères du hash. Conséquences élégantes :

  • Déduplication automatique : si 200 étudiants déposent le même PDF, il n’est stocké qu’une fois. La table mdl_files en base fait le lien entre les « fichiers logiques » (nom, cours, zone) et le blob physique.
  • Les noms de fichiers réels, les chemins, les métadonnées vivent en base de données, pas sur le disque. Ne cherchez jamais un fichier par son nom dans filedir/ — passez par la table mdl_files.

💡 Pour un dev React : filedir/ fonctionne exactement comme un bucket S3 utilisé en content-addressable (ou comme le store d’objets de git : .git/objects/aa/bbcc...). D’ailleurs Moodle peut réellement brancher un stockage objet à la place du disque local via $CFG->alternative_file_system_class (voir section 9). Et cache/ vs localcache/, c’est la distinction entre un cache Redis partagé entre instances et un cache en mémoire locale par instance.

⚠️ Piège : ne supprimez jamais de fichiers directement dans filedir/ pour « faire de la place » : vous corrompriez des cours (la base référencerait des blobs disparus). La purge légitime passe par la corbeille de Moodle et le cron (trashdir/). En revanche, localcache/, cache/ et temp/ peuvent être purgés via php public/admin/cli/purge_caches.php.

📚 Aller plus loin : sur les grosses installations, moodledata est souvent monté sur un stockage réseau (NFS) pour être partagé entre plusieurs serveurs frontaux, ou remplacé par S3 via le plugin tool_objectfs. La page « File API internals » de moodledev.io décrit le fonctionnement complet du File Storage.


7. Le vhost Nginx complet

7.1 Installation de Nginx

sudo apt install -y nginx sudo systemctl enable --now nginx

7.2 Le problème des « slash arguments »

Avant de voir la config, comprenons LA spécificité de Moodle côté serveur web. Moodle sert les fichiers des cours via des URL de la forme :

https://moodle.example.com/pluginfile.php/123/mod_resource/content/1/poly.pdf

Remarquez : le chemin continue après .php. La partie /123/mod_resource/... est le PATH_INFO, que le script pluginfile.php analyse pour retrouver le fichier. C’est ce que Moodle appelle les slash arguments. Beaucoup de configurations Nginx génériques ne transmettent pas correctement PATH_INFO à PHP-FPM ; résultat : les fichiers, images et parfois les CSS des thèmes ne se chargent plus (Moodle a un mode dégradé, mais il casse certaines fonctionnalités comme les packages SCORM). Notre config gère cela explicitement avec fastcgi_split_path_info.

7.3 Le vhost complet et commenté (Moodle 5.1+/5.2)

Créez le fichier /etc/nginx/sites-available/moodle :

sudo nano /etc/nginx/sites-available/moodle
# ===================================================================== # Vhost Nginx pour Moodle 5.2 (structure public/ de Moodle 5.1+) # Version HTTP initiale — le HTTPS sera ajouté par certbot (section 10) # ===================================================================== server { listen 80; listen [::]:80; server_name moodle.example.com; # POINT CRITIQUE depuis Moodle 5.1 : le webroot est le sous-répertoire # public/ du projet, PAS la racine du projet. config.php, vendor/, # node_modules/ restent ainsi physiquement hors de portée du web. root /var/www/moodle/public; index index.php; # Doit être cohérent avec post_max_size / upload_max_filesize de PHP. # (défaut Nginx : 1m — beaucoup trop bas pour Moodle !) client_max_body_size 512M; # Journaux dédiés à ce site access_log /var/log/nginx/moodle.access.log; error_log /var/log/nginx/moodle.error.log; # ------------------------------------------------------------------ # Routage principal : servir le fichier demandé s'il existe, # sinon le répertoire, sinon 404. Moodle n'a pas besoin de # réécriture "attrape-tout" pour un fonctionnement standard. # (Variante "routing engine" : try_files $uri $uri/ /r.php; — # active les URL propres via le routeur de Moodle.) # ------------------------------------------------------------------ location / { try_files $uri $uri/ =404; } # ------------------------------------------------------------------ # PHP + slash arguments. # Matche "script.php" ET "script.php/chemin/apres" (PATH_INFO). # fastcgi_split_path_info découpe l'URI en deux : # $fastcgi_script_name = /pluginfile.php # $fastcgi_path_info = /123/mod_resource/content/1/poly.pdf # ------------------------------------------------------------------ location ~ ^(.+\.php)(/.*)?$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; # $fastcgi_path_info est volatile : on le fige dans une variable # AVANT try_files, qui l'écraserait sinon (piège Nginx classique). set $path_info $fastcgi_path_info; # Vérifie que le script PHP existe réellement ; sinon 404. # Empêche l'exécution de "faux" scripts (/upload.jpg/x.php). try_files $fastcgi_script_name =404; # La socket Unix du pool PHP-FPM (définie dans www.conf) fastcgi_pass unix:/run/php/php8.3-fpm.sock; include fastcgi_params; fastcgi_param PATH_INFO $path_info; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; # Confort pour les grosses réponses PHP (rapports, exports) fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; fastcgi_read_timeout 300; } # ------------------------------------------------------------------ # Sécurité : blocages explicites # ------------------------------------------------------------------ # Fichiers et répertoires cachés (.git, .htaccess, .env...), # sauf .well-known (nécessaire à Let's Encrypt / ACME). location ~ /\.(?!well-known) { deny all; return 404; } # Ceintures et bretelles : avec la structure public/ de Moodle 5.1+, # vendor/ et node_modules/ sont déjà HORS du webroot. Ce bloc ne sert # que si vous déployez un jour une version <= 5.0 (structure legacy) # ou un plugin mal empaqueté qui embarque son propre vendor/. location ~ (/vendor/|/node_modules/|composer\.json|/readme|/README|/upgrade\.txt|/UPGRADING\.md|db/install\.xml|/fixtures/|/behat/|phpunit\.xml) { deny all; return 404; } # Cache navigateur long pour les assets statiques versionnés par Moodle location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { try_files $uri =404; expires 7d; access_log off; } }

Activez le site et testez :

sudo ln -s /etc/nginx/sites-available/moodle /etc/nginx/sites-enabled/moodle sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t # DOIT afficher "syntax is ok" et "test is successful" sudo systemctl reload nginx

Quelques explications supplémentaires sur les choix faits :

  • SCRIPT_FILENAME $realpath_root$fastcgi_script_name : $realpath_root résout les liens symboliques, ce qui rend la config compatible avec les déploiements par symlink (pattern à la Capistrano : current -> releases/2026-07-08) et évite des incohérences OPcache.
  • try_files $fastcgi_script_name =404 dans le bloc PHP : sans cela, une URL comme /dataroot/avatar.jpg/index.php pourrait, selon la config, faire exécuter du contenu uploadé comme du PHP. Ici, si le fichier .php n’existe pas dans le webroot, c’est 404, point.
  • Le bloc regex ^(.+\.php)(/.*)?$ matche à la fois /index.php et /pluginfile.php/..., contrairement au classique \.php$ qui rate les slash arguments.

⚠️ Piège : le piège Nginx n° 1 avec PHP-FPM : $fastcgi_path_info est réinitialisé par try_files. Si vous écrivez fastcgi_param PATH_INFO $fastcgi_path_info; directement, PATH_INFO arrivera vide dans PHP et les slash arguments seront silencieusement cassés — symptôme typique : les images des cours renvoient des 404 ou le thème perd ses CSS. D’où le set $path_info $fastcgi_path_info; placé AVANT try_files. Ce détail fait perdre des heures à beaucoup d’admins.

⚠️ Piège : après l’installation, vérifiez que les slash arguments fonctionnent : Administration du site → Développement → Purger les caches, puis visitez https://moodle.example.com/theme/image.php/boost/core/1/moodlelogo — si l’image s’affiche, PATH_INFO est bien transmis. Moodle propose un test similaire dans Administration du site → Serveur → Environnement.

7.4 L’équivalent Apache, pour comparaison

Beaucoup de documentation Moodle suppose Apache. Voici l’équivalent de notre vhost, pour que vous sachiez lire les deux :

# /etc/apache2/sites-available/moodle.conf <VirtualHost *:80> ServerName moodle.example.com # Même point critique : DocumentRoot sur public/, pas sur la racine DocumentRoot /var/www/moodle/public <Directory /var/www/moodle/public> Options -Indexes -MultiViews # Moodle n'a pas besoin de .htaccess ; None = plus rapide et plus sûr AllowOverride None Require all granted </Directory> # Slash arguments : Apache les gère nativement via AcceptPathInfo <FilesMatch "\.php$"> SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost" </FilesMatch> ErrorLog ${APACHE_LOG_DIR}/moodle.error.log CustomLog ${APACHE_LOG_DIR}/moodle.access.log combined </VirtualHost>

Différences notables : Apache gère PATH_INFO nativement (pas de gymnastique fastcgi_split_path_info), et le mécanisme .htaccess/AllowOverride n’a pas d’équivalent Nginx (toute la config Nginx est centralisée — ce qui est un avantage en lisibilité et en performance).

📚 Aller plus loin : la page « Nginx » de docs.moodle.org contient des variantes avancées : X-Accel-Redirect ($CFG->xsendfile) pour déléguer l’envoi des gros fichiers de moodledata à Nginx au lieu de PHP (gain de performance majeur sur les sites riches en vidéos), et le « routing engine » (try_files $uri $uri/ /r.php;) qui active les belles URL du nouveau routeur de Moodle.


8. Installation : les deux voies

Le code est en place, la base existe, Nginx et PHP-FPM tournent. Il reste à exécuter l’installeur, qui va créer les ~500 tables et le compte administrateur, et générer config.php. Deux voies : l’installeur web (pédagogique) et le CLI (recommandé, scriptable).

8.1 Voie n° 1 : l’installeur web

Ouvrez http://moodle.example.com dans un navigateur. Le déroulé écran par écran :

  1. Choix de la langue : sélectionnez Français (fr). Cela ne concerne que l’installation ; le pack de langue complet sera téléchargé ensuite.
  2. Confirmation des chemins : Moodle affiche l’adresse web (wwwroot), le répertoire Moodle et le répertoire de données. Saisissez /var/moodledata comme répertoire de données. Si Moodle affiche un avertissement sur le répertoire de données, vérifiez les permissions de la section 6.
  3. Choix du pilote de base de données : « PostgreSQL (native/pgsql) ».
  4. Réglages de la base de données : hôte localhost, base moodle, utilisateur moodleuser, le mot de passe, préfixe des tables mdl_ (le préfixe doit faire 10 caractères maximum), port et socket vides.
  5. Écriture de config.php : c’est ici que notre choix « code en lecture seule » se manifeste. Comme www-data ne peut pas écrire dans /var/www/moodle, Moodle affiche le contenu de config.php à copier. Copiez-le, puis sur le serveur :
sudo nano /var/www/moodle/config.php # coller le contenu affiché sudo chown root:www-data /var/www/moodle/config.php sudo chmod 640 /var/www/moodle/config.php

(640 : lisible par www-data via le groupe, modifiable seulement par root, illisible pour les autres utilisateurs du système.) 6. Licence et vérifications d’environnement : Moodle valide la version de PHP, chaque extension, max_input_vars, la version de PostgreSQL. Tout doit être vert — si un point est rouge, retournez aux sections 3-4. 7. Création des tables : quelques minutes de barres de progression. 8. Compte administrateur : nom d’utilisateur (admin par défaut), mot de passe fort, adresse mail. 9. Réglages de la page d’accueil : nom complet du site, nom abrégé, fuseau horaire par défaut.

C’est fonctionnel, mais cliquable seulement. Pour de l’infrastructure reproductible, préférez la voie suivante.

8.2 Voie n° 2 : l’installation en CLI (recommandée)

L’installeur CLI fait tout cela en une seule commande non interactive — idéale pour vos scripts de provisioning, et cohérente avec vos réflexes d’automatisation. Rappel : depuis Moodle 5.1, les scripts CLI vivent dans public/admin/cli/ et se lancent depuis la racine du projet.

Il faut l’exécuter sous l’utilisateur www-data pour que les fichiers créés dans moodledata aient le bon propriétaire :

cd /var/www/moodle sudo -u www-data /usr/bin/php public/admin/cli/install.php \ --lang=fr \ --wwwroot="https://moodle.example.com" \ --dataroot=/var/moodledata \ --dbtype=pgsql \ --dbhost=localhost \ --dbname=moodle \ --dbuser=moodleuser \ --dbpass='RemplacezParUnVraiMotDePasseFort' \ --prefix=mdl_ \ --fullname="Plateforme de formation Exemple" \ --shortname="Exemple" \ --adminuser=admin \ --adminpass='UnMotDePasseAdminTresFort!42' \ --adminemail=alexlemia13@gmail.com \ --agree-license \ --non-interactive

Chaque option expliquée :

OptionRôle
--lang=frLangue par défaut du site ; télécharge et installe le pack français
--wwwrootURL publique canonique du site. Mettez directement le https:// si vous allez configurer TLS juste après (section 10), même si le certificat n’existe pas encore — sinon il faudra éditer config.php ensuite. Pas de slash final, pas de /public.
--datarootChemin absolu de moodledata (doit exister et être inscriptible par www-data)
--dbtype=pgsqlPilote : pgsql, mysqli, mariadb, sqlsrv ou auroramysql
--dbhost / --dbname / --dbuser / --dbpassConnexion à la base créée en section 4
--prefix=mdl_Préfixe de toutes les tables (max 10 caractères). Permet de cohabiter avec d’autres applis dans la même base — gardez mdl_, c’est la convention universelle
--fullname / --shortnameNom complet (page d’accueil, mails) et nom court (fil d’Ariane) du site
--adminuser / --adminpass / --adminemailLe compte administrateur initial
--agree-licenseAccepte la GPL sans prompt
--non-interactiveÉchoue au lieu de poser des questions — indispensable en script

Le script crée les tables, le compte admin, et écrit config.php… sauf qu’il ne peut pas, puisque www-data n’a pas le droit d’écrire dans /var/www/moodle ! Deux stratégies propres :

Stratégie A (la plus simple) : rendre la racine temporairement inscriptible, le temps de l’installation :

sudo chown www-data /var/www/moodle # temporaire ! # ... lancer install.php comme ci-dessus ... sudo chown root:root /var/www/moodle # on referme sudo chown root:www-data /var/www/moodle/config.php sudo chmod 640 /var/www/moodle/config.php

Stratégie B (infrastructure as code) : écrire config.php vous-même d’abord (à partir du modèle de la section 9), puis utiliser le script frère install_database.php, qui fait la même chose qu’install.php mais suppose que config.php existe déjà et ne crée que la base et l’admin :

cd /var/www/moodle sudo -u www-data /usr/bin/php public/admin/cli/install_database.php \ --lang=fr \ --fullname="Plateforme de formation Exemple" \ --shortname="Exemple" \ --adminuser=admin \ --adminpass='UnMotDePasseAdminTresFort!42' \ --adminemail=alexlemia13@gmail.com \ --agree-license

C’est la variante que vous retiendrez pour Ansible/Terraform : le config.php est un template versionné dans votre outil de config management, et install_database.php est idempotent-ish (il refuse de tourner si les tables existent déjà).

⚠️ Piège : lancez toujours les CLI Moodle avec sudo -u www-data. Si vous les lancez en root, les fichiers créés dans moodledata (caches, packs de langue) appartiendront à root, et le site web — qui tourne en www-data — se mettra à échouer avec des erreurs d’écriture incompréhensibles. Si c’est déjà arrivé : sudo chown -R www-data:www-data /var/moodledata répare.

À ce stade, le site répond en HTTP. Ne vous connectez pas encore pour configurer des choses : passons d’abord au fichier le plus important de tout Moodle.


9. config.php décortiqué

config.php est l’unique fichier de configuration de Moodle côté code. Tout le reste (des milliers de réglages) vit en base de données et s’édite via l’interface d’administration ; config.php ne contient que ce dont Moodle a besoin avant de pouvoir se connecter à la base, plus les réglages qu’on veut verrouiller au niveau infrastructure.

💡 Pour un dev React : config.phpnext.config.js + .env.production fusionnés. Comme .env, il contient les secrets (credentials de la base) et ne doit jamais être commité tel quel dans git ni exposé par le serveur web. Comme next.config.js, c’est du vrai code exécuté au bootstrap — vous pouvez y mettre des conditions (if (getenv('MOODLE_ENV') === 'staging') { ... }), lire des variables d’environnement avec getenv(), etc. Un pattern courant en conteneur : $CFG->dbpass = getenv('MOODLE_DB_PASSWORD');.

9.1 Le fichier généré, en entier

Voici (à quelques commentaires près) ce que l’installeur a généré dans /var/www/moodle/config.php :

<?php // Moodle configuration file unset($CFG); global $CFG; $CFG = new stdClass(); $CFG->dbtype = 'pgsql'; $CFG->dblibrary = 'native'; $CFG->dbhost = 'localhost'; $CFG->dbname = 'moodle'; $CFG->dbuser = 'moodleuser'; $CFG->dbpass = 'RemplacezParUnVraiMotDePasseFort'; $CFG->prefix = 'mdl_'; $CFG->dboptions = array ( 'dbpersist' => 0, 'dbport' => '', 'dbsocket' => '', ); $CFG->wwwroot = 'https://moodle.example.com'; $CFG->dataroot = '/var/moodledata'; $CFG->admin = 'admin'; $CFG->directorypermissions = 0777; require_once(__DIR__ . '/public/lib/setup.php'); // There is no php closing tag in this file, // it is intentional because it prevents trailing whitespace problems!

Notez la dernière ligne : depuis Moodle 5.1, config.php (à la racine du projet) inclut public/lib/setup.php — la preuve que le fichier vit bien au-dessus du webroot. Décortiquons maintenant chaque directive, puis les options avancées que l’installeur n’écrit pas mais que vous utiliserez.

9.2 Le bloc base de données

$CFG->dbtype — le pilote. Valeurs possibles : pgsql (PostgreSQL — notre choix), mysqli (MySQL), mariadb (MariaDB — pilote distinct de mysqli depuis longtemps, à utiliser pour MariaDB), sqlsrv (SQL Server) et auroramysql (Amazon Aurora MySQL). Ce choix est fait une fois pour toutes : on ne migre pas de SGBD en changeant cette ligne (il faudrait une migration de données complète).

$CFG->dblibrary — toujours 'native' de nos jours. Vestige de l’époque où Moodle supportait plusieurs couches d’abstraction (ADOdb). Ne changez jamais cette valeur.

$CFG->dbhost — l’hôte de la base. localhost a une subtilité selon le SGBD ; pour PostgreSQL avec le driver pgsql, localhost passe par TCP sur 127.0.0.1. Pour forcer une socket Unix (légèrement plus rapide en local), laissez dbhost à localhost et renseignez dbsocket (voir dboptions). Sur une architecture à base de données séparée, mettez l’IP privée ou le hostname du serveur PostgreSQL.

$CFG->dbname, $CFG->dbuser, $CFG->dbpass — les identifiants créés en section 4. C’est LA raison pour laquelle ce fichier doit être en chmod 640 et hors webroot : quiconque lit ce fichier possède votre base.

$CFG->prefix — le préfixe de tables (mdl_user, mdl_course…). Maximum 10 caractères (contrainte durcie dans les versions récentes ; avec Oracle, désormais retiré, c’était 2). Ne le changez jamais après installation : toutes les tables devraient être renommées.

$CFG->dboptions — un tableau d’options de connexion :

$CFG->dboptions = array( // Connexions persistantes : PHP-FPM réutilise la connexion entre requêtes. // Gain de latence, MAIS chaque worker FPM garde une connexion PG ouverte // en permanence — dimensionnez max_connections en face. Avec PostgreSQL, // la doc Moodle recommande plutôt un pooler (PgBouncer) que dbpersist=1. 'dbpersist' => 0, // Port non standard (ex. 5433 ou un PgBouncer sur 6432). Vide = défaut. 'dbport' => '', // Socket Unix. Pour PostgreSQL local : '/var/run/postgresql' // (plus rapide que TCP, et évite la stack réseau). 'dbsocket' => '', // Spécifique MySQL/MariaDB : collation (ex. 'utf8mb4_unicode_ci'). // Sans objet en PostgreSQL (la collation est fixée à la création de la base). // 'dbcollation' => 'utf8mb4_unicode_ci', );

9.3 wwwroot : les règles absolues

$CFG->wwwroot = 'https://moodle.example.com';

C’est l’URL canonique du site, utilisée pour générer tous les liens (pages, mails, redirections OAuth…). Les règles, dans l’ordre où elles font trébucher les débutants :

  1. Pas de slash final. https://moodle.example.com/ est invalide.
  2. Jamais de /public à la fin. Le webroot du serveur web pointe sur le répertoire public/, mais l’URL publique, elle, ne contient pas ce segment. Si vous écrivez $CFG->wwwroot = 'https://moodle.example.com/public', Moodle refuse de démarrer avec l’erreur explicite : « Incorrect $CFG->wwwroot in config.php. It should not end in /public. » Si votre site affiche /public dans ses URL, c’est votre vhost qui est faux (root sur /var/www/moodle au lieu de /var/www/moodle/public), pas wwwroot qu’il faut tordre.
  3. Le protocole compte. Passer le site en HTTPS = changer http:// en https:// ici (section 10). Un wwwroot en http derrière un site servi en https provoque des boucles de redirection et du contenu mixte.
  4. Un seul wwwroot. Moodle ne sait pas répondre sur deux domaines à la fois ; il redirige tout vers wwwroot.

$CFG->dataroot — chemin absolu de moodledata. Ne doit jamais être sous le webroot (section 6). Si vous déplacez moodledata, changez cette ligne et rien d’autre.

$CFG->admin — le nom du répertoire d’administration (/admin). Ne changez cette valeur que dans le cas exotique d’un hébergeur qui réserve /admin ; sinon laissez 'admin'.

$CFG->directorypermissions — le mode (octal) des répertoires que Moodle crée dans moodledata. Le défaut généré 0777 est historique et laxiste ; sur un serveur dédié où seul www-data accède à moodledata, resserrez :

$CFG->directorypermissions = 02770; // rwxrwx--- + setgid // ou plus strict encore : $CFG->directorypermissions = 0770;

9.4 $CFG->root et $CFG->dirroot : ne pas confondre (Moodle 5.1+)

Depuis la restructuration 5.1, deux variables décrivent l’arborescence :

  • $CFG->root : la racine du projet (/var/www/moodle) — là où vivent config.php, vendor/, node_modules/. C’est une variable en lecture seule, calculée par Moodle : ne la définissez jamais dans config.php.
  • $CFG->dirroot : le répertoire du code web, c’est-à-dire public/ (/var/www/moodle/public). Historiquement, dirroot désignait la racine unique ; pour la compatibilité, il pointe désormais vers public/. Le code de plugin qui fait require($CFG->dirroot . '/lib/filelib.php') continue donc de fonctionner sans modification.

Retenez : root = projet, dirroot = webroot ; aucun des deux ne se définit à la main.

9.5 Derrière un reverse proxy : sslproxy et reverseproxy

Deux directives cruciales dès que Moodle n’est pas directement exposé — c’est-à-dire derrière un load balancer, Cloudflare, Traefik, ou un Nginx frontal distinct :

// La terminaison TLS est faite EN AMONT (le proxy parle à Moodle en HTTP, // mais les utilisateurs sont en HTTPS). Sans cette ligne, Moodle croit être // en HTTP, génère des liens http://, et refuse cookiesecure. $CFG->sslproxy = true; // Le proxy réécrit l'hôte ou le port (ex. proxy sur :443 -> Moodle sur :8080, // ou changement de chemin). Indique à Moodle de ne pas comparer l'URL entrante // à wwwroot pour détecter les accès "hors wwwroot". // ATTENTION : avec reverseproxy=true, l'accès DIRECT au serveur Moodle // (en contournant le proxy) ne fonctionne plus correctement. $CFG->reverseproxy = true;

Quand les utiliser :

  • Terminaison TLS sur le serveur Moodle lui-même (notre setup de ce chapitre, certbot sur le Nginx local) : aucune des deux.
  • Cloudflare en mode « Full » ou LB qui parle HTTPS au backend : aucune des deux non plus (le backend voit du HTTPS).
  • Cloudflare/LB/Traefik qui parle HTTP au backend : sslproxy = true, et wwwroot en https://.
  • Proxy qui change hôte/port (port mapping Docker, par exemple) : ajouter reverseproxy = true.

⚠️ Piège : le symptôme classique du sslproxy manquant est une boucle de redirection infinie après passage derrière Cloudflare, ou des utilisateurs déconnectés en boucle parce que le cookie Secure n’est jamais posé. Et l’inverse — mettre reverseproxy = true sans proxy — casse le site (Moodle n’accepte plus l’accès direct). Ces deux booléens se règlent en connaissance de cause, jamais « au cas où ».

9.6 Sessions : fichiers vs Redis

Par défaut, les sessions PHP de Moodle sont stockées dans la base de données (table mdl_sessions), avec un fallback fichiers dans moodledata/sessions/. Ça fonctionne, mais sur un site chargé, chaque hit écrit dans la base, et les verrous de session deviennent un goulot d’étranglement. La solution standard en production : Redis.

sudo apt install -y redis-server sudo systemctl enable --now redis-server

Puis dans config.php, avant le require_once final :

// ---- Sessions dans Redis ---- $CFG->session_handler_class = '\core\session\redis'; $CFG->session_redis_host = '127.0.0.1'; $CFG->session_redis_port = 6379; // Base Redis dédiée aux sessions (0-15) — séparez sessions et cache MUC $CFG->session_redis_database = 0; // Préfixe des clés (utile si le Redis est partagé) $CFG->session_redis_prefix = 'mdl_sess_'; // Timeout d'acquisition du verrou de session (secondes) $CFG->session_redis_acquire_lock_timeout = 120; // Durée de vie du verrou — doit dépasser la durée max d'une requête $CFG->session_redis_lock_expire = 7200; // Compression des données de session (économise la RAM Redis) $CFG->session_redis_compressor = 'gzip';

Avantages par rapport aux sessions base/fichiers : lecture/écriture en mémoire (latence quasi nulle), verrous efficaces, expiration native (TTL), et surtout partage naturel entre plusieurs frontaux si vous scalez horizontalement un jour — là où des sessions fichiers exigeraient un NFS.

💡 Pour un dev React : c’est exactement la raison pour laquelle on ne stocke pas les sessions Express en mémoire locale derrière un load balancer, et qu’on branche connect-redis. Même problème, même solution — Moodle a juste son propre handler intégré.

9.7 Redis pour MUC (le cache applicatif)

Ne confondez pas sessions et cache. Le MUC (Moodle Universal Cache) est le système de cache applicatif de Moodle (chaînes de langue, configs, coursemodinfo…). Par défaut il utilise des fichiers dans moodledata ; en production, on lui donne aussi Redis — mais cela se configure dans l’interface d’administration, pas dans config.php : Administration du site → Plugins → Caching → Configuration → ajouter une instance de magasin « Redis » (serveur 127.0.0.1:6379, base 1 pour la séparer des sessions) → la mapper sur le cache « Application ».

config.php ne sert ici qu’à des configurations MUC avancées et figées (via $CFG->alternative_cache_factory_class ou la définition anticipée des stores pour les clusters) — hors périmètre de ce chapitre. Retenez : sessions Redis = config.php ; cache MUC Redis = interface d’admin.

9.8 Options de debug — pour la recette uniquement

Sur un serveur de recette/staging, ces lignes transforment les écrans blancs en vrais messages d’erreur :

// ---- RECETTE UNIQUEMENT — JAMAIS EN PRODUCTION ---- @error_reporting(E_ALL); @ini_set('display_errors', '1'); $CFG->debug = (E_ALL); // équivaut au niveau DEVELOPER $CFG->debugdisplay = 1; // affiche les erreurs dans la page // Empêcher toute fuite de mail depuis la recette : $CFG->noemailever = true; // aucun mail ne part, point. // OU rediriger tous les mails vers une seule boîte : $CFG->divertallemailsto = 'alexlemia13@gmail.com';

Pourquoi jamais en prod : debugdisplay expose chemins de fichiers, requêtes SQL et parfois des données dans les pages d’erreur — un cadeau pour un attaquant, et une honte devant les utilisateurs. En production, les erreurs vont dans les logs PHP-FPM/Nginx, pas à l’écran.

noemailever et divertallemailsto sont vos meilleurs amis en recette : rien de pire qu’une base de prod restaurée en staging qui envoie de vrais rappels de devoirs à de vrais étudiants.

9.9 Chemins d’exécutables et verrouillage

// Chemins vers les binaires que Moodle exécute (rapports, cron, antivirus...) $CFG->pathtophp = '/usr/bin/php'; // utilisé pour lancer des sous-tâches CLI $CFG->pathtodu = '/usr/bin/du'; // calculs de taille de dossiers (rapides) $CFG->aspellpath = '/usr/bin/aspell'; // correcteur orthographique $CFG->pathtogs = '/usr/bin/gs'; // ghostscript (annotation PDF des devoirs) // Verrouille la modification de ces chemins DEPUIS l'interface web. // Sans cela, un compte admin compromis peut pointer "pathto..." vers un // binaire arbitraire => exécution de code sur le serveur. À activer en prod. $CFG->preventexecpath = true;

preventexecpath = true est une défense en profondeur importante : les chemins d’exécutables ne peuvent plus être définis que dans config.php (donc par quelqu’un qui a déjà un accès shell), plus jamais depuis le web.

9.10 Stockage de fichiers alternatif (mention)

Pour les architectures cloud, Moodle permet de remplacer le stockage local de filedir/ par une implémentation alternative :

// Exemple avec le plugin tool_objectfs (S3, Azure Blob, GCS) : $CFG->alternative_file_system_class = '\tool_objectfs\s3_file_system';

Retenez simplement que ça existe : le jour où votre moodledata pèse 2 To et que vous voulez du S3 avec cycle de vie, la porte s’appelle alternative_file_system_class + plugin tool_objectfs. Le stockage par hash SHA1 vu en section 6 rend cette abstraction naturelle.

9.11 Le config.php de production complet, annoté

Pour synthétiser, voici un config.php de production réaliste qui assemble tout ce chapitre :

<?php // config.php — Moodle 5.2 production — moodle.example.com unset($CFG); global $CFG; $CFG = new stdClass(); // --- Base de données ------------------------------------------------- $CFG->dbtype = 'pgsql'; $CFG->dblibrary = 'native'; $CFG->dbhost = 'localhost'; $CFG->dbname = 'moodle'; $CFG->dbuser = 'moodleuser'; $CFG->dbpass = getenv('MOODLE_DB_PASSWORD') ?: 'RemplacezParUnVraiMotDePasseFort'; $CFG->prefix = 'mdl_'; $CFG->dboptions = array( 'dbpersist' => 0, 'dbport' => '', 'dbsocket' => '/var/run/postgresql', // socket Unix locale ); // --- Chemins et URL --------------------------------------------------- $CFG->wwwroot = 'https://moodle.example.com'; // ni slash final, ni /public $CFG->dataroot = '/var/moodledata'; $CFG->admin = 'admin'; $CFG->directorypermissions = 02770; // --- Sessions dans Redis ---------------------------------------------- $CFG->session_handler_class = '\core\session\redis'; $CFG->session_redis_host = '127.0.0.1'; $CFG->session_redis_port = 6379; $CFG->session_redis_database = 0; $CFG->session_redis_prefix = 'mdl_sess_'; $CFG->session_redis_acquire_lock_timeout = 120; $CFG->session_redis_lock_expire = 7200; // --- Durcissement ----------------------------------------------------- $CFG->preventexecpath = true; $CFG->pathtophp = '/usr/bin/php'; $CFG->pathtodu = '/usr/bin/du'; $CFG->aspellpath = '/usr/bin/aspell'; $CFG->pathtogs = '/usr/bin/gs'; // --- Proxy (décommenter UNIQUEMENT si terminaison TLS en amont) -------- // $CFG->sslproxy = true; // $CFG->reverseproxy = true; require_once(__DIR__ . '/public/lib/setup.php'); // Pas de tag de fermeture PHP : intentionnel.

📚 Aller plus loin : le fichier config-dist.php à la racine du dépôt Moodle est la référence exhaustive : chaque option de config.php y est documentée en commentaire, y compris des dizaines que nous n’avons pas couvertes (clusters, lock factories, forced settings via $CFG->forced_plugin_settings…). Lisez-le au moins une fois en diagonale.


10. Finalisation : HTTPS avec Let’s Encrypt

Un Moodle en HTTP n’est pas envisageable : identifiants, notes, données personnelles. Let’s Encrypt + certbot rendent le TLS gratuit et automatique.

10.1 Installation de certbot et obtention du certificat

sudo apt install -y certbot python3-certbot-nginx # Le plugin nginx modifie automatiquement le vhost : il ajoute le bloc # listen 443 ssl, les chemins des certificats, et (si vous le demandez) # la redirection 80 -> 443. sudo certbot --nginx -d moodle.example.com \ --email alexlemia13@gmail.com \ --agree-tos --no-eff-email --redirect

Le flag --redirect fait ajouter par certbot la redirection permanente HTTP → HTTPS dans le bloc listen 80. Après exécution, inspectez le vhost modifié :

sudo nginx -t && sudo systemctl reload nginx grep -A3 'listen 443' /etc/nginx/sites-available/moodle

Le renouvellement est automatique (timer systemd certbot.timer, deux vérifications par jour). Testez le à blanc :

sudo certbot renew --dry-run systemctl list-timers | grep certbot

10.2 Mettre à jour wwwroot

Si vous aviez installé avec un wwwroot en http://, c’est le moment de passer config.php en https:// :

$CFG->wwwroot = 'https://moodle.example.com';

Puis purgez les caches (des URL absolues sont cachées) :

cd /var/www/moodle sudo -u www-data php public/admin/cli/purge_caches.php

Activez ensuite le cookie sécurisé dans l’admin : Administration du site → Sécurité → Sécurité HTTP → Cookies sécurisés uniquement (cookiesecure) — ou en CLI :

sudo -u www-data php public/admin/cli/cfg.php --name=cookiesecure --set=1

10.3 HSTS : oui, mais avec prudence

HSTS (Strict-Transport-Security) ordonne aux navigateurs de ne plus jamais tenter le HTTP pour votre domaine pendant max-age secondes. Excellent pour la sécurité, mais irréversible à l’échelle du max-age : si votre certificat casse, les visiteurs ne pourront même plus voir une page d’erreur en HTTP. Démarrez court, allongez quand tout est stable :

# Dans le bloc server { listen 443 ... } du vhost : # Phase 1 — test (1 jour) : add_header Strict-Transport-Security "max-age=86400" always; # Phase 2 — après quelques semaines sans incident (6 mois + sous-domaines) : # add_header Strict-Transport-Security "max-age=15768000; includeSubDomains" always;

⚠️ Piège : n’ajoutez includeSubDomains que si tous vos sous-domaines (présents et futurs) sont en HTTPS — cet attribut s’applique à tout le domaine et a déjà cassé bien des intranets internes en http. Et ne soumettez le domaine à la « preload list » des navigateurs qu’en toute connaissance de cause : c’est quasi définitif.


11. Le cron : le battement de cœur de Moodle

11.1 Pourquoi c’est vital

Une énorme partie de Moodle fonctionne en différé, via des tâches exécutées par le script de cron : envoi des mails et notifications (forums, devoirs rendus…), exécution des tâches planifiées (backups automatiques de cours, nettoyage des sessions, purge de la corbeille, statistiques), traitement des tâches ad hoc (suppressions de cours en masse, conversions de fichiers, mises à jour d’inscriptions), synchronisations (LDAP, flux d’inscription), mise à jour des complétions et des restrictions d’accès temporelles.

Sans cron, Moodle semble fonctionner… puis les mails ne partent pas, les backups n’existent pas, les inscriptions par cohorte ne se propagent pas, et l’interface d’administration affiche l’avertissement « Le cron ne s’est pas exécuté depuis au moins 24 heures ». La consigne officielle est sans ambiguïté : le cron doit tourner chaque minute. Le script interne est intelligent : il ne lance que ce qui est dû, se parallélise, et pose des verrous — l’appeler toutes les minutes ne « surcharge » pas le serveur.

💡 Pour un dev React : le cron Moodle = votre worker BullMQ + votre scheduler cron réunis. Les « tâches planifiées » sont les jobs récurrents (repeat de BullMQ), les « tâches ad hoc » sont la queue de jobs ponctuels que le code applicatif empile (queue.add(...)) et que le worker dépile. La différence : au lieu d’un process long-vivant, PHP relance un process chaque minute qui dépile ce qui est dû. Même modèle mental, exécution différente.

11.2 Méthode 1 : crontab classique

Le cron doit tourner sous l’utilisateur www-data (même raison que pour les CLI : les fichiers créés dans moodledata doivent appartenir au serveur web).

sudo crontab -u www-data -e

Ajoutez cette ligne (chemin complet depuis Moodle 5.1, avec public/) :

* * * * * /usr/bin/php /var/www/moodle/public/admin/cli/cron.php >/dev/null
  • * * * * * : chaque minute.
  • >/dev/null : jette stdout (sinon cron enverrait un mail local à chaque exécution) ; stderr reste visible, donc les vraies erreurs remontent dans les logs mail/syslog du système.

Vérifiez la prise en compte :

sudo crontab -u www-data -l # Et une exécution manuelle pour valider tout de suite : sudo -u www-data /usr/bin/php /var/www/moodle/public/admin/cli/cron.php

11.3 Méthode 2 : timer systemd (recommandée)

Le timer systemd est plus moderne : logs centralisés dans journald, dépendances (ne démarre qu’après PostgreSQL), protection contre les chevauchements au niveau du service, statut consultable. Créez deux unités.

Le service — /etc/systemd/system/moodle-cron.service :

[Unit] Description=Moodle cron After=postgresql.service redis-server.service Wants=postgresql.service [Service] Type=oneshot User=www-data Group=www-data ExecStart=/usr/bin/php /var/www/moodle/public/admin/cli/cron.php # Garde-fous Nice=10 TimeoutStartSec=1800 # Durcissement (optionnel mais gratuit) PrivateTmp=true ProtectSystem=full ReadWritePaths=/var/moodledata NoNewPrivileges=true

Le timer — /etc/systemd/system/moodle-cron.timer :

[Unit] Description=Lance le cron Moodle chaque minute [Timer] # À la seconde 00 de chaque minute OnCalendar=*:*:00 # Rattraper une exécution manquée après un reboot Persistent=true [Install] WantedBy=timers.target

Activation et vérification :

sudo systemctl daemon-reload sudo systemctl enable --now moodle-cron.timer # Le timer est-il armé ? Quand est la prochaine exécution ? systemctl list-timers moodle-cron.timer # Suivre les logs du cron en temps réel — LE gros avantage vs crontab : sudo journalctl -u moodle-cron.service -f # Les 50 dernières lignes : sudo journalctl -u moodle-cron.service -n 50 --no-pager

Si vous adoptez le timer, supprimez la ligne crontab de la méthode 1 (une seule des deux méthodes doit être active).

11.4 Vérification côté Moodle

Dans Administration du site → Notifications, l’avertissement de cron doit avoir disparu. Vous pouvez aussi consulter Administration du site → Serveur → Tâches → Journal des tâches pour voir chaque tâche, sa durée et son résultat. En CLI, pour lister l’état des tâches planifiées :

cd /var/www/moodle sudo -u www-data php public/admin/cli/scheduled_task.php --list

⚠️ Piège : si le cron tourne mais que Moodle continue d’afficher « cron ne s’est pas exécuté », c’est presque toujours l’un de ces trois cas : (1) le cron tourne sous le mauvais utilisateur et échoue sur des permissions moodledata ; (2) le chemin du script est l’ancien chemin pré-5.1 (admin/cli/cron.php au lieu de public/admin/cli/cron.php) et cron échoue silencieusement à cause du >/dev/null ; (3) le CLI utilise un autre php.ini où une extension manque. Diagnostic : lancez la commande à la main avec sudo -u www-data et lisez la sortie.


12. Maintenance et upgrade en CLI

Un Moodle de production se met à jour souvent : des mises à jour mineures de sécurité sortent environ tous les deux mois. Grâce au clone git de la section 5, la procédure est courte. La voici, dans l’ordre, sans étape sautable.

12.1 La procédure de mise à jour mineure (ex. 5.2 → 5.2.x)

Étape 1 — Activer le mode maintenance (les utilisateurs voient une page d’attente, les sessions sont préservées) :

cd /var/www/moodle sudo -u www-data php public/admin/cli/maintenance.php --enable # Variante élégante : maintenance programmée avec compte à rebours de 10 min # sudo -u www-data php public/admin/cli/maintenance.php --enablelater=10

Étape 2 — Sauvegarder les trois composants (base + moodledata + config.php ; le code, lui, est dans git) :

STAMP=$(date +%Y%m%d-%H%M) sudo mkdir -p /var/backups/moodle # 1. La base (format custom : compressé, restaurable sélectivement avec pg_restore) sudo -u postgres pg_dump -Fc moodle > /tmp/moodle-db-$STAMP.dump sudo mv /tmp/moodle-db-$STAMP.dump /var/backups/moodle/ # 2. moodledata (en excluant les caches, reconstructibles) sudo tar --exclude='./cache' --exclude='./localcache' --exclude='./temp' \ --exclude='./sessions' --exclude='./trashdir' \ -czf /var/backups/moodle/moodledata-$STAMP.tar.gz -C /var/moodledata . # 3. config.php sudo cp /var/www/moodle/config.php /var/backups/moodle/config-$STAMP.php

Étape 3 — Mettre à jour le code :

cd /var/www/moodle sudo git fetch origin sudo git merge origin/MOODLE_502_STABLE # (équivalent d'un git pull ; sudo car le code appartient à root)

Pour une montée de version majeure (ex. 5.2 → 5.3 à sa sortie), on change de branche au lieu de merger :

sudo git fetch origin sudo git checkout -b MOODLE_503_STABLE origin/MOODLE_503_STABLE # ou, en déploiement figé par tags : # sudo git checkout v5.3.0

Rappel des règles de chemin d’upgrade : Moodle 5.2 accepte les montées directes depuis 4.4 minimum ; plus ancien, il faut passer par un palier intermédiaire.

Étape 4 — Lancer l’upgrade de la base :

sudo -u www-data php public/admin/cli/upgrade.php --non-interactive

Ce script exécute toutes les migrations de schéma (les upgrade.php du core et de chaque plugin), exactement comme le ferait la page de notification web, mais scriptable et sans timeout HTTP.

Étape 5 — Purger les caches et sortir de maintenance :

sudo -u www-data php public/admin/cli/purge_caches.php sudo -u www-data php public/admin/cli/maintenance.php --disable

Étape 6 — Vérifier : Administration du site → Notifications (tout doit être vert), et un parcours rapide du site en tant qu’utilisateur.

En cas de problème grave, le rollback est : restaurer la base (pg_restore -d moodle ... après drop/recreate), revenir au commit git précédent (git log pour retrouver le hash, git checkout <hash>), restaurer moodledata si besoin, purger les caches. D’où l’importance de l’étape 2 — jamais d’upgrade sans backup frais.

💡 Pour un dev React : upgrade.php joue le rôle de prisma migrate deploy : il compare la version du code à la version du schéma en base (stockée dans mdl_config) et rejoue les migrations manquantes, pour le core et chaque plugin. Et comme avec Prisma : les migrations descendantes n’existent pas — le rollback, c’est le backup.

12.2 Cas particulier : la migration 5.0 → 5.1 (structure public/)

Si vous administrez un jour une montée de version qui traverse la frontière 5.0 → 5.1, la mise à jour du code déplace toute l’arborescence web dans public/. Deux actions supplémentaires vous incombent :

  1. Modifier le vhost : root /var/www/moodle; devient root /var/www/moodle/public; (idem DocumentRoot sous Apache), puis nginx -t && systemctl reload nginx. À faire pendant la fenêtre de maintenance, de façon coordonnée avec le checkout du code.
  2. Re-déplacer les plugins tiers : vos plugins additionnels installés dans l’ancienne arborescence (ex. /var/www/moodle/mod/attendance) doivent être déplacés dans la nouvelle (/var/www/moodle/public/mod/attendance) avant de lancer upgrade.php — sinon Moodle les considérera comme « manquants » et proposera de les désinstaller (avec perte de données à la clé si vous confirmez).
  3. Mettez également à jour les chemins externes : la ligne de cron (admin/cli/cron.phppublic/admin/cli/cron.php), les scripts de backup, les sondes de monitoring.

Rien à changer dans config.php : wwwroot reste identique (sans /public), et dirroot/root sont recalculés automatiquement.

12.3 Les CLI d’administration à connaître

Petit inventaire des scripts de public/admin/cli/ que vous utiliserez régulièrement (tous à lancer avec sudo -u www-data php ... depuis /var/www/moodle) :

ScriptUsage
cron.phpLe cron (section 11)
upgrade.php --non-interactiveMigrations après mise à jour du code
maintenance.php --enable/--disableMode maintenance
purge_caches.phpPurger tous les caches MUC
cfg.php --name=xxx [--set=yyy]Lire/écrire n’importe quel réglage d’admin
reset_password.phpRéinitialiser un mot de passe (dont l’admin verrouillé dehors !)
checks.phpExécuter les vérifications de santé du site
uninstall_plugins.phpDésinstaller proprement des plugins en CLI
mysql_collation.php(MySQL uniquement) conversion de collation

📚 Aller plus loin : la page « Administration via command line » de docs.moodle.org liste l’intégralité des scripts CLI avec leurs options ; chaque script accepte aussi --help.


13. Checklist sécurité de production

Avant d’ouvrir le site aux utilisateurs, passez cette liste. Dix minutes qui évitent des mois de regrets.

Système et réseau

  • ufw actif : seuls 22, 80, 443 ouverts (sudo ufw status)
  • SSH : PermitRootLogin no, PasswordAuthentication no
  • Mises à jour de sécurité automatiques : sudo apt install unattended-upgrades && sudo dpkg-reconfigure -plow unattended-upgrades
  • PostgreSQL et Redis n’écoutent que sur localhost (ss -tlnp | grep -E '5432|6379')

Fichiers et permissions

  • Code en lecture seule pour www-data : /var/www/moodle appartient à root, fichiers 644 / répertoires 755
  • config.php : root:www-data, mode 640
  • moodledata hors webroot (/var/moodledata), appartient à www-data, mode 770 ; test rapide : curl -I https://moodle.example.com/filedir/ doit renvoyer 404
  • Le webroot Nginx pointe bien sur /var/www/moodle/public (test : curl -I https://moodle.example.com/config.php → 404, puisque config.php est hors webroot)

Moodle

  • HTTPS actif, redirection 80→443, wwwroot en https://
  • cookiesecure activé (Sécurité HTTP) — automatique en HTTPS sur les versions récentes, mais vérifiez
  • $CFG->preventexecpath = true dans config.php
  • Aucun réglage de debug en prod : ni $CFG->debug, ni debugdisplay (vérifiez avec sudo -u www-data php public/admin/cli/cfg.php --name=debug)
  • Cron opérationnel chaque minute (aucun avertissement dans Notifications)
  • Page Administration du site → Serveur → Environnement (admin/environment.php) : tout est vert, aucune extension manquante
  • Page Administration du site → Rapports → Contrôles de sécurité : passer en revue chaque ligne
  • Auto-inscription désactivée si non nécessaire ; politique de mots de passe activée
  • Abonnez-vous aux annonces de sécurité Moodle (liste de diffusion) et planifiez la mise à jour mineure à chaque publication

Sauvegardes

  • Backup automatique quotidien des trois composants : dump PostgreSQL, moodledata, config.php
  • Backups stockés hors du serveur (autre machine, stockage objet)
  • Restauration testée au moins une fois (un backup non testé n’est pas un backup)

Récapitulatif

Ce chapitre vous a fait construire un Moodle 5.2 de production complet, à la main, et surtout comprendre chaque pièce :

  • La stack : Ubuntu 24.04 + Nginx + PHP-FPM 8.3 + PostgreSQL 16, chaque service configuré explicitement (deux php.ini — fpm et cli —, max_input_vars=5000, OPcache dimensionné, pool FPM calculé selon la RAM, base UTF-8 avec authentification scram-sha-256).
  • La séparation en trois : le code (/var/www/moodle, propriété root, lecture seule pour le web, suivi par git sur MOODLE_502_STABLE), les données fichiers (/var/moodledata, propriété www-data, hors webroot, stockage par hash SHA1 dans filedir/), et la base PostgreSQL. Trois choses à sauvegarder : la base, moodledata, config.php.
  • La structure public/ de Moodle 5.1+ : le vhost pointe sur /var/www/moodle/public, config.php reste à la racine du projet, $CFG->wwwroot ne se termine jamais par /public, $CFG->root (lecture seule) désigne le projet tandis que $CFG->dirroot désigne public/, et tous les CLI se lancent en php public/admin/cli/xxx.php.
  • Le vhost Nginx : fastcgi_split_path_info + variable $path_info figée avant try_files pour les slash arguments, client_max_body_size aligné sur PHP, blocages des fichiers cachés.
  • config.php : le seul fichier de configuration côté code — connexion base, wwwroot/dataroot, sessions Redis, sslproxy/reverseproxy derrière un proxy, options de debug réservées à la recette, preventexecpath et chemins d’exécutables verrouillés.
  • Le cron chaque minute sous www-data (crontab ou, mieux, timer systemd avec logs journalctl), et la procédure d’upgrade CLI : maintenance → backup → git merge → upgrade.php --non-interactive → purge des caches → sortie de maintenance.

Vous savez désormais lire n’importe quelle installation Moodle de production : où est le code, où sont les données, qui a le droit d’écrire quoi, et par où passent les requêtes.

Dans le prochain chapitre

Maintenant que vous maîtrisez les deux extrêmes — l’environnement Docker jetable du développeur et le serveur de production assemblé à la main — le prochain chapitre s’attaque à l’outillage du développeur Moodle : activation du mode développeur et des options de debug, installation des dépendances Composer et Node.js, découverte de config-dist.php, des outils XMLDB et des premiers scripts utiles pour inspecter une instance. C’est là que votre quotidien de développeur de plugins commence réellement.