Administration Linux
Installer une pile LAMP : Apache, MariaDB, PHP-FPM, virtual hosts et TLS
Intermédiaire9 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Un serveur LAMP, ce sont quatre services indépendants qui doivent s'accorder sur les sockets, les utilisateurs et les permissions de fichiers. Ce guide en monte un en connaissance de cause plutôt qu'en copiant une commande toute faite : Apache avec le MPM event et des virtual hosts nommés, MariaDB avec un compte applicatif au moindre privilège, PHP-FPM dans son propre pool, une propriété raisonnable de la racine web et le TLS de Let's Encrypt. Chaque étape se termine par un moyen de la vérifier.
Ce que vous allez apprendre
- Installer et vérifier Apache, MariaDB et PHP-FPM sur un serveur Debian ou de la famille RHEL
- Configurer des virtual hosts par nom avec des racines web et des fichiers de log distincts
- Créer une base de données et un compte applicatif doté uniquement des privilèges nécessaires
- Relier Apache à PHP-FPM par un socket et dimensionner un pool selon la mémoire disponible
- Obtenir et renouveler un certificat Let's Encrypt, puis appliquer une checklist de durcissement de base
LAMP est depuis vingt-cinq ans la manière par défaut de servir un site web dynamique, et le sens de l'acronyme a discrètement changé en chemin. Le L de Linux n'a pas bougé. Le A désigne toujours Apache, mais le modèle de processus recommandé n'est plus le même. Le M correspond désormais le plus souvent à MariaDB. Et le P ne tourne plus du tout dans le serveur web : PHP est un service à part, auquel Apache parle via un socket.
C'est ce dernier changement qu'il faut comprendre, car il explique l'essentiel de la configuration qui suit. Ce guide monte pas à pas un serveur fonctionnel sur un hôte Debian ou Ubuntu récent, signale les équivalents pour la famille RHEL et vérifie chaque couche avant de passer à la suivante. Rien d'exotique ici : tout l'intérêt est de procéder en connaissance de cause, pour savoir, le jour où quelque chose casse, lequel des quatre services examiner.
Installer les composants
# Debian / Ubuntu
sudo apt update
sudo apt install apache2 mariadb-server php-fpm php-mysql php-mbstring php-xml php-curl php-zip
# RHEL family (Rocky, Alma, RHEL) — Apache is packaged as httpd
sudo dnf install httpd mariadb-server php-fpm php-mysqlnd php-mbstring php-xml
sudo systemctl enable --now httpd mariadb php-fpm
Les noms de paquets diffèrent (apache2 contre httpd, php-mysql contre php-mysqlnd), tout comme l'organisation de la configuration : Debian utilise /etc/apache2/ avec sites-available et sites-enabled, plus les utilitaires a2ensite et a2enmod, tandis que RHEL s'appuie sur un répertoire plat, /etc/httpd/conf.d/. Tout le reste se transpose. Si la gestion des paquets elle-même vous est peu familière, le guide sur la gestion des paquets couvre les dépôts et l'épinglage de versions.
Vérifiez que les services tournent avant de toucher à la moindre configuration :
$ systemctl is-active apache2 mariadb php8.3-fpm
active
active
active
$ sudo ss -ltnp | grep -E ':(80|3306)'
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("apache2",pid=1201,fd=4))
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:* users:(("mariadb",pid=1408,fd=25))
Notez que MariaDB n'écoute par défaut que sur l'interface de loopback. C'est le bon réglage pour un hôte LAMP mono-serveur, et il doit le rester : une base de données dotée d'une adresse publique est une invitation, et le guide sur le réseau explique comment l'atteindre par un tunnel SSH quand vous avez besoin d'un client sur votre poste.
Apache : modèle de processus et modules
Apache 2.4 choisit un module multi-processus (MPM) à la compilation ou à la configuration. prefork crée un processus par connexion et existe surtout pour mod_php ; event est un modèle multithreadé qui sort les connexions keep-alive inactives des threads de travail et encaisse bien plus de concurrence pour la même mémoire. Avec PHP exécuté sous FPM, event est le bon choix.
sudo a2dismod php8.3 mpm_prefork 2>/dev/null
sudo a2enmod mpm_event proxy_fcgi setenvif rewrite headers ssl http2
sudo a2enconf php8.3-fpm
sudo systemctl restart apache2
apache2ctl -M | grep -E 'mpm|fcgi|rewrite'
proxy_fcgi transmet les requêtes PHP à FPM ; rewrite est requis par le front controller de presque tous les frameworks PHP ; headers permet de définir les en-têtes de sécurité ; quant à ssl et http2, vous en aurez besoin sous peu. N'activez rien d'autre : chaque module, c'est du code sur le chemin de la requête.
Régler le MPM event relève de l'arithmétique, pas du folklore. Apache est désormais léger puisque PHP tourne ailleurs ; les limites servent donc surtout à vous éviter de mettre en file d'attente plus de travail que la machine ne peut en absorber :
# /etc/apache2/mods-available/mpm_event.conf
<IfModule mpm_event_module>
StartServers 2
MinSpareThreads 25
MaxSpareThreads 75
ThreadsPerChild 25
MaxRequestWorkers 150
MaxConnectionsPerChild 0
</IfModule>
Virtual hosts
Un virtual host par nom permet à une seule adresse de servir plusieurs sites : Apache retient le bloc dont le ServerName ou un ServerAlias correspond à l'en-tête Host. Donnez à chaque site son propre fichier, sa propre racine web et ses propres fichiers de log.
# /etc/apache2/sites-available/example.com.conf
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public
<Directory /var/www/example.com/public>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# Never serve the application's internals, even if a path escapes the root
<DirectoryMatch "/(\.git|vendor|storage)/">
Require all denied
</DirectoryMatch>
ErrorLog ${APACHE_LOG_DIR}/example.com-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined
</VirtualHost>
sudo a2ensite example.com
sudo apache2ctl configtest # or: httpd -t
sudo systemctl reload apache2
Quatre détails de ce bloc méritent une explication. DocumentRoot pointe vers un sous-répertoire public/ et non vers la racine du projet : les fichiers de configuration et les dépendances se trouvent ainsi physiquement hors de l'arborescence servie. Options -Indexes empêche Apache de générer un listing du répertoire en l'absence de fichier d'index, une source classique de divulgation accidentelle. AllowOverride None désactive .htaccess : Apache n'a plus à chercher ces fichiers à chaque requête et votre configuration reste en un seul endroit ; ne le réactivez que pour les applications qui en ont réellement besoin. Enfin, Require all granted est la syntaxe d'autorisation de la 2.4, qui a remplacé Order allow,deny ; les vieux tutoriels qui montrent la forme 2.2 produisent une configuration qui ne se charge pas.
Lancez toujours configtest avant de recharger. Une erreur de syntaxe au moment d'un reload laisse l'ancienne configuration en service ; une erreur découverte seulement au restart laisse le site hors ligne.
MariaDB : sécuriser, puis appliquer le moindre privilège
La première chose à exécuter après l'installation est le script de durcissement, qui supprime les comptes anonymes et la base de test, et propose de désactiver la connexion root à distance :
sudo mariadb-secure-installation # mysql_secure_installation on MySQL
Sur les versions actuelles de MariaDB, le compte root s'authentifie via le plugin unix_socket : sudo mariadb fonctionne sans mot de passe, et une connexion root à distance est impossible par construction. Gardez ce fonctionnement. Les applications reçoivent leur propre compte, limité à leur propre base et à l'hôte depuis lequel elles se connectent :
CREATE DATABASE shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'shop_app'@'localhost' IDENTIFIED BY 'a-long-random-passphrase';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_app'@'localhost';
-- A separate account for migrations, used only by the deployment process
CREATE USER 'shop_ddl'@'localhost' IDENTIFIED BY 'another-long-passphrase';
GRANT ALL PRIVILEGES ON shop.* TO 'shop_ddl'@'localhost';
FLUSH PRIVILEGES;
SHOW GRANTS FOR 'shop_app'@'localhost';
utf8mb4 est le seul jeu de caractères correct ; l'ancien utf8 de MySQL stocke au plus trois octets par caractère et ne peut représenter ni les emojis ni certains caractères CJK. Séparer le compte d'exécution du compte de migration du schéma garantit qu'une faille d'injection SQL dans l'application ne pourra pas supprimer une table : c'est un contrôle peu coûteux et efficace. Le versant schéma et requêtes est traité dans les bonnes pratiques SQL.
Deux tâches d'exploitation ont leur place ici plutôt que plus tard. Faites une sauvegarde et prouvez que vous savez la restaurer :
sudo mariadb-dump --single-transaction --routines --events shop | zstd -o /srv/backups/shop-$(date +%F).sql.zst
zstd -dc /srv/backups/shop-2026-09-07.sql.zst | sudo mariadb shop_restore_test
Et tenez les identifiants à l'écart de l'historique du shell et de la liste des processus en les plaçant dans un fichier d'options en mode 600 plutôt que sur la ligne de commande :
# /root/.my.cnf (chmod 600)
[client]
user = root
password = only-if-you-are-not-using-socket-auth
PHP-FPM
FPM gère des pools : des groupes indépendants de processus workers, chacun avec son utilisateur, son socket et ses limites. Donnez à chaque site son propre pool, pour qu'une application qui s'emballe ne puisse pas priver les autres de ressources et que la propriété des fichiers reste cloisonnée.
; /etc/php/8.3/fpm/pool.d/example.com.conf
[example.com]
user = www-example
group = www-example
listen = /run/php/php8.3-fpm-example.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
php_admin_value[error_log] = /var/log/php/example.com-error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 16M
php_admin_value[post_max_size] = 16M
php_admin_value[open_basedir] = /var/www/example.com:/tmp
Le paramètre qui compte, c'est pm.max_children. Multipliez-le par la mémoire moyenne consommée par une requête (mesurez-la, ne la devinez pas) : le résultat doit tenir largement en RAM, en laissant de la place à MariaDB et au cache de pages. Vingt workers à 80 Mo, cela fait 1,6 Go, déjà trop pour une machine de 2 Go. pm.max_requests recycle un worker après un certain nombre de requêtes, ce qui masque les fuites lentes du code tiers.
open_basedir confine l'interpréteur aux chemins listés : une faille d'inclusion de fichier ne peut donc pas lire /etc/passwd. Contrairement à un simple php_value, une valeur définie par php_admin_value ne peut pas être modifiée par l'application à l'exécution.
Faites pointer le virtual host vers le socket :
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.3-fpm-example.sock|fcgi://localhost/"
</FilesMatch>
<FilesMatch "\.(env|ini|log|sh|sql|bak)$">
Require all denied
</FilesMatch>
sudo systemctl reload php8.3-fpm apache2
En production, activez le cache d'opcodes et désactivez les réglages qui laissent fuiter des informations. Dans /etc/php/8.3/fpm/php.ini :
display_errors = Off
expose_php = Off
opcache.enable = 1
opcache.memory_consumption = 192
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
opcache.validate_timestamps = 0 évite à PHP de vérifier la date de modification des fichiers à chaque requête, un gain mesurable, mais vous oblige à recharger FPM à chaque déploiement. Intégrez-le à votre script de déploiement plutôt que de compter sur votre mémoire.
Permissions de la racine web
C'est ici que la plupart des installations LAMP dérapent sans bruit, généralement à coups de chmod -R 777. Le principe est simple : le serveur web doit pouvoir lire l'application et n'écrire que dans les quelques répertoires qui en ont réellement besoin.
sudo useradd -r -M -d /var/www/example.com -s /usr/sbin/nologin www-example
sudo chown -R deploy:www-example /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 750 {} +
sudo find /var/www/example.com -type f -exec chmod 640 {} +
# Only these need to be writable by the PHP worker
sudo chown -R www-example:www-example /var/www/example.com/storage /var/www/example.com/var/cache
sudo chmod -R 770 /var/www/example.com/storage
Les fichiers appartiennent au compte de déploiement et sont lisibles par le groupe du pool ; l'utilisateur du pool ne possède que les répertoires accessibles en écriture. Rien n'est lisible par tous, et les fichiers téléversés ne peuvent pas être exécutés, puisque l'interpréteur ne traite que les chemins correspondant au handler FilesMatch. C'est aussi pourquoi un répertoire d'upload devrait être servi depuis un chemin où la prise en charge de .php est entièrement désactivée. Le raisonnement derrière ces modes, ainsi que l'alternative par ACL quand la propriété devient compliquée, se trouve dans utilisateurs, groupes et permissions.
Sur un hôte de la famille RHEL, SELinux ajoute une seconde couche. Les fichiers doivent porter la bonne étiquette, et le démon a besoin d'un booléen avant de pouvoir, par exemple, ouvrir une connexion réseau :
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/example.com(/.*)?"
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example.com/storage(/.*)?"
sudo restorecon -Rv /var/www/example.com
sudo setsebool -P httpd_can_network_connect_db on
Désactiver SELinux n'est pas une solution ; ausearch -m avc -ts recent vous dit précisément ce qui a été refusé.
TLS avec Let's Encrypt
Certbot obtient un certificat, modifie le virtual host et installe un timer de renouvellement.
sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d example.com -d www.example.com --redirect --agree-tos -m [email protected]
sudo certbot renew --dry-run
systemctl list-timers 'certbot*'
--redirect ajoute la redirection permanente du port 80 vers le 443. Le challenge HTTP-01 a besoin du port 80 ouvert sur Internet : laissez-le joignable plutôt que de le bloquer au pare-feu après la première émission. Fixez ensuite explicitement la politique de protocoles dans le virtual host TLS :
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLHonorCipherOrder off
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Protocols h2 http/1.1
N'activez HSTS qu'une fois certain que chaque sous-domaine continuera de fonctionner en HTTPS : les navigateurs gardent l'en-tête en cache pendant toute sa durée de vie, et il n'existe aucun moyen rapide de revenir en arrière. Commencez avec un max-age court, vérifiez que rien n'a cassé, puis augmentez-le.
Vérifier toute la pile
$ curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' http://example.com/
301 https://example.com/
$ curl -sSI https://example.com/ | head -6
HTTP/2 200
server: Apache
strict-transport-security: max-age=31536000; includeSubDomains
$ printf '<?php echo "db: ", (new PDO("mysql:host=localhost;dbname=shop","shop_app","...")) ? "ok" : "fail";' \
| sudo -u www-example php
db: ok
$ sudo tail -n 20 /var/log/apache2/example.com-error.log
Vérifiez quatre choses séparément : la redirection fonctionne, la terminaison TLS renvoie bien les en-têtes que vous avez définis, PHP atteint la base de données en tant qu'utilisateur du pool, et le log d'erreurs reste silencieux. Ne laissez jamais traîner une page phpinfo() : elle publie vos chemins, vos extensions et souvent vos variables d'environnement.
Checklist de durcissement
- Maintenez l'hôte à jour, idéalement avec les mises à jour de sécurité automatiques activées.
- Filtrez tout au pare-feu sauf 80, 443 et SSH ; gardez MariaDB sur le loopback.
- Faites tourner chaque site sous l'utilisateur de son propre pool FPM, avec
open_basedirdéfini. display_errors = Off,expose_php = Off,ServerTokens ProdetServerSignature Off.- Interdisez dans le virtual host l'accès aux fichiers cachés, à
.env, aux sauvegardes et aux répertoires de gestion de versions. - Désactivez
.htaccess, sauf si une application en a besoin. - Définissez
LimitRequestBody, et utilisezmod_reqtimeoutetmod_evasiveou un reverse proxy pour un rate limiting de base. - Assurez la rotation et la surveillance des logs de chaque site ; expédiez-les là où ils survivront à la machine.
- Sauvegardez la base de données et le répertoire des uploads, et restaurez régulièrement les deux dans un environnement jetable.
Erreurs fréquentes
chmod -R 777sur la racine web. N'importe quel compte local peut désormais réécrire votre application. Corrigez plutôt la propriété des fichiers.- Faire pointer
DocumentRootvers la racine du projet..env,composer.jsonet.gitdeviennent téléchargeables. - Laisser
AllowOverride Allpar habitude. Cela coûte des accès au système de fichiers à chaque requête et éparpille la configuration. - Utiliser la syntaxe d'autorisation d'Apache 2.2.
Order allow,denyne s'applique plus ; utilisezRequire. - Redémarrer au lieu de recharger après une modification de configuration.
configtestpuisreloadgarde le site en ligne si vous avez fait une faute de frappe. - Monter
pm.max_childrentrès haut parce qu'un tutoriel l'a fait. Multipliez d'abord par la mémoire par requête ; un surdimensionnement transforme un pic de trafic en swap. - Accorder
ALL PRIVILEGESau compte applicatif. Séparez les identifiants d'exécution et ceux de migration. - Désactiver SELinux pour faire disparaître une 403. Étiquetez les fichiers ;
ausearchvous dit lesquels.
Points clés à retenir
- Une pile LAMP moderne exécute PHP dans un service FPM distinct, ce qui libère Apache pour le MPM event multithreadé et permet à chaque site de tourner sous son propre utilisateur.
- Donnez à chaque site son propre fichier de virtual host, une racine web sous
public/, ses fichiers de log et son pool FPM. - Servez depuis un répertoire que l'utilisateur web peut lire mais pas modifier, avec une courte liste de chemins explicitement accessibles en écriture ; jamais de
777. - Sécurisez MariaDB dès l'installation, laissez-la sur le loopback, utilisez
utf8mb4et séparez le compte d'exécution du compte de migration. - Dimensionnez
pm.max_childrenà partir de la mémoire par requête mesurée, et activez OPcache avec la validation des timestamps désactivée, plus un rechargement de FPM dans votre déploiement. - Let's Encrypt via certbot gère l'émission et le renouvellement ; ajoutez HSTS et les autres en-têtes de sécurité de façon délibérée, en commençant par une durée de vie courte.
- Validez chaque couche avec
configtest,curl -I, une connexion à la base en tant qu'utilisateur du pool et un log d'erreurs silencieux avant de déclarer le serveur terminé.
Sources et lectures complémentaires
Questions fréquentes
- Faut-il choisir MySQL ou MariaDB ?
- Pour un nouveau serveur LAMP, les deux conviennent et les deux sont dans les dépôts par défaut. MariaDB est le choix par défaut de la plupart des distributions Linux, est entièrement open source et reste compatible au niveau du protocole pour les applications PHP courantes. Prenez MySQL lorsqu'une application exige explicitement une fonctionnalité d'Oracle MySQL ou une version précise. Quel que soit votre choix, gardez la bibliothèque cliente et le serveur dans des versions compatibles, et évitez de mélanger sur un même hôte des paquets des deux projets.
- Quelle différence entre mod_php et PHP-FPM ?
- mod_php embarque l'interpréteur dans chaque processus Apache, ce qui impose le MPM prefork et un processus lourd par connexion. PHP-FPM exécute PHP dans un pool distinct de workers auquel Apache s'adresse via un socket : Apache peut alors utiliser le MPM event multithreadé, les fichiers statiques ne coûtent presque rien et chaque site peut tourner sous son propre utilisateur, avec ses propres limites. FPM est la recommandation actuelle pour tout ce qui dépasse le stade du site jouet.
- Pourquoi une erreur 403 Forbidden sur un répertoire qui existe bel et bien ?
- Trois causes expliquent presque tous les cas. L'utilisateur Apache ne peut pas traverser chacun des répertoires du chemin : vérifiez-les un par un avec namei -l. La configuration ne contient pas de Require all granted pour ce répertoire. Ou SELinux est en mode enforcing et les fichiers ne portent pas l'étiquette httpd_sys_content_t. Lisez d'abord le log d'erreurs : Apache y indique laquelle des trois il a rencontrée, et deviner fait perdre bien plus de temps.
- Comment les renouvellements Let's Encrypt se font-ils concrètement ?
- Le paquet certbot installe un timer systemd qui s'exécute deux fois par jour, contrôle chaque certificat et renouvelle ceux auxquels il reste moins de trente jours. Rien n'est renouvelé avant, il est donc normal que le timer tourne souvent. Vérifiez toute la chaîne avec certbot renew --dry-run, qui effectue un vrai challenge contre l'environnement de staging, et veillez à ce que le port 80 reste joignable, car le challenge HTTP-01 en a besoin.