devops x aidd x docker

Déployer Moodle en 30 minutes avec Claude Code

Comment l'IA a piloté le déploiement de A à Z : scan de l'infra, debug en direct, et résolution de 3 obstacles majeurs.

1 février 2026 · 1 min

Ce matin, j'avais besoin d'un LMS Moodle en production. 30 minutes et 25 commandes SSH plus tard, courses.ai-driven-dev.com était live. Pas grâce à un script magique : grâce à Claude Code qui a piloté l'ensemble du déploiement, découvrant l'infrastructure existante, analysant les options Docker, et résolvant 3 problèmes critiques en temps réel.


Étape 1 : Comprendre le besoin et l'approche

le besoin initial

Déployer un LMS Moodle en production pour héberger des formations AI Driven Ops. Les contraintes étaient claires :

  • Serveur existant avec infrastructure Docker + Traefik
  • SSL automatique via Let's Encrypt
  • Solution production-ready, pas un POC
  • Langue française par défaut

l'approche AI Driven Ops

Au lieu de chercher manuellement la documentation Moodle, les images Docker disponibles, et la configuration Traefik, j'ai laissé Claude Code piloter. L'IA avait accès à mon vault Obsidian contenant :

Vault Obsidian avec :
├── Inventaire Ansible du serveur AIDD
├── Variables de connexion SSH
├── Configuration Traefik existante
└── Historique des déploiements précédents (n8n, Shlink, etc.)

AI Driven Ops en vrai : l'IA ne se contente pas d'exécuter des commandes. Elle comprend le contexte, découvre l'infrastructure, et adapte sa stratégie en fonction des obstacles rencontrés.


Étape 2 : Découverte automatique de l'infrastructure

recherche des informations de connexion

Claude Code a d'abord analysé le vault pour trouver les credentials SSH. En quelques secondes, il a localisé le fichier d'inventaire Ansible :

# Fichier trouvé : inventory/hosts.yml
aidd-server:
  ansible_host: 203.0.113.42
  ansible_user: deploy
  ansible_port: 22

exploration du serveur distant

Sans intervention manuelle, l'IA s'est connectée au serveur pour découvrir les services existants :

# Connexion SSH et découverte
ssh deploy@203.0.113.42 "docker ps"

# Services existants découverts :
# - traefik (reverse proxy, ports 80/443)
# - n8n-app + n8n-postgres
# - shlink-app + shlink-db
# - portainer

compréhension du pattern Traefik

En analysant les docker-compose existants (notamment celui de n8n), Claude Code a identifié le pattern de déploiement utilisé sur ce serveur :

# Pattern identifié dans /srv/docker/n8n/docker-compose.yml
labels:
  - "traefik.enable=true"
  - "traefik.docker.network=proxy_internal"
  - "traefik.http.routers.n8n.rule=Host(`automation.ai-driven-dev.com`)"
  - "traefik.http.routers.n8n.tls.certresolver=letsencrypt"

Ce qui change tout

Tous les services utilisent le réseau proxy_internal et les labels Traefik pour l'auto-découverte. Claude Code a reproduit ce pattern pour Moodle sans que j'aie à l'expliquer.


Étape 3 : Analyser les images Docker Moodle

recherche des options disponibles

Claude Code a effectué une recherche web et interrogé l'API Docker Hub pour comparer les images Moodle disponibles :

image stars disponibilité production ready
bitnami/moodle 198 Retiré de Docker Hub Oui
moodlehq/moodle-php-apache - Disponible Non (dev only)
jauer/moodle 49 Disponible Non maintenu
public.ecr.aws/bitnami/moodle - Disponible Oui

pourquoi pas les images officielles MoodleHQ ?

Claude Code a trouvé la réponse dans la documentation officielle :

"moodlehq/moodle-php-apache : PHP + Apache docker images for Moodle development"

— GitHub MoodleHQ

Ces images ne contiennent pas Moodle : juste l'environnement PHP/Apache. Elles sont conçues pour le CI/CD et les tests, pas pour la production.


Obstacle 1 : Bitnami introuvable sur Docker Hub

le symptôme

Error response from daemon: manifest for bitnami/moodle:latest not found

l'analyse

Claude Code a vérifié via l'API Docker Hub :

# Test effectué
curl -s https://hub.docker.com/v2/repositories/bitnami/moodle/tags
# Résultat : {"count":0,"results":[]}

la cause découverte

Depuis août 2025, Bitnami (Broadcom) a migré vers un modèle payant "Bitnami Secure Images". Les images gratuites sont désormais sur Amazon ECR Public.

la solution

# Pull réussi depuis ECR Public
docker pull public.ecr.aws/bitnami/moodle:latest
# Status: Downloaded newer image for public.ecr.aws/bitnami/moodle:latest

point d'attention

Ce changement n'est pas documenté partout. Si vous suivez d'anciens tutoriels mentionnant bitnami/moodle, vous aurez cette erreur. Utilisez public.ecr.aws/bitnami/moodle à la place.


Obstacle 2 : Permissions UID 1001

le symptôme

mkdir: cannot create directory '/bitnami/mariadb/data': Permission denied

la cause

Les conteneurs Bitnami s'exécutent avec UID 1001 (non-root) pour des raisons de sécurité. Mais les volumes montés appartiennent à root par défaut.

la solution

Claude Code a proposé d'utiliser un conteneur Alpine temporaire pour fixer les permissions :

# Fixer les permissions pour UID 1001
docker run --rm -v $(pwd)/data:/data alpine sh -c \
  'chown -R 1001:1001 /data'

Cette commande modifie récursivement le propriétaire du dossier data/ pour correspondre à l'UID utilisé par les conteneurs Bitnami.


Obstacle 3 : Erreur 500 Reverse Proxy

le symptôme

<div class='alert-danger'>
Le proxy inverse est activé ; il n'est donc pas possible
d'accéder au serveur de manière directe.
</div>

diagnostic différentiel

Deux tests pour isoler le problème :

# Test interne (fonctionne)
docker exec traefik wget -qO- http://172.19.0.7:8080 | grep "<title>"
# <title>Accueil | AIDD Courses</title>

# Test externe (erreur 500)
curl -sI https://courses.ai-driven-dev.com
# HTTP/2 500

Conclusion : Moodle fonctionne en interne, mais rejette les requêtes passant par Traefik.

la cause

Moodle avec $CFG->reverseproxy = true vérifie strictement les headers HTTP. Traefik ne transmettait pas les headers attendus (notamment X-Forwarded-Host).

la solution

# Désactiver la vérification stricte
docker exec moodle-app sed -i \
  's/reverseproxy = true/reverseproxy = false/' \
  /bitnami/moodle/config.php
docker restart moodle-app

Le wwwroot reste configuré en HTTPS, donc les URLs générées sont correctes. C'est un workaround pragmatique : l'alternative "propre" serait de configurer Traefik pour transmettre les headers X-Forwarded-Host et X-Forwarded-Proto attendus par Moodle. Mais quand Traefik gère déjà le SSL correctement, désactiver cette vérification est une solution valide et rapide.


Étape 4 : Configuration finale

structure des fichiers

/srv/docker/moodle/
├── docker-compose.yml      # Orchestration des 3 services
├── .env                    # Secrets (mots de passe, domaine)
├── data/
│   ├── mariadb/           # Données persistantes MariaDB
│   ├── moodle/            # Code Moodle + config.php
│   └── moodledata/        # Fichiers utilisateurs (cours, uploads)
└── backups/               # Sauvegardes automatiques (7 jours)

docker-compose.yml complet

# Moodle LMS Stack for AIDD
# Domain: courses.ai-driven-dev.com
# Registry: Amazon ECR Public (Bitnami)

services:
  mariadb:
    container_name: moodle-mariadb
    image: public.ecr.aws/bitnami/mariadb:latest
    restart: unless-stopped
    environment:
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
      - MARIADB_USER=${MARIADB_USER}
      - MARIADB_PASSWORD=${MARIADB_PASSWORD}
      - MARIADB_DATABASE=${MARIADB_DATABASE}
      - MARIADB_CHARACTER_SET=utf8mb4
      - MARIADB_COLLATE=utf8mb4_unicode_ci
    volumes:
      - ./data/mariadb:/bitnami/mariadb
    networks:
      - moodle_backend
    healthcheck:
      test: ["/opt/bitnami/scripts/mariadb/healthcheck.sh"]
      interval: 15s
      timeout: 5s
      retries: 6

  moodle:
    container_name: moodle-app
    image: public.ecr.aws/bitnami/moodle:latest
    restart: unless-stopped
    environment:
      - MOODLE_DATABASE_TYPE=mariadb
      - MOODLE_DATABASE_HOST=moodle-mariadb
      - MOODLE_DATABASE_PORT_NUMBER=3306
      - MOODLE_DATABASE_USER=${MARIADB_USER}
      - MOODLE_DATABASE_PASSWORD=${MARIADB_PASSWORD}
      - MOODLE_DATABASE_NAME=${MARIADB_DATABASE}
      - MOODLE_USERNAME=${MOODLE_USERNAME}
      - MOODLE_PASSWORD=${MOODLE_PASSWORD}
      - MOODLE_EMAIL=${MOODLE_EMAIL}
      - MOODLE_SITE_NAME=${MOODLE_SITE_NAME}
      - MOODLE_LANG=${MOODLE_LANG}
      - MOODLE_HOST=${PRIMARY_DOMAIN}
      - MOODLE_REVERSEPROXY=false
      - MOODLE_SSLPROXY=false
      - PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT}
    volumes:
      - ./data/moodle:/bitnami/moodle
      - ./data/moodledata:/bitnami/moodledata
    networks:
      - proxy_internal
      - moodle_backend
    depends_on:
      mariadb:
        condition: service_healthy
    labels:
      - traefik.enable=true
      - traefik.docker.network=proxy_internal
      - traefik.http.routers.moodle.rule=Host(`courses.ai-driven-dev.com`)
      - traefik.http.routers.moodle.entrypoints=websecure
      - traefik.http.routers.moodle.tls=true
      - traefik.http.routers.moodle.tls.certresolver=letsencrypt
      - traefik.http.services.moodle.loadbalancer.server.port=8080

  backup:
    container_name: moodle-backup
    image: offen/docker-volume-backup:latest
    restart: unless-stopped
    environment:
      BACKUP_CRON_EXPRESSION: "0 4 * * *"
      BACKUP_FILENAME: moodle-backup-%Y-%m-%d
      BACKUP_RETENTION_DAYS: "7"
    volumes:
      - ./data/moodle:/backup/moodle:ro
      - ./data/moodledata:/backup/moodledata:ro
      - ./data/mariadb:/backup/mariadb:ro
      - ./backups:/archive

networks:
  proxy_internal:
    external: true
  moodle_backend:
    name: moodle_backend
    internal: true
architecture finale : deploiement moodle
courses.ai-driven-dev.com
internet
https:443
traefik
SSL Let's Encrypt
moodle-app
Apache + PHP :8080
mariadb
:3306
proxy_internal
moodle_backend (internal)
moodle-backup
cron 4h · retention 7 jours

Étape 5 : Lessons Learned

ce que l'IA fait bien

  • Découverte contextuelle : Trouver les fichiers Ansible avec les credentials SSH sans qu'on lui indique le chemin
  • Pattern recognition : Identifier le pattern Traefik en analysant les services existants
  • Recherche adaptative : Découvrir l'alternative ECR quand Docker Hub a échoué
  • Debugging itératif : Diagnostiquer l'erreur 500 via tests internes vs externes

ce qui nécessite encore l'humain

  • Décision finale : Choisir de désactiver reverseproxy plutôt que de configurer les headers Traefik
  • Validation sécurité : Vérifier que les passwords générés sont stockés correctement
  • Priorisation : Décider que "rapidité de déploiement" prime sur "config parfaite"
timeline de resolution : 3 obstacles en 30 minutes
1
image introuvable
bitnami/moodle retire de Docker Hub
ECR Public
10 min
2
permission denied
conteneurs Bitnami UID 1001 non-root
chown 1001
5 min
3
erreur 500 reverse proxy
Moodle vérifie strictement les headers HTTP
reverseproxy=false
15 min
total : 30 minutes de l'analyse au site live
metriques de la session ai driven ops
30
minutes
durée totale
25
commandes
exécutées par l'IA
3
problèmes
résolus en temps réel
1
url live
courses.ai-driven-dev.com
production-ready en une session

Vos 4 Points Clés

1

Bitnami a quitté Docker Hub

Utilisez public.ecr.aws/bitnami/moodle au lieu de bitnami/moodle. Ce changement affecte toutes les images Bitnami depuis août 2025.

2

UID 1001 pour les conteneurs Bitnami

Avant de démarrer, fixez les permissions : chown -R 1001:1001 ./data. C'est le pattern non-root standard de Bitnami.

3

L'erreur 500 reverse proxy se corrige simplement

Passez MOODLE_REVERSEPROXY=false dans docker-compose. Le SSL reste géré par Traefik, les URLs restent en HTTPS.

4

L'AI Driven Ops excelle en découverte et debug

Claude Code a trouvé seul les credentials, identifié les patterns existants, et diagnostiqué chaque problème. Le gain de temps est réel.


Adoptez l'AI Driven Ops

Vous voulez déployer plus vite avec moins de friction ? Découvrez comment intégrer Claude Code dans vos workflows DevOps.

Discutons de votre projet
Victor Langlois

Victor Langlois

Expert DevOps & IA · Architecte Cloud

10+ ans d'automatisation — du secret défense aux agents IA. Ex-ITSF (Xavier Niel), Gouvernement de Monaco. Je construis des systèmes qui libèrent les équipes tech des tâches répétitives.