1. Synopsis
Ce document décrit le fonctionnement d'une stack LAMP (Linux, Apache, MySQL/MariaDB, PHP) placée derrière un reverse proxy HAProxy avec terminaison TLS. Il s'adresse aux techniciens support de niveau 2 et couvre le parcours complet d'une requête HTTP : de la résolution DNS jusqu'à la réponse client.
La stack typique utilise un conteneur LXC dédié par service, reliés par un bridge interne (vmbr1) sur un réseau privé 10.0.0.0/24. HAProxy est le seul point d'entrée exposé aux ports 80 et 443.
2. Architecture
2.1 Schéma réseau
┌─────────────────────────────────────────┐ │ I N T E R N E T │ └────────────────┬────────────────────────┘ │ ┌─────────┴─────────┐ │ DNS Resolver │ │ exemple.fr → IP │ └─────────┬─────────┘ │ Port 443 (HTTPS) ┌─────────┴─────────┐ │ HAProxy │ │ Terminaison SSL │ │ ACL Host routing │ │ :80 → redirect │ │ :443 → backends │ └─────────┬─────────┘ │ Port 80 (HTTP interne) ┌─────────┴─────────┐ │ Apache2 │ │ VirtualHost │ │ DocumentRoot │ │ .php → PHP-FPM │ └─────────┬─────────┘ │ Socket UNIX ┌─────────┴─────────┐ │ PHP-FPM │ │ Exécute le code │ │ Génère le HTML │ └─────────┬─────────┘ │ Socket / Port 3306 ┌─────────┴─────────┐ │ MariaDB │ │ Stockage données │ │ Requêtes SQL │ └───────────────────┘
2.2 Rôle de chaque brique
| Composant | Rôle | Écoute | Config principale |
|---|---|---|---|
| DNS | Résolution nom → IP | 53/udp | Zone chez le registrar |
| HAProxy | Reverse proxy, TLS termination, routage ACL | 80, 443 | /etc/haproxy/haproxy.cfg |
| Apache2 | Serveur web, VirtualHosts, délégation PHP | 80 (interne) | /etc/apache2/sites-enabled/ |
| PHP-FPM | Interpréteur PHP, pool de workers | socket unix | /etc/php/8.2/fpm/pool.d/ |
| MariaDB | SGBD, stockage persistant, requêtes SQL | 3306 | /etc/mysql/mariadb.conf.d/ |
3. Flux d'une requête
Un utilisateur saisit https://exemple.fr/index.php dans son navigateur. Voici ce qui se passe :
3.1 Simulateur interactif
Cliquez sur chaque étape pour voir le détail :
Étape 1 — Résolution DNS
Le navigateur interroge le résolveur DNS configuré (ex: 8.8.8.8). Le résolveur parcourt la hiérarchie : serveur racine → TLD (.fr) → serveur autoritaire pour obtenir l'enregistrement A. Le résultat est mis en cache selon le TTL.
Résultat : exemple.fr → 203.0.113.50
Étape 2 — Réception par HAProxy
Le paquet arrive sur le port 443. HAProxy effectue le handshake TLS (terminaison SSL), déchiffre le contenu et lit le header Host: exemple.fr. Il compare aux ACL définies et route vers le backend correspondant.
Si la requête arrive sur le port 80, HAProxy renvoie un 301 redirect vers HTTPS.
Étape 3 — Traitement Apache2
Apache reçoit la requête en HTTP pur (SSL déjà terminé). Il identifie le VirtualHost correspondant, localise le fichier dans le DocumentRoot.
Si c'est un .php, Apache le transmet à PHP-FPM via le socket UNIX. Les fichiers statiques (CSS, JS, images) sont servis directement.
Étape 4 — Exécution PHP-FPM
Un worker PHP-FPM libre prend la requête. Il exécute le script : initialisation $_GET, $_POST, $_SERVER, et si nécessaire, ouverture d'une connexion PDO vers MariaDB.
Le script génère le HTML de sortie retourné à Apache.
Étape 5 — Requête MariaDB
PHP envoie une requête SQL via PDO ou mysqli. MariaDB l'exécute avec le moteur InnoDB : parsing → optimisation → exécution → retour du jeu de résultats.
Étape 6 — Réponse au client
Le HTML généré remonte : MariaDB → PHP-FPM → Apache2 → HAProxy → Client
HAProxy réencapsule en TLS et l'envoie au navigateur. Les assets statiques déclenchent des requêtes supplémentaires qui suivent le même chemin sans passer par PHP.
X-Forwarded-For et Apache peut loguer les temps de réponse backend avec %D dans le LogFormat.
4. Configurations
4.1 HAProxy
Le fichier principal /etc/haproxy/haproxy.cfg se compose de 4 sections : global, defaults, frontend et backend.
global log /dev/log local0 maxconn 4096 ssl-default-bind-options ssl-min-ver TLSv1.2 defaults log global mode http option httplog timeout connect 5000ms timeout client 50000ms timeout server 50000ms # ── Frontend : redirection HTTP → HTTPS ── frontend http_front bind *:80 redirect scheme https code 301 # ── Frontend : entrée HTTPS ── frontend https_front bind *:443 ssl crt /etc/haproxy/certs/ # ACL : routage par header Host acl host_www hdr(host) -i exemple.fr acl host_api hdr(host) -i api.exemple.fr use_backend backend_www if host_www use_backend backend_api if host_api default_backend backend_deny # ── Backends ── backend backend_www server web1 10.0.0.10:80 check inter 10s backend backend_api server api1 10.0.0.11:80 check inter 10s backend backend_deny http-request deny deny_status 403
haproxy -c -f /etc/haproxy/haproxy.cfg
4.2 Apache2
Apache n'écoute qu'en HTTP sur le réseau interne. Chaque site a son VirtualHost.
<VirtualHost *:80> ServerName exemple.fr DocumentRoot /var/www/exemple/public ErrorLog ${APACHE_LOG_DIR}/exemple-error.log CustomLog ${APACHE_LOG_DIR}/exemple-access.log combined <Directory /var/www/exemple/public> Options -Indexes +FollowSymLinks AllowOverride All Require all granted </Directory> # Délégation PHP → PHP-FPM <FilesMatch \.php$> SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost" </FilesMatch> </VirtualHost>
Commandes Apache utiles
$ a2ensite exemple.conf # Activer le site $ a2enmod proxy_fcgi rewrite # Activer les modules $ apachectl -t # Tester la syntaxe $ systemctl reload apache2 # Appliquer sans coupure $ tail -f /var/log/apache2/error.log # Logs temps réel
4.3 PHP-FPM
PHP-FPM gère un pool de processus pour exécuter le code PHP. Chaque site peut avoir son propre pool.
[exemple] user = www-data group = www-data ; Socket UNIX — plus rapide que TCP en local listen = /run/php/php8.2-fpm.sock listen.owner = www-data listen.group = www-data ; Gestion dynamique du pool pm = dynamic pm.max_children = 20 ; max simultanés pm.start_servers = 5 ; au démarrage pm.min_spare_servers = 3 ; minimum idle pm.max_spare_servers = 10 ; maximum idle php_admin_value[error_log] = /var/log/php/exemple-error.log php_admin_flag[log_errors] = on
pm = dynamic convient à la plupart des cas. Utilisez ondemand pour les sites à faible trafic et static pour le fort trafic prévisible.
Directives php.ini importantes
memory_limit = 256M max_execution_time = 30 upload_max_filesize = 50M post_max_size = 50M display_errors = Off ; JAMAIS On en prod log_errors = On extension=pdo_mysql extension=mysqli
4.4 MariaDB
[mysqld] bind-address = 127.0.0.1 port = 3306 # InnoDB — adapter à la RAM dispo innodb_buffer_pool_size = 1G innodb_log_file_size = 256M innodb_flush_log_at_trx_commit = 2 # Slow query log slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow-query.log long_query_time = 1 character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
Créer un utilisateur et une base pour un site
mysql> CREATE DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql> CREATE USER 'app_user'@'localhost' IDENTIFIED BY '<MOT_DE_PASSE_FORT>'; mysql> GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'localhost'; mysql> FLUSH PRIVILEGES;
4.5 SSL / Certbot
Avec HAProxy en frontal, Certbot fonctionne en mode standalone. Il faut temporairement libérer le port 80.
# 1. Libérer le port 80 $ systemctl stop haproxy # 2. Demander le certificat $ certbot certonly --standalone \ --http-01-port 80 \ -d exemple.fr \ --email admin@exemple.fr \ --agree-tos --non-interactive # 3. Combiner fullchain + privkey pour HAProxy $ cat /etc/letsencrypt/live/exemple.fr/fullchain.pem \ /etc/letsencrypt/live/exemple.fr/privkey.pem \ > /etc/haproxy/certs/exemple.fr.pem # 4. Rétablir HAProxy $ systemctl start haproxy # 5. Vérifier $ echo | openssl s_client -connect exemple.fr:443 2>/dev/null \ | openssl x509 -noout -dates
--pre-hook / --post-hook de Certbot.
Renouvellement automatique via cron
# Renouvellement mensuel à 3h du matin
0 3 1 * * root \
systemctl stop haproxy && \
certbot renew --quiet && \
for domain in /etc/letsencrypt/live/*/; do \
d=$(basename "$domain"); \
cat "$domain/fullchain.pem" "$domain/privkey.pem" \
> "/etc/haproxy/certs/$d.pem"; \
done && \
systemctl start haproxy
5. Ports & protocoles
| Service | Port | Proto | Direction | Remarques |
|---|---|---|---|---|
| DNS | 53 | UDP/TCP | Client → Résolveur | Avant toute connexion |
| HAProxy (HTTP) | 80 | TCP | Client → HAProxy | Redirect 301 → HTTPS |
| HAProxy (HTTPS) | 443 | TLS | Client → HAProxy | Terminaison SSL + ACL |
| Apache2 | 80 | TCP | HAProxy → Apache | HTTP interne uniquement |
| PHP-FPM | — | UNIX | Apache → PHP-FPM | /run/php/php-fpm.sock |
| MariaDB | 3306 | TCP | PHP → MariaDB | bind 127.0.0.1 |
6. Troubleshooting
Diagnostiquez toujours de l'extérieur vers l'intérieur : DNS → HAProxy → Apache → PHP → BDD.
- ERR_NAME_NOT_RESOLVED — Le domaine ne résout pas
-
L'enregistrement DNS A est absent ou pointe vers la mauvaise IP. Vérifier la zone chez le registrar et le TTL.
$ dig exemple.fr +short @8.8.8.8 - ERR_CONNECTION_REFUSED — Rien n'écoute
-
HAProxy est down ou le firewall bloque. Vérifier le service et les règles iptables/DNAT.
$ ss -tlnp | grep -E ':80|:443' && systemctl status haproxy - 503 Service Unavailable — Backend injoignable
-
HAProxy ne peut pas joindre Apache (conteneur down, IP incorrecte, Apache stoppé).
$ curl -v http://10.0.0.10:80 && systemctl status apache2 - 502 Bad Gateway — PHP-FPM ne répond plus
-
Le socket PHP-FPM est absent, mauvaises permissions, ou le service a crashé.
$ ls -la /run/php/php8.2-fpm.sock && systemctl status php8.2-fpm - Page blanche / HTTP 500 — Erreur fatale PHP
-
Erreur de syntaxe, extension manquante ou exception non catchée.
$ tail -30 /var/log/php/exemple-error.log - SQLSTATE[HY000] — Connexion BDD refusée
-
MariaDB stoppée, credentials incorrects ou droits SQL manquants.
$ systemctl status mariadb && mysql -u app_user -p -e "SHOW DATABASES;" - NET::ERR_CERT_DATE_INVALID — Certificat expiré
-
Le cron de renouvellement a échoué ou le .pem n'a pas été regénéré.
$ echo | openssl s_client -connect exemple.fr:443 2>/dev/null | openssl x509 -noout -dates - Lenteurs > 2s — Requêtes lentes
-
SQL lent, pool FPM saturé ou buffer pool InnoDB sous-dimensionné.
$ tail -20 /var/log/mysql/slow-query.log
curl -v à chaque étape suffit souvent à trouver.