Par un étudiant de 42 pour les étudiants de 42
Salut ! Si tu lis ce tuto, c'est que tu t'apprêtes à te lancer dans Inception, ou que tu es en plein dedans et que tu galères. Je suis passé par là, et je vais t'expliquer TOUT mon code ligne par ligne, comme si on codait ensemble.
- Introduction et Architecture
- Le Makefile
- Docker Compose
- Services Obligatoires
- Services Bonus
- Gestion des Secrets
- Conseils et Pièges
Inception, c'est un projet de la branche System Administration de 42. Le but : monter une infrastructure complète avec Docker, en créant tes propres images (pas de Docker Hub autorisé !), avec plusieurs services qui communiquent entre eux.
Voici comment j'ai organisé mon projet :
.
├── Makefile # Orchestration du projet
├── .env # Variables d'environnement (gitignore!)
├── secrets/ # Mots de passe (gitignore!)
│ ├── db_password.txt
│ ├── db_root_password.txt
│ ├── wp_admin_password.txt
│ ├── wp_user_password.txt
│ ├── ftp_password.txt
│ └── portainer_password.txt
└── srcs/
├── docker-compose.yml # Orchestration des conteneurs
└── requirements/
├── nginx/ # Reverse proxy HTTPS
├── wordpress/ # Site WordPress + PHP-FPM
├── mariadb/ # Base de données
└── bonus/
├── redis/ # Cache WordPress
├── ftp/ # Serveur FTP
├── adminer/ # Interface DB web
├── static/ # Site statique
└── portainer/ # UI Docker
- Alpine Linux 3.22 : légère, rapide, parfaite pour Docker
- Volumes bind : les données persistent sur
/home/namichel/data/ - Secrets Docker : sécurité des mots de passe
- Network bridge : tous les services communiquent via un réseau privé
Le Makefile, c'est ton chef d'orchestre. C'est lui qui va setup tout le projet, générer les certificats SSL, et lancer/arrêter tes conteneurs.
# Variables de projet
NAME = inception
SRCS_DIR = ./srcs
DOCKER_COMPOSE = docker compose -f $(SRCS_DIR)/docker-compose.ymlPourquoi ces variables ?
NAME: juste pour la cohérenceSRCS_DIR: pour pas répéter./srcspartoutDOCKER_COMPOSE: le chemin complet vers ton docker-compose.yml
# Chemins des données sur l'hôte
DATA_PATH = /home/namichel/data
WP_DATA = $(DATA_PATH)/wordpress
DB_DATA = $(DATA_PATH)/mariadb
PORTAINER_DATA = $(DATA_PATH)/portainer
CERTS_DATA = $(DATA_PATH)/certs
SECRETS_DIR = ./secretsAstuce importante : Change /home/namichel par ton login ! Tu peux utiliser $(HOME) si tu veux que ce soit dynamique.
Ces dossiers vont contenir toutes tes données persistantes :
WP_DATA: les fichiers WordPress (thèmes, plugins, uploads)DB_DATA: les fichiers de la base de données MariaDBPORTAINER_DATA: config de PortainerCERTS_DATA: certificats SSLSECRETS_DIR: fichiers secrets (mots de passe)
# Couleurs
GREEN = \033[0;32m
RED = \033[0;31m
YELLOW = \033[0;33m
RESET = \033[0mLes couleurs, c'est juste pour rendre le Makefile sexy dans le terminal. Vert = succès, Rouge = destruction, Jaune = info.
# Règle par défaut
all: setup certs
@echo "$(GREEN)Démarrage de l'infrastructure Alpine...$(RESET)"
@$(DOCKER_COMPOSE) up -d --buildLigne par ligne :
all: setup certs: avant de lancer les conteneurs, on faitsetupetcerts@echo: affiche un message (@ = pas d'affichage de la commande elle-même)up -d --build: lance les conteneurs en arrière-plan et rebuild les images
setup:
@echo "$(YELLOW)Préparation des dossiers de volumes...$(RESET)"
@mkdir -p $(WP_DATA)
@mkdir -p $(DB_DATA)
@mkdir -p $(PORTAINER_DATA)
@mkdir -p $(CERTS_DATA)
@mkdir -p $(SECRETS_DIR)Pourquoi mkdir -p ?
-p: crée les dossiers parents si nécessaire, et ne renvoie pas d'erreur si le dossier existe déjà
@if [ ! -f $(SECRETS_DIR)/db_password.txt ]; then echo "db_password" > $(SECRETS_DIR)/db_password.txt; fi
@if [ ! -f $(SECRETS_DIR)/db_root_password.txt ]; then echo "db_root_password" > $(SECRETS_DIR)/db_root_password.txt; fi
@if [ ! -f $(SECRETS_DIR)/portainer_password.txt ]; then echo "pass123456789" > $(SECRETS_DIR)/portainer_password.txt; fi
@if [ ! -f $(SECRETS_DIR)/wp_user_password.txt ]; then echo "123" > $(SECRETS_DIR)/wp_user_password.txt; fi
@if [ ! -f $(SECRETS_DIR)/wp_admin_password.txt ]; then echo "123" > $(SECRETS_DIR)/wp_admin_password.txt; fi
@if [ ! -f $(SECRETS_DIR)/ftp_password.txt ]; then echo "123" > $(SECRETS_DIR)/ftp_password.txt; fiCes lignes créent les fichiers secrets avec des valeurs par défaut.
La logique :
if [ ! -f fichier ]: si le fichier n'existe pasthen echo "valeur" > fichier: créer le fichier avec la valeurfi: fin du if
Pourquoi cette approche ?
- Tu lances
makeet ça marche direct (pratique pour tester) - Mais en correction, tu devras montrer que tu utilises des vrais mots de passe complexes
- Tu peux créer les fichiers manuellement avant le
makepour override ces valeurs
certs:
@if [ ! -f $(CERTS_DATA)/inception.crt ]; then \
echo "$(YELLOW)Génération des certificats SSL auto-signés...$(RESET)"; \
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout $(CERTS_DATA)/inception.key \
-out $(CERTS_DATA)/inception.crt \
-subj "/C=FR/ST=Angouleme/L=Angouleme/O=42/OU=namichel/CN=namichel.42.fr"; \
chmod 644 $(CERTS_DATA)/inception.crt; \
chmod 600 $(CERTS_DATA)/inception.key; \
else \
echo "$(GREEN)Certificats déjà présents.$(RESET)"; \
fiDécortiquons la commande openssl :
req: crée une demande de certificat-x509: génère un certificat auto-signé (pas besoin d'autorité de certification)-nodes: pas de mot de passe sur la clé privée-days 365: valide 1 an-newkey rsa:2048: crée une nouvelle clé RSA de 2048 bits-keyout: où sauver la clé privée-out: où sauver le certificat-subj: les infos du certificat (Country, State, Locality, Organization, Organizational Unit, Common Name)
Pourquoi CN=namichel.42.fr ?
C'est le nom de domaine que tu vas utiliser. Le CN (Common Name) doit correspondre à ton server_name dans nginx.
Les permissions :
644pour le.crt: tout le monde peut lire, seul root peut écrire600pour le.key: seul root peut lire/écrire (sécurité!)
down:
@echo "$(RED)Arrêt des conteneurs...$(RESET)"
@$(DOCKER_COMPOSE) downSimple : arrête tous les conteneurs.
clean: down
@echo "$(RED)Suppression des images inutilisées...$(RESET)"
@docker system prune -a -fdown: d'abord on arrête toutsystem prune -a -f: supprime toutes les images non utilisées (-a), sans confirmation (-f)
fclean: clean
@echo "$(RED)Suppression des volumes et certificats sur l'hôte...$(RESET)"
@sudo rm -rf $(DATA_PATH)
@sudo rm -rf $(SECRETS_DIR)
@if [ $$(docker volume ls -q | wc -l) -gt 0 ]; then \
docker volume rm $$(docker volume ls -q); \
fiLe grand nettoyage :
clean: déjà fait juste avantrm -rf $(DATA_PATH): supprime TOUTES les données persistantes (d'où lesudo)rm -rf $(SECRETS_DIR): supprime tous les secrets- La condition
if: si y'a des volumes Docker (docker volume ls -q | wc -l), on les supprime tous
fclean détruit TOUT. Tu perds ta base de données, tes fichiers WordPress, tout. À utiliser seulement pour repartir de zéro.
re: fclean allLe classique : nettoyage total puis reconstruction.
.PHONY: all setup certs down clean fclean re.PHONY : indique que ces règles ne correspondent pas à des fichiers réels. Sans ça, si tu avais un fichier nommé "clean", make serait perdu.
Le docker-compose.yml, c'est le cœur du projet. Il définit tous tes services, leurs relations, leurs volumes, leurs secrets.
x-config: &config
restart: on-failure:3
stdin_open: true
tty: true
networks:
- inceptionC'est quoi ce truc bizarre ?
x-config est une anchor YAML. C'est comme un template qu'on va réutiliser.
restart: on-failure:3: si le conteneur crash, Docker le redémarre max 3 foisstdin_open: true: garde stdin ouvert (commedocker run -i)tty: true: alloue un pseudo-TTY (commedocker run -t)networks: - inception: tous les services seront sur le réseauinception
Pourquoi -i -t ?
Pour pouvoir faire docker exec -it et avoir un shell interactif. Pratique pour le debug.
services:
mariadb:
<<: *config<<: *config = "copie-colle la config de l'anchor ici". Tous les services auront cette config de base.
build: requirements/mariadb
container_name: mariadbbuild: où trouver le Dockerfilecontainer_name: nom du conteneur (sinon Docker génère un nom random)
volumes:
- mariadb_data:/var/lib/mysqlVolume bind : /home/namichel/data/mariadb (sur l'hôte) → /var/lib/mysql (dans le conteneur)
Pourquoi /var/lib/mysql ?
C'est là que MariaDB stocke ses bases de données par défaut.
env_file:
- ../.envCharge les variables d'environnement depuis .env (à la racine du projet).
secrets:
- db_password
- db_root_passwordMonte les secrets dans /run/secrets/ du conteneur. On verra ça plus tard.
wordpress:
<<: *config
build: requirements/wordpress
container_name: wordpress
volumes:
- wordpress_data:/var/www/htmlMême logique que MariaDB, mais les fichiers WordPress vont dans /var/www/html.
env_file:
- ../.env
secrets:
- db_password
- wp_user_password
- wp_admin_password
depends_on:
- mariadb
- redisdepends_on : Docker va démarrer mariadb et redis AVANT wordpress.
depends_on garantit l'ordre de démarrage, mais pas que MariaDB soit prête à accepter des connexions. C'est pour ça qu'on fait une boucle while dans le script de setup de WordPress.
nginx:
<<: *config
build: requirements/nginx
container_name: nginx
volumes:
- wordpress_data:/var/www/html
- certs:/etc/nginx/ssl2 volumes :
wordpress_data: nginx a besoin d'accéder aux fichiers PHP de WordPresscerts: les certificats SSL générés par le Makefile
ports:
- "443:443"Port mapping : 443 de l'hôte → 443 du conteneur.
Pourquoi seulement 443 ? Le sujet impose HTTPS uniquement (TLS 1.2 ou 1.3). Pas de HTTP en clair.
depends_on:
- wordpress
- staticNginx démarre après WordPress et le site statique.
adminer:
<<: *config
build: requirements/bonus/adminer
container_name: adminer
depends_on:
- mariadbAdminer : une interface web pour gérer la base de données. Pratique pour vérifier que MariaDB fonctionne.
static:
<<: *config
build: requirements/bonus/static
container_name: staticUn site statique tout simple (juste du HTML).
redis:
<<: *config
build: requirements/bonus/redis
container_name: redisRedis : un cache en mémoire pour accélérer WordPress.
ftp:
<<: *config
build: requirements/bonus/ftp
container_name: ftp
env_file:
- ../.env
volumes:
- wordpress_data:/var/www/html
- certs:/etc/nginx/ssl
ports:
- "21:21"
- "40000-40005:40000-40005"FTP avec SSL (FTPS) :
- Port
21: contrôle FTP - Ports
40000-40005: mode passif (pour les transferts de données) - Volume
wordpress_data: pour modifier les fichiers WordPress via FTP - Volume
certs: pour FTPS (FTP sécurisé)
secrets:
- ftp_passwordLe mot de passe FTP est stocké dans un secret.
portainer:
<<: *config
build: requirements/bonus/portainer
container_name: portainer
environment:
- DOCKER_API_VERSION=1.43
privileged: true
user: rootPortainer : une UI pour gérer Docker.
DOCKER_API_VERSION=1.43: version de l'API Docker
Solutions :
- Downgrade Docker vers une version stable (28.x)
- Forcer une API version plus ancienne au niveau système (voir section "Erreurs courantes")
- Spécifier l'API version dans le docker-compose (déjà fait ici avec
DOCKER_API_VERSION=1.43)
privileged: true: accès complet au système (nécessaire pour Portainer)user: root: run en tant que root
ports:
- "9443:9443"Portainer écoute sur le port 9443 (HTTPS).
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data/var/run/docker.sock : le socket Docker de l'hôte. Portainer en a besoin pour communiquer avec le daemon Docker.
secrets:
- portainer_passwordsecrets:
db_password:
file: ../secrets/db_password.txt
db_root_password:
file: ../secrets/db_root_password.txt
portainer_password:
file: ../secrets/portainer_password.txt
wp_user_password:
file: ../secrets/wp_user_password.txt
wp_admin_password:
file: ../secrets/wp_admin_password.txt
ftp_password:
file: ../secrets/ftp_password.txtDocker secrets : Docker monte ces fichiers en read-only dans /run/secrets/ de chaque conteneur qui les déclare.
Pourquoi utiliser des secrets ?
- Sécurité : pas de mots de passe dans les variables d'environnement (visibles avec
docker inspect) - Pas de mots de passe hardcodés dans les Dockerfiles
- Respect des bonnes pratiques
networks:
inception:
driver: bridgeNetwork bridge : tous les conteneurs peuvent se parler par leur nom de service.
Exemple : WordPress peut faire mariadb:3306 pour se connecter à MariaDB.
volumes:
wordpress_data:
driver: local
driver_opts:
type: none
o: bind
device: /home/namichel/data/wordpressBind mount : lie un dossier de l'hôte à un volume Docker.
type: none: pas de filesystem Docker, on utilise le filesystem de l'hôteo: bind: bind mountdevice: chemin absolu sur l'hôte
Pourquoi bind mount et pas volume Docker classique ?
Le sujet impose que les volumes soient sur /home/login/data. Avec un volume Docker classique, Docker choisirait lui-même l'emplacement (genre /var/lib/docker/volumes/).
Les autres volumes (mariadb_data, certs, portainer_data) suivent la même logique.
NGINX est ton reverse proxy. Il reçoit les requêtes HTTPS sur le port 443 et les dispatche vers les bons services.
FROM alpine:3.22Alpine 3.22 : version stable et légère.
RUN apk update && apk upgrade && \
apk add --no-cache nginx opensslLigne par ligne :
apk update: met à jour la liste des paquets disponiblesapk upgrade: met à jour les paquets installésapk add --no-cache: installe des paquets sans garder le cache (image plus légère)nginx: le serveur webopenssl: pour générer/manipuler des certificats SSL (même si on les génère avec le Makefile, c'est une bonne pratique)
Pourquoi && et \ ?
&&: exécute la commande suivante seulement si la précédente réussit\: continue la commande sur la ligne suivante (pour la lisibilité)
RUN mkdir -p /run/nginxCrée le dossier où nginx va stocker son PID. Sans ça, nginx refuse de démarrer.
RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
ln -sf /dev/stderr /var/log/nginx/error.logRedirection des logs vers stdout/stderr.
ln -sf: crée un lien symbolique (-s) en forçant (-f)/dev/stdout: sortie standard/dev/stderr: sortie d'erreur
Pourquoi faire ça ?
Docker capture stdout/stderr. En redirigeant les logs nginx vers stdout/stderr, tu peux les voir avec docker logs nginx.
COPY conf/nginx.conf /etc/nginx/http.d/default.confCopie ta config nginx personnalisée.
Pourquoi /etc/nginx/http.d/ et pas /etc/nginx/nginx.conf ?
Sur Alpine, nginx charge automatiquement tous les fichiers .conf dans /etc/nginx/http.d/. C'est plus propre de mettre ta config là-dedans.
EXPOSE 443Indique que le conteneur écoute sur le port 443. C'est purement informatif, ça ne fait pas le port mapping (c'est dans le docker-compose).
CMD ["nginx", "-g", "daemon off;"]Lance nginx en foreground.
-g "daemon off;": force nginx à rester en premier plan
Pourquoi daemon off ?
Docker a besoin d'un processus qui tourne en foreground. Si nginx se lance en daemon (arrière-plan), Docker pense que le conteneur est fini et l'arrête.
server {
listen 443 ssl;
server_name namichel.42.fr;Bloc server : définit un virtual host.
listen 443 ssl: écoute sur le port 443 avec SSL/TLSserver_name: le nom de domaine (doit correspondre au CN du certificat)
N'oublie pas d'ajouter namichel.42.fr dans /etc/hosts !
127.0.0.1 namichel.42.fr
ssl_certificate /etc/nginx/ssl/inception.crt;
ssl_certificate_key /etc/nginx/ssl/inception.key;
ssl_protocols TLSv1.2 TLSv1.3;Config SSL :
ssl_certificate: chemin vers le certificatssl_certificate_key: chemin vers la clé privéessl_protocols: protocoles autorisés (le sujet impose TLSv1.2 ou 1.3)
root /var/www/html;
index index.php;root: dossier racine des fichiers webindex: fichier par défaut (ici,index.phpde WordPress)
location / {
try_files $uri $uri/ /index.php?$args;
}Bloc location / : gère toutes les requêtes.
try_files $uri $uri/ /index.php?$args; :
- Essaye de servir le fichier tel quel (
$uri) - Si c'est un dossier, essaye de servir l'index (
$uri/) - Sinon, redirige vers
index.php(pour les permaliens WordPress)
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}Bloc location ~ \.php$ : gère les fichiers PHP.
~: regex\.php$: fichiers qui se terminent par.php
FastCGI :
include fastcgi_params: charge les paramètres FastCGI standardsfastcgi_pass wordpress:9000: transmet la requête au servicewordpresssur le port9000fastcgi_index index.php: fichier index par défautfastcgi_param SCRIPT_FILENAME: chemin complet du script PHP à exécuter
Pourquoi wordpress:9000 ?
Dans le réseau Docker inception, les services communiquent par leur nom. wordpress est le nom du service dans le docker-compose, et PHP-FPM écoute sur le port 9000.
location /adminer {
proxy_pass http://adminer:8080/;
proxy_set_header Host $host;
}Proxy vers Adminer.
Quand tu vas sur https://namichel.42.fr/adminer, nginx redirige vers http://adminer:8080/.
location /static/ {
proxy_pass http://static:8081/;
proxy_set_header Host $host;
}Même chose pour le site statique.
location /portainer/ {
proxy_pass https://portainer:9443/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Proxy vers Portainer (avec WebSocket).
UpgradeetConnection "upgrade": nécessaires pour les WebSockets (Portainer les utilise pour les mises à jour en temps réel)
WordPress, c'est le CMS (Content Management System). PHP-FPM (FastCGI Process Manager) exécute le code PHP.
FROM alpine:3.22RUN apk update && apk upgrade && \
apk add --no-cache php84 php84-fpm php84-mysqli php84-phar php84-mbstring wget mariadb-client php84-pecl-redis php84-tokenizer php84-ctypePaquets installés :
php84: PHP 8.4php84-fpm: PHP-FPM (pour communiquer avec nginx via FastCGI)php84-mysqli: extension MySQL/MariaDB pour PHPphp84-phar: pour manipuler les archives PHAR (nécessaire pour WP-CLI)php84-mbstring: gestion des strings multi-bytes (requis par WordPress)wget: pour télécharger WP-CLImariadb-client: client MySQL (pour tester la connexion à MariaDB)php84-pecl-redis: extension Redis pour PHP (pour le cache WordPress)php84-tokenizer: tokenizer PHP (requis par WP-CLI)php84-ctype: extension ctype (requis par WordPress)
RUN ln -s /usr/bin/php84 /usr/bin/phpCrée un lien symbolique php → php84. Comme ça, tu peux taper php au lieu de php84.
RUN wget https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x wp-cli.phar \
&& mv wp-cli.phar /usr/local/bin/wpInstallation de WP-CLI (WordPress Command Line Interface).
- Télécharge l'archive PHAR
- Rend le fichier exécutable
- Le déplace dans
/usr/local/bin/wp(pour pouvoir fairewpdans le terminal)
Pourquoi WP-CLI ? WP-CLI permet d'installer et configurer WordPress en ligne de commande. C'est bien plus simple que de le faire manuellement.
COPY conf/www.conf /etc/php84/php-fpm.d/www.confCopie ta config PHP-FPM personnalisée.
WORKDIR /var/www/htmlDéfinit le dossier de travail (tous les chemins relatifs partiront de là).
COPY conf/setup.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/setup.shCopie et rend exécutable le script de setup.
EXPOSE 9000PHP-FPM écoute sur le port 9000 (pour communiquer avec nginx via FastCGI).
ENTRYPOINT ["/usr/local/bin/setup.sh"]ENTRYPOINT vs CMD :
ENTRYPOINT: commande principale qui sera toujours exécutéeCMD: arguments par défaut (peuvent être overridés)
Ici, on veut que le script setup.sh s'exécute à chaque démarrage du conteneur.
Configuration PHP-FPM (www.conf)
[www]
user = PHP_USER_REPLACE
group = www-dataPool PHP-FPM.
user: l'utilisateur qui exécute PHP-FPM (on va le remplacer dynamiquement dans le script)group: le groupe
Pourquoi PHP_USER_REPLACE ?
C'est un placeholder. Le script setup.sh va le remplacer par la vraie valeur de $PHP_USER (définie dans le .env).
listen = 9000PHP-FPM écoute sur le port 9000.
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3Process Manager :
pm = dynamic: le nombre de processus s'adapte dynamiquementmax_children = 5: max 5 processus PHP en même tempsstart_servers = 2: démarre avec 2 processusmin_spare_servers = 1: garde au moins 1 processus en idlemax_spare_servers = 3: garde max 3 processus en idle
Pourquoi ces valeurs ?
Pour un projet comme Inception (usage local/dev), ces valeurs sont largement suffisantes. En prod, tu augmenterais max_children.
catch_workers_output = yesCapture les sorties des workers PHP (utile pour le debug).
#!/bin/shShebang : indique que c'est un script shell.
addgroup -S www-data 2>/dev/null || trueCréation du groupe www-data.
addgroup -S: crée un groupe système2>/dev/null: redirige les erreurs vers le néant|| true: retourne toujours un succès (même si le groupe existe déjà)
if ! id "$PHP_USER" > /dev/null 2>&1; then
deluser www-data 2>/dev/null
adduser -D -u 82 -G www-data "$PHP_USER"
fiGestion de l'utilisateur PHP.
if ! id "$PHP_USER": si l'utilisateur n'existe pasdeluser www-data: supprime l'utilisateur par défautadduser -D -u 82 -G www-data "$PHP_USER": crée l'utilisateur-D: pas de mot de passe-u 82: UID 82 (standard pour www-data)-G www-data: ajoute au groupe www-data
Pourquoi UID 82 ?
C'est l'UID standard de www-data. Ça évite les problèmes de permissions avec nginx (qui tourne aussi en www-data).
chown -R "$PHP_USER":www-data /var/www/htmlChange le propriétaire de tous les fichiers WordPress.
Pourquoi c'est important ?
PHP-FPM doit pouvoir lire/écrire dans /var/www/html (pour les uploads, les mises à jour, etc.).
while [ ! -f /run/secrets/db_password ]; do
echo "Waiting for secret file"
sleep 1
doneAttente du secret.
Boucle tant que le fichier secret n'existe pas. C'est une sécurité : si Docker met du temps à monter le secret, on attend.
DB_PASS=$(cat /run/secrets/db_password)
WP_ADMIN_PASS=$(cat /run/secrets/wp_admin_password)
WP_USER_PASS=$(cat /run/secrets/wp_user_password)Lecture des secrets.
$(cat fichier) : exécute la commande cat et stocke le résultat dans la variable.
until mariadb-admin ping -h"mariadb" --user="$SQL_USER" --password="$DB_PASS" --silent; do
echo "wordpress waiting for Mariadb"
sleep 2
doneAttente de MariaDB.
until commande; do: boucle tant que la commande échouemariadb-admin ping: envoie un "ping" à MariaDB pour vérifier qu'elle répond-h"mariadb": host (le nom du service Docker)--silent: mode silencieux
C'est LA solution au problème de depends_on.
depends_on garantit juste que MariaDB démarre avant WordPress, mais pas qu'elle soit prête. Cette boucle attend que MariaDB accepte les connexions.
if [ ! -f ./wp-config.php ]; then
echo "Wordpress is being installed"Vérification de l'installation.
Si wp-config.php existe, WordPress est déjà installé. Sinon, on installe.
Pourquoi cette vérification ? Si tu redémarres le conteneur, tu ne veux pas réinstaller WordPress à chaque fois (tu perdrais tes données).
php -d memory_limit=-1 /usr/local/bin/wp core download --allow-rootTéléchargement de WordPress.
php -d memory_limit=-1: lance PHP sans limite de mémoirewp core download: télécharge les fichiers WordPress--allow-root: autorise WP-CLI à tourner en root (nécessaire dans Docker)
wp config create \
--dbname=$SQL_DATABASE \
--dbuser=$SQL_USER \
--dbpass=$DB_PASS \
--dbhost=mariadb:3306 \
--allow-rootCréation du fichier wp-config.php.
--dbname: nom de la base de données--dbuser: utilisateur MySQL--dbpass: mot de passe--dbhost=mariadb:3306: host et port de MariaDB
Pourquoi mariadb:3306 et pas localhost ?
Dans Docker, localhost = le conteneur lui-même. Pour parler à MariaDB, on utilise le nom du service (mariadb).
wp core install \
--url=$DOMAIN_NAME \
--title="Inception" \
--admin_user=$WP_ADMIN_USER \
--admin_password=$WP_ADMIN_PASS \
--admin_email=$WP_ADMIN_EMAIL \
--skip-email \
--allow-rootInstallation de WordPress.
--url: l'URL du site (ex:https://namichel.42.fr)--title: titre du site--admin_user: nom de l'admin--admin_password: mot de passe admin--admin_email: email admin--skip-email: ne pas envoyer d'email de confirmation
wp user create $WP_USER $WP_USER_EMAIL \
--role=author \
--user_pass=$WP_USER_PASS \
--allow-rootCréation d'un deuxième utilisateur.
Le sujet impose d'avoir au moins 2 utilisateurs : un admin et un utilisateur normal.
--role=author: rôle "auteur" (peut créer/éditer ses propres articles)
echo "Wordpress OK!"
fiFin du bloc if. Si wp-config.php existait déjà, tout ce bloc est sauté.
wp config set WP_REDIS_HOST redis --allow-root
wp config set WP_REDIS_PORT 6379 --raw --allow-rootConfiguration de Redis dans WordPress.
WP_REDIS_HOST: l'host du serveur Redis (le nom du service Docker)WP_REDIS_PORT: le port (6379 par défaut)--raw: stocke la valeur brute (sans guillemets)
wp plugin install redis-cache --activate --allow-rootInstallation et activation du plugin Redis Cache.
Ce plugin permet à WordPress d'utiliser Redis comme cache.
wp redis enable --allow-rootActivation du cache Redis.
sed -i "s/PHP_USER_REPLACE/$PHP_USER/g" /etc/php84/php-fpm.d/www.confRemplacement du placeholder dans la config PHP-FPM.
sed -i: édite le fichier en places/PHP_USER_REPLACE/$PHP_USER/g: remplace toutes les occurrences dePHP_USER_REPLACEpar la valeur de$PHP_USER
Rappel : dans www.conf, on avait mis user = PHP_USER_REPLACE. Cette ligne le remplace par la vraie valeur (ex: user = www-data).
exec /usr/sbin/php-fpm84 -FLancement de PHP-FPM.
exec: remplace le processus du script par PHP-FPM-F: foreground (ne pas se lancer en daemon)
Pourquoi exec ?
Sans exec, le script tournerait en PID 1, et PHP-FPM en processus fils. Si le script se termine, Docker arrête le conteneur. Avec exec, PHP-FPM devient le PID 1, et le conteneur reste actif.
MariaDB, c'est la base de données. WordPress y stocke tous ses articles, utilisateurs, paramètres, etc.
FROM alpine:3.22RUN apk update && apk upgrade && \
apk add --no-cache mariadb mariadb-client && \
mkdir -p /var/run/mysqld /var/lib/mysql && \
chown -R mysql:mysql /var/run/mysqld /var/lib/mysqlInstallation de MariaDB.
mariadb: le serveur de base de donnéesmariadb-client: le client (pour se connecter en ligne de commande)mkdir -p /var/run/mysqld /var/lib/mysql: crée les dossiers nécessaireschown -R mysql:mysql: donne les permissions à l'utilisateurmysql
Pourquoi ces dossiers ?
/var/run/mysqld: où MariaDB stocke son socket et son PID/var/lib/mysql: où MariaDB stocke les bases de données
COPY conf/mariadb-server.cnf /etc/my.cnf.d/mariadb-server.cnfCopie ta config MariaDB personnalisée.
COPY conf/setup.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/setup.shEXPOSE 3306Port MySQL standard.
ENTRYPOINT ["/usr/local/bin/setup.sh"][client]
socket = /var/run/mysqld/mysqld.sockSection [client] : config pour les clients MySQL.
socket: où trouver le socket Unix
[mysqld]
skip-networking = false
bind-address = 0.0.0.0
port = 3306
user = mysqlSection [mysqld] : config du serveur.
skip-networking = false: autorise les connexions réseau (par défaut, MariaDB n'écoute que sur le socket Unix)bind-address = 0.0.0.0: écoute sur toutes les interfaces réseauport = 3306: port d'écouteuser = mysql: utilisateur qui exécute MariaDB
Pourquoi 0.0.0.0 et pas 127.0.0.1 ?
Dans Docker, 127.0.0.1 = localhost du conteneur. Pour que WordPress (dans un autre conteneur) puisse se connecter, MariaDB doit écouter sur toutes les interfaces (0.0.0.0).
#!/bin/sh
if [ ! -d "/var/lib/mysql/mysql" ]; then
echo "MariaDB storage init..."Vérification de l'initialisation.
Si le dossier /var/lib/mysql/mysql existe, MariaDB est déjà initialisée. Sinon, on initialise.
Pourquoi /var/lib/mysql/mysql ?
mysql est une base de données système créée par MariaDB lors de l'initialisation. Si elle existe, c'est que MariaDB a déjà été initialisée.
DB_PASS=$(cat /run/secrets/db_password)
DB_ROOT_PASS=$(cat /run/secrets/db_root_password)Lecture des secrets.
mysql_install_db --user=mysql --datadir=/var/lib/mysql --skip-test-db > /dev/nullInitialisation de MariaDB.
mysql_install_db: script d'initialisation--user=mysql: exécute en tant qu'utilisateurmysql--datadir=/var/lib/mysql: où stocker les données--skip-test-db: ne pas créer la base de test> /dev/null: redirige la sortie vers le néant (pour ne pas polluer les logs)
/usr/bin/mysqld --user=mysql --bootstrap << EOFMode bootstrap de MariaDB.
Le mode --bootstrap permet d'exécuter des commandes SQL avant le démarrage complet du serveur.
<< EOF : heredoc, tout ce qui suit jusqu'à EOF est passé en entrée de la commande.
USE mysql;
FLUSH PRIVILEGES;USE mysql: sélectionne la base de donnéesmysqlFLUSH PRIVILEGES: recharge les privilèges
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');Nettoyage de sécurité.
- Ligne 1 : supprime les utilisateurs anonymes (ceux sans nom)
- Ligne 2 : supprime les comptes
rootdistants (garde seulement les connexions locales)
Pourquoi ? Par défaut, MariaDB crée des comptes peu sécurisés. On les supprime.
ALTER USER 'root'@'localhost' IDENTIFIED BY '$DB_ROOT_PASS';Change le mot de passe root.
IDENTIFIED BY : définit le mot de passe.
CREATE DATABASE IF NOT EXISTS $SQL_DATABASE;Création de la base de données WordPress.
IF NOT EXISTS : ne fait rien si la base existe déjà (évite les erreurs).
CREATE USER IF NOT EXISTS '$SQL_USER'@'%' IDENTIFIED BY '$DB_PASS';Création de l'utilisateur WordPress.
'$SQL_USER'@'%': l'utilisateur peut se connecter depuis n'importe où (%= wildcard)
Pourquoi '%' et pas 'localhost' ?
WordPress tourne dans un autre conteneur. Pour MariaDB, ce n'est pas localhost, c'est une connexion réseau.
GRANT ALL PRIVILEGES ON $SQL_DATABASE.* TO '$SQL_USER'@'%';
FLUSH PRIVILEGES;
EOFAttribution des droits.
GRANT ALL PRIVILEGES ON $SQL_DATABASE.*: donne tous les droits sur toutes les tables de la base WordPressFLUSH PRIVILEGES: applique les changements
echo "Database and users created !"
fiFin du bloc if.
exec /usr/bin/mysqld --user=mysql --consoleLancement de MariaDB.
--user=mysql: exécute en tant qu'utilisateurmysql--console: affiche les logs sur la console (stdout)
Redis, c'est un cache en mémoire. WordPress l'utilise pour stocker temporairement des données fréquemment utilisées (posts, pages, requêtes), ce qui accélère le site.
FROM alpine:3.22RUN apk update && apk upgrade && \
apk add --no-cache redisSimple : on installe Redis.
COPY conf/redis.conf /etc/redis.confCopie la config Redis.
EXPOSE 6379Port Redis standard.
CMD ["redis-server", "/etc/redis.conf"]Lance Redis avec la config personnalisée.
bind 0.0.0.0
port 6379
protected-mode no
Config minimaliste.
bind 0.0.0.0: écoute sur toutes les interfaces (pour que WordPress puisse se connecter)port 6379: port par défautprotected-mode no: désactive le mode protégé
Pourquoi protected-mode no ?
En mode protégé, Redis refuse les connexions externes si aucun mot de passe n'est défini. Dans un réseau Docker privé, c'est safe de désactiver ce mode.
Le serveur FTP (avec SSL) permet d'uploader/modifier les fichiers WordPress depuis un client FTP (FileZilla, etc.).
FROM alpine:3.22RUN apk add --no-cache vsftpd openssl shadowPaquets installés :
vsftpd: Very Secure FTP Daemonopenssl: pour SSL/TLS (FTPS)shadow: pour gérer les utilisateurs/mots de passe
COPY conf/vsftpd.conf /etc/vsftpd.conf
COPY conf/setup.sh /usr/local/bin/setup.sh
RUN chmod +x /usr/local/bin/setup.shEXPOSE 21 40000-40005Ports FTP :
21: port de contrôle (commandes)40000-40005: ports passifs (transferts de données)
ENTRYPOINT ["/usr/local/bin/setup.sh"]listen=YES
listen_ipv6=NO
listen=YES: vsftpd écoute en IPv4listen_ipv6=NO: désactive IPv6 (pas nécessaire pour Inception)
local_enable=YES
write_enable=YES
local_enable=YES: autorise les utilisateurs locaux à se connecterwrite_enable=YES: autorise l'upload
chroot_local_user=YES
allow_writeable_chroot=YES
local_root=/var/www/html
Chroot (prison) :
chroot_local_user=YES: enferme l'utilisateur dans son dossier homeallow_writeable_chroot=YES: autorise l'écriture dans le chroot (normalement interdit par sécurité)local_root=/var/www/html: dossier racine du chroot
Pourquoi chroot ?
Pour éviter que l'utilisateur FTP puisse naviguer dans tout le filesystem. Il sera limité à /var/www/html.
seccomp_sandbox=NO
Désactive le sandbox seccomp (sinon vsftpd crash sur Alpine).
local_umask=022
Umask : permissions par défaut des fichiers créés.
022 = les fichiers créés auront les permissions 644 (rw-r--r--).
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=40005
pasv_address=0.0.0.0
Mode passif :
pasv_enable=YES: active le mode passif (nécessaire pour les connexions derrière un firewall)pasv_min_portetpasv_max_port: plage de ports pour les transfertspasv_address=0.0.0.0: adresse IP publique (ici, n'importe quelle interface)
ssl_enable=YES
allow_anon_ssl=NO
force_local_data_ssl=YES
force_local_logins_ssl=YES
ssl_tlsv1=YES
ssl_sslv2=NO
ssl_sslv3=NO
Configuration SSL (FTPS) :
ssl_enable=YES: active SSLallow_anon_ssl=NO: pas de SSL pour les connexions anonymesforce_local_data_ssl=YES: force SSL pour les transferts de donnéesforce_local_logins_ssl=YES: force SSL pour l'authentificationssl_tlsv1=YES: autorise TLS 1.xssl_sslv2=NOetssl_sslv3=NO: désactive SSLv2 et SSLv3 (obsolètes et non sécurisés)
rsa_cert_file=/etc/nginx/ssl/inception.crt
rsa_private_key_file=/etc/nginx/ssl/inception.key
Certificats SSL : on réutilise les mêmes certificats que nginx (via le volume certs).
require_ssl_reuse=NO
ssl_ciphers=HIGH
require_ssl_reuse=NO: ne pas réutiliser la session SSL (évite des bugs avec certains clients)ssl_ciphers=HIGH: utilise uniquement des ciphers forts
#!/bin/sh
FTP_PASS=$(cat /run/secrets/ftp_password)Lecture du mot de passe FTP depuis le secret.
if [ -z "$FTP_USER" ] || [ -z "$FTP_PASS" ]; then
echo "ERROR: FTP_USER or FTP_PASS not set"
exit 1
fiVérification des variables.
[ -z "$VAR" ]: vrai si la variable est videexit 1: quitte avec un code d'erreur
echo "Nettoyage manuel de l'UID 82..."
sed -i "/:x:82:/d" /etc/passwd
sed -i "/:x:82:/d" /etc/group
sed -i "/^$FTP_USER:/d" /etc/passwd
sed -i "/^$FTP_USER:/d" /etc/groupNettoyage des conflits d'UID.
Alpine crée parfois un utilisateur avec l'UID 82. On le supprime manuellement pour éviter les conflits.
sed -i "/:x:82:/d": supprime toutes les lignes contenant:x:82:(l'UID 82)sed -i "/^$FTP_USER:/d": supprime l'utilisateur s'il existe déjà
echo "Création de l'utilisateur $FTP_USER..."
adduser -D -u 82 -h /var/www/html -s /bin/sh "$FTP_USER"Création de l'utilisateur FTP.
-D: pas de mot de passe (on le définira après avecchpasswd)-u 82: UID 82 (pour correspondre àwww-data)-h /var/www/html: dossier home-s /bin/sh: shell par défaut
echo "$FTP_USER:$FTP_PASS" | chpasswdDéfinit le mot de passe.
chpasswd lit l'entrée standard au format user:password.
mkdir -p /var/run/vsftpd/empty
chmod 555 /var/run/vsftpd/emptyDossier requis par vsftpd pour le mode seccomp_sandbox.
chown root:root /var/www/html
chmod 755 /var/www/html
chown -R 82:82 /var/www/html 2>/dev/nullPermissions :
- Le dossier racine doit appartenir à
root(sécurité) - Les sous-dossiers appartiennent à l'utilisateur FTP (UID 82)
echo "Starting FTPS for $FTP_USER..."
exec /usr/sbin/vsftpd /etc/vsftpd.confLance vsftpd en foreground.
Adminer, c'est une interface web pour gérer MariaDB (comme phpMyAdmin, mais en un seul fichier PHP).
FROM alpine:3.22RUN apk update && apk upgrade && \
apk add --no-cache php84 php84-mysqli php84-session php84-iconv wgetPaquets installés :
php84: PHPphp84-mysqli: pour se connecter à MariaDBphp84-session: gestion des sessions (requis par Adminer)php84-iconv: conversion d'encodage (requis par Adminer)wget: pour télécharger Adminer
RUN ln -s /usr/bin/php84 /usr/bin/phpWORKDIR /var/www/html
RUN wget https://www.adminer.org/latest.php -O index.phpTéléchargement d'Adminer.
Adminer est un seul fichier PHP. On le télécharge et on le renomme index.php.
EXPOSE 8080CMD ["php", "-S", "0.0.0.0:8080", "index.php"]Serveur web PHP intégré.
php -S: lance le serveur web intégré de PHP0.0.0.0:8080: écoute sur toutes les interfaces, port 8080index.php: fichier à servir
Pourquoi ne pas utiliser nginx ? Pour Adminer, c'est overkill. Le serveur intégré de PHP suffit largement.
Un site statique tout simple pour montrer que tu maîtrises nginx.
FROM alpine:3.22RUN apk update && apk upgrade && \
apk add --no-cache nginxRUN mkdir -p /var/www/static /run/nginx/var/www/static: où stocker le HTML/run/nginx: pour le PID de nginx
COPY conf/default.conf /etc/nginx/http.d/default.conf
COPY conf/index.html /var/www/static/index.htmlEXPOSE 8081Port personnalisé (pour différencier du nginx principal).
CMD ["nginx", "-g", "daemon off;"]server {
listen 8081;
server_name localhost;
root /var/www/static;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Config ultra-simple.
- Écoute sur le port
8081 - Sert les fichiers depuis
/var/www/static - Si le fichier n'existe pas, retourne 404
<!DOCTYPE html>
<html>
<head>
<title>Site Statique - Inception</title>
<style>
body { background: #232323; color: #00ff00; font-family: monospace; text-align: center; padding: 50px; }
h1 { border: 2px solid #00ff00; display: inline-block; padding: 10px; }
</style>
</head>
<body>
<h1>SYSTEM STATUS: ONLINE</h1>
<p>Ceci est le site statique bonus de namichel.</p>
</body>
</html>Style hacker/matrix : fond noir, texte vert, monospace. Ça fait son petit effet ! 😎
Portainer, c'est une UI web pour gérer Docker (voir les conteneurs, images, volumes, etc.).
FROM alpine:3.22RUN apk update && apk upgrade && \
apk add --no-cache libc6-compat gcompat ca-certificates curlCompatibilité avec les binaires Linux.
Portainer est compilé pour glibc, mais Alpine utilise musl. On installe des libs de compatibilité :
libc6-compat: compatibilité libc6gcompat: compatibilité glibcca-certificates: certificats SSL (pour HTTPS)curl: pour télécharger Portainer
RUN curl -L https://github.com/portainer/portainer/releases/download/2.21.5/portainer-2.21.5-linux-amd64.tar.gz -o portainer.tar.gz && \
tar -xvf portainer.tar.gz && \
rm portainer.tar.gz && \
mv portainer /usr/local/bin/portainer_dirTéléchargement et extraction de Portainer.
curl -L: télécharge Portainer (le-Lsuit les redirections)tar -xvf: extrait l'archiverm portainer.tar.gz: supprime l'archivemv portainer /usr/local/bin/portainer_dir: déplace le dossier
WORKDIR /usr/local/bin/portainer_dirEXPOSE 9443Portainer écoute sur le port 9443 (HTTPS).
COPY conf/setup.sh /usr/local/bin/setup.sh
RUN chmod +x /usr/local/bin/setup.shENTRYPOINT ["/usr/local/bin/setup.sh"]#!/bin/sh
if [ ! -f /run/secrets/portainer_password ]; then
echo "Portainer: Password secret not found!"
else
echo "Portainer: Password secret OK!"
fiVérification du secret.
Simple check pour debug.
mkdir -p /dataCrée le dossier de données de Portainer.
exec /usr/local/bin/portainer_dir/portainer \
--data /data \
--assets /usr/local/bin/portainer_dir/ \
--admin-password-file /run/secrets/portainer_password \
-H unix:///var/run/docker.sockLancement de Portainer.
--data /data: où stocker les données--assets: où trouver les assets web (CSS, JS, etc.)--admin-password-file: fichier contenant le mot de passe admin-H unix:///var/run/docker.sock: se connecte au socket Docker de l'hôte
Le mot de passe Portainer :
Dans le Makefile, j'ai mis pass123456789 (13 caractères). Change-le pour un vrai mot de passe en correction.
Les secrets Docker permettent de stocker des données sensibles (mots de passe) de manière sécurisée.
Avantages :
- Pas de mots de passe dans les variables d'environnement (visibles avec
docker inspect) - Pas de mots de passe hardcodés dans les Dockerfiles
- Fichiers montés en read-only dans
/run/secrets/
- Dans le docker-compose.yml :
secrets:
db_password:
file: ../secrets/db_password.txt-
Docker monte le fichier dans
/run/secrets/db_passworddu conteneur. -
Dans le conteneur, on lit le secret :
DB_PASS=$(cat /run/secrets/db_password)db_password: mot de passe de l'utilisateur WordPress sur MariaDBdb_root_password: mot de passe root de MariaDBwp_admin_password: mot de passe de l'admin WordPresswp_user_password: mot de passe du deuxième utilisateur WordPressftp_password: mot de passe FTPportainer_password: mot de passe admin Portainer (min 12 caractères)
# Exemple avec un mot de passe fort
openssl rand -base64 32 > secrets/db_password.txt
# Ou manuellement
echo "MonMotDePasseComplexe123!" > secrets/wp_admin_password.txtsecrets/ dans ton .gitignore !
secrets/
.envTu dois créer un fichier .env à la racine de ton projet avec toutes les variables d'environnement.
Exemple de .env :
# Domain
DOMAIN_NAME=namichel.42.fr
# MariaDB
SQL_DATABASE=wordpress
SQL_USER=wpuser
# WordPress
WP_ADMIN_USER=admin
WP_ADMIN_EMAIL=admin@inception.fr
WP_USER=author
WP_USER_EMAIL=author@inception.fr
# PHP
PHP_USER=www-data
# FTP
FTP_USER=ftpuser.env dans le .gitignore !
Pour que namichel.42.fr fonctionne en local, ajoute-le dans /etc/hosts :
sudo nano /etc/hostsAjoute la ligne :
127.0.0.1 namichel.42.fr
Sauvegarde et ferme. Maintenant tu peux accéder à ton site via https://namichel.42.fr.
Ton navigateur va te crier dessus : "Connexion non sécurisée !". C'est NORMAL.
Le certificat est auto-signé, donc pas reconnu par les autorités de certification. En correction, tu devras expliquer ça.
Pour contourner l'avertissement :
- Chrome/Brave : clique sur "Avancé" → "Continuer vers le site"
- Firefox : "Avancé" → "Accepter le risque et continuer"
Si tu as des problèmes de permissions (WordPress ne peut pas écrire, FTP ne peut pas uploader), vérifie les UID/GID.
Vérifier les permissions :
ls -la /home/namichel/data/wordpressSi c'est cassé, reset les permissions :
sudo chown -R 82:82 /home/namichel/data/wordpressL'UID 82 = www-data (utilisé par nginx et PHP-FPM).
Voir les logs :
docker logs mariadb
docker logs wordpress
docker logs nginxEntrer dans un conteneur :
docker exec -it mariadb sh
docker exec -it wordpress shVérifier la connexion MariaDB depuis WordPress :
docker exec -it wordpress sh
mariadb -hmariadb -uwpuser -p
# Entre le mot de passe
SHOW DATABASES;Vérifier que nginx voit les fichiers WordPress :
docker exec -it nginx sh
ls -la /var/www/htmldepends_on garantit l'ordre de démarrage, mais PAS que le service soit prêt.
Exemple : MariaDB peut démarrer, mais mettre 5 secondes à accepter les connexions. Si WordPress se lance trop tôt, il crash.
Solution : les boucles until dans les scripts de setup.
until mariadb-admin ping -h"mariadb" --silent; do
echo "Waiting for MariaDB..."
sleep 2
doneVoir les stats Redis :
docker exec -it redis sh
redis-cli
INFO statsTu verras le nombre de commandes exécutées, les hits/misses du cache, etc.
Dans WordPress : va dans le dashboard, cherche "Redis" dans les plugins. Tu devrais voir le plugin "Redis Object Cache" activé avec un statut "Connected".
Avec FileZilla :
- Host :
namichel.42.fr - Port :
21 - Protocol :
FTP - Protocole de transfert de fichiers - Encryption :
Require explicit FTP over TLS - User : la valeur de
FTP_USERdans ton.env - Password : le contenu de
secrets/ftp_password.txt
Tu devrais voir les fichiers WordPress et pouvoir uploader/modifier.
Lors du premier accès à Portainer (https://namichel.42.fr/portainer/), tu devras :
- Créer un compte admin (le mot de passe que tu as mis dans le secret)
- Sélectionner "Docker" comme environnement
- Connect to the local Docker socket
Ensuite, tu verras tous tes conteneurs, images, volumes, etc. C'est très pratique pour le debug !
"Error establishing a database connection" (WordPress) :
- MariaDB n'est pas prête → vérifie les logs avec
docker logs mariadb - Mauvais mot de passe → vérifie que le secret est bien monté
- Mauvais nom d'hôte → dans
wp-config.php, ça doit êtremariadb:3306, paslocalhost
"502 Bad Gateway" (nginx) :
- PHP-FPM est down →
docker logs wordpress - PHP-FPM n'écoute pas sur le bon port → vérifie
www.conf(doit êtrelisten = 9000)
Portainer ne démarre pas :
- Mot de passe trop court → doit faire au moins 12 caractères
- Socket Docker non monté → vérifie le volume
/var/run/docker.sockdans le docker-compose
Portainer : "Cannot connect to the Docker daemon" (Docker Engine 29.0.0+) :
-
PROBLÈME CONNU : Docker Engine 29.0.0 a introduit des breaking changes incompatibles avec Portainer
-
Solution temporaire : forcer une version d'API Docker plus ancienne
# Éditer la configuration systemd de Docker sudo systemctl edit docker.service # Ajouter ces lignes AVANT "### Lines below this comment will be discarded" [Service] Environment=DOCKER_MIN_API_VERSION=1.24 # Sauvegarder et quitter (Ctrl+X, puis Y, puis Enter) # Redémarrer Docker sudo systemctl restart docker # Relancer tes conteneurs make re
-
Alternative : spécifier la version d'API directement dans le docker-compose (comme je l'ai fait)
portainer: environment: - DOCKER_API_VERSION=1.43
-
Vérifier ta version Docker :
docker --version
Si tu as Docker 29.0.0 ou plus récent, ce problème te concerne.
-
Solution long terme : attendre une mise à jour de Portainer compatible avec Docker 29.0.0+
FTP : "Connection refused" :
- Le port 21 n'est pas exposé → vérifie le
portsdans le docker-compose - vsftpd a crashé →
docker logs ftp
Rebuild tout et relancer :
make reVoir les conteneurs actifs :
docker psVoir tous les conteneurs (même arrêtés) :
docker ps -aVoir les volumes :
docker volume lsVoir les réseaux :
docker network lsVoir les images :
docker imagesSupprimer une image en force :
docker rmi -f <image_id>Voir l'utilisation disque de Docker :
docker system dfNettoyer tout ce qui est inutilisé :
docker system prune -a --volumesAvant ta correction, vérifie que :
✅ Tous les conteneurs démarrent correctement (docker ps)
✅ WordPress est accessible sur https://namichel.42.fr
✅ Tu peux te connecter avec l'admin et l'utilisateur normal
✅ Adminer est accessible sur https://namichel.42.fr/adminer
✅ Le site statique est accessible sur https://namichel.42.fr/static/
✅ Portainer est accessible sur https://namichel.42.fr/portainer/
✅ Tu peux te connecter en FTP avec FileZilla
✅ Redis est connecté (vérifie dans le dashboard WordPress)
✅ Tous tes Dockerfiles commencent par FROM alpine:3.22 ou une version d'Alpine
✅ Aucune image ne vient de Docker Hub (sauf Alpine)
✅ Les volumes persistent sur /home/login/data
✅ Les secrets ne sont pas visibles avec docker inspect
✅ Ton .env et secrets/ sont dans le .gitignore
✅ Tu n'as pas de mot de passe en clair dans les Dockerfiles ou docker-compose
"Pourquoi Alpine ?" → Légère (5 Mo), sécurisée, parfaite pour Docker. Les images sont plus petites et démarrent plus vite.
"Pourquoi l'UID 82 partout ?"
→ Sur Alpine, www-data a l'UID 82 (contrairement à Debian où c'est le 33). J'ai aligné tous mes services (PHP, nginx, FTP) sur cet UID pour éviter les conflits de permissions sur le volume partagé /var/www/html. C'est LE défi technique du projet : trois services doivent pouvoir lire/écrire les mêmes fichiers. La solution : un seul UID pour tout le monde.
"Pourquoi tu supprimes l'utilisateur par défaut dans le FTP ?" → Alpine crée automatiquement un utilisateur avec l'UID 82. Pour créer mon propre utilisateur FTP avec l'UID 82 (pour matcher PHP et nginx), je dois d'abord "libérer" cet UID en supprimant l'utilisateur par défaut. C'est un peu brutal, mais c'est la seule solution pour garantir que FTP, PHP et nginx utilisent tous le même UID.
"C'est quoi la différence entre CMD et ENTRYPOINT ?"
→ ENTRYPOINT = commande principale (toujours exécutée), CMD = arguments par défaut (peuvent être overridés).
"Pourquoi daemon off pour nginx ?"
→ Docker a besoin d'un processus en foreground. Si nginx se lance en daemon, Docker pense que le conteneur est fini.
"C'est quoi FastCGI ?" → Un protocole pour exécuter du code dynamique (PHP) via un serveur web (nginx). PHP-FPM écoute sur un port, nginx lui transmet les requêtes PHP.
"Pourquoi bind mount et pas volume Docker ?"
→ Le sujet impose que les volumes soient sur /home/login/data. Avec un volume Docker classique, on n'a pas le contrôle de l'emplacement.
"Comment nginx communique avec WordPress ?"
→ Via le réseau Docker inception. nginx envoie les requêtes PHP à wordpress:9000 (FastCGI). Pour les fichiers statiques, nginx les lit directement sur le volume partagé.
"Pourquoi Redis ?" → Cache en mémoire. WordPress stocke des données fréquemment utilisées (posts, pages, widgets) dans Redis au lieu de requêter MariaDB à chaque fois. Ça accélère le site.
"C'est quoi un secret Docker ?"
→ Un mécanisme pour stocker des données sensibles. Docker monte les secrets en read-only dans /run/secrets/. C'est plus sécurisé que les variables d'environnement.
"Pourquoi TLS 1.2 ou 1.3 ?" → Ce sont les versions sécurisées de TLS. Les versions antérieures (TLS 1.0, 1.1, SSL 2, SSL 3) sont obsolètes et vulnérables.
Voilà, tu as maintenant une vue complète de mon projet Inception. J'ai tout expliqué ligne par ligne, comme si on codait ensemble.
Les points clés à retenir :
- Architecture : nginx (reverse proxy) → WordPress (PHP-FPM) → MariaDB
- Alpine : légère et parfaite pour Docker
- Secrets : pour sécuriser les mots de passe
- Volumes bind : pour persister les données sur
/home/login/data - Network bridge : pour que les conteneurs communiquent entre eux
- Scripts de setup : pour initialiser les services au premier démarrage
- Boucles
until: pour attendre que les services dépendants soient prêts
Les bonus :
- Redis : cache WordPress
- FTP : uploader des fichiers
- Adminer : gérer la base de données
- Site statique : montrer la maîtrise de nginx
- Portainer : UI Docker
Mes conseils :
- Test, test, test. Lance
make resouvent pendant le dev. - Utilise
docker logsetdocker execpour debugger. - N'hésite pas à lire les docs officielles (nginx, PHP-FPM, MariaDB, etc.).
- En correction, sois clair et précis. Explique tes choix techniques.
Si tu as des questions ou des problèmes, n'hésite pas à relire ce tuto ou à checker les docs officielles. Bon courage pour ta correction, et que la force de Docker soit avec toi ! 🐳🚀
- Docker Docs
- Docker Compose Docs
- Alpine Linux Docs
- NGINX Docs
- WordPress Codex
- MariaDB Docs
- Redis Docs
- WP-CLI Handbook
Fait avec ❤️ par un étudiant de 42 pour les étudiants de 42.
N'oublie pas : le meilleur moyen d'apprendre, c'est de casser (et réparer) ton projet 1000 fois. make fclean && make est ton meilleur ami ! 😄