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
É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
reverseproxyplutô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"
Vos 4 Points Clés
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.
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.
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.
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