Stack LAMP sur Ubuntu 24.04 avec PHP-FPM
Construisez une stack LAMP complète sur Ubuntu 24.04 : Apache, MariaDB en unix_socket, PHP 8.3 avec PHP-FPM, un vhost par nom et HTTPS gratuit via Certbot.
Ce que vous allez construire
Une stack LAMP, ce sont quatre éléments sur un seul serveur Ubuntu 24.04 : Linux en dessous, Apache qui répond en HTTP, MariaDB qui stocke les données et PHP 8.3 qui exécute le code. À la fin, vous aurez un hôte virtuel par nom qui sert un vrai répertoire d'application, une base de données avec un utilisateur dédié à privilèges minimaux, PHP relié à Apache via PHP-FPM et un certificat Let's Encrypt gratuit par-dessus.
L'installation elle-même tient en quatre commandes apt. Presque tout dans ce guide concerne le câblage entre les pièces, et le petit ensemble d'erreurs qui font qu'une stack toute neuve sert une page blanche, livre votre code source au navigateur sous forme de téléchargement ou refuse de vous laisser entrer dans la base de données que vous venez d'installer. Chacune de ces erreurs a une signature reconnaissable, et chacune est nommée ci-dessous avec le texte exact que vous verrez.
Prérequis et les pièges à connaître
On part d'un VPS KVM Ubuntu 24.04 tout neuf, avec un utilisateur sudo ou root, et une adresse IPv4 publique. Une stack minimale tourne dans 1 Go de RAM ; prévoyez-en 2 Go avant d'y installer une vraie application adossée à une base de données, car les tampons par défaut de MariaDB plus une poignée de processus PHP-FPM consomment vite le premier gigaoctet.
Deux conditions doivent être remplies avant que Certbot fonctionne à la fin, alors préparez-les dès maintenant. Il vous faut un nom de domaine avec un enregistrement A pointant vers l'IP publique du VPS : Let's Encrypt valide en HTTP vers ce nom, et une simple adresse IP ne peut jamais obtenir de certificat. Et les ports 80 et 443 doivent être joignables depuis Internet, ce qui, chez beaucoup d'hébergeurs, signifie les ouvrir dans un pare-feu réseau du panneau de contrôle en plus de ufw sur la machine. Les changements DNS peuvent prendre jusqu'à une heure pour se propager, alors définissez l'enregistrement A en premier et il sera actif au moment où vous en aurez besoin.
Étape 1 - Installer Apache et vérifier la page par défaut
sudo apt update
sudo apt install -y apache2apt démarre et active le service pour vous. Vérifiez-le :
systemctl status apache2Vous voulez voir une ligne indiquant active (running). Ouvrez maintenant http://YOUR_SERVER_IP/ dans un navigateur. La Apache2 Ubuntu Default Page avec sa grande bannière "It works!" est le résultat correct : c'est la preuve qu'Apache sert des pages, pas une erreur. Cette page se trouve dans /var/www/html/index.html et est servie par l'hôte virtuel par défaut fourni, 000-default.conf. Vous désactiverez les deux plus tard ; pour l'instant, leur présence est exactement ce que vous voulez voir.
Si la page ne se charge pas du tout alors que systemctl indique que le processus tourne, c'est un pare-feu qui bloque. C'est l'étape suivante.
Étape 2 - Ouvrir le pare-feu pour HTTP et HTTPS
Le paquet apache2 enregistre trois profils d'application ufw. Listez-les :
sudo ufw app listVous verrez Apache, Apache Full et Apache Secure. Apache couvre uniquement le port 80, Apache Secure uniquement le 443, et Apache Full les deux : c'est celui qu'il vous faut, puisque vous ajoutez TLS à la fin.
sudo ufw allow OpenSSH
sudo ufw allow "Apache Full"
sudo ufw enableAutorisez OpenSSH avant de lancer ufw enable. Par défaut, ufw refuse tout le trafic entrant, et l'activer sans règle SSH coupe votre propre connexion dès qu'il s'active : vous gardez la session en cours mais ne pourrez jamais vous reconnecter. Confirmez avec sudo ufw status ; vous voulez que OpenSSH, Apache Full et leurs équivalents v6 indiquent tous ALLOW.
Étape 3 - Installer MariaDB et le sécuriser
sudo apt install -y mariadb-server
systemctl status mariadbUbuntu 24.04 fournit MariaDB 10.11, une version à support long terme (LTS), vous n'avez donc pas besoin d'un dépôt externe. Une fois le service lancé, renforcez-le :
sudo mysql_secure_installationCes invites méritent d'être lues plutôt que de marteler Entrée. Quand elle demande le mot de passe root actuel, appuyez sur Entrée : il n'y en a pas encore. Quand elle demande "Switch to unix_socket authentication?", la réponse ne change rien car c'est déjà activé sur ce paquet, alors tapez n. Répondez n à "Change the root password?" pour la raison expliquée au paragraphe suivant, puis répondez Y au reste : supprimer les utilisateurs anonymes, interdire la connexion root à distance, supprimer la base de test et recharger les tables de privilèges.
Voici la partie qui déroute tout le monde. Sur MariaDB d'Ubuntu, le compte de base de données root utilise l'authentification unix_socket, pas un mot de passe. Cela signifie que la base fait confiance à l'utilisateur du système d'exploitation avec lequel vous êtes déjà authentifié. Ainsi, ceci fonctionne depuis un shell root :
sudo mysql...et vous arrivez à une invite MariaDB [(none)]> sans qu'aucun mot de passe soit demandé. La même commande lancée par un utilisateur non privilégié est refusée, ce qui est tout l'intérêt : l'accès au root de la base est lié à sudo sur la machine, et il n'y a aucun mot de passe à voler, hameçonner ou casser par force brute. C'est plus sûr qu'un mot de passe, alors n'y touchez pas. La règle qui en découle : ne pointez jamais une application vers le compte root. Créez un utilisateur dédié par application (Étape 7), car une application qui se connecte en TCP avec un nom d'utilisateur et un mot de passe ne peut de toute façon pas utiliser l'authentification par socket, et vous voulez limiter chaque application à sa propre base de données.
Étape 4 - Installer PHP 8.3 avec PHP-FPM
Le PHP par défaut d'Ubuntu 24.04 est le 8.3. Installez le gestionnaire de processus FPM et les extensions dont une application typique a besoin :
sudo apt install -y php8.3-fpm php8.3-mysql php8.3-cli \
php8.3-curl php8.3-xml php8.3-mbstring php8.3-zipRemarquez ce qui n'est pas dans cette liste : libapache2-mod-php. Ce paquet plus ancien intègre un interpréteur PHP dans chaque processus Apache. C'est simple, mais chaque worker embarque une copie de PHP qu'il serve un script ou une image statique, les deux partagent le même cycle de vie, et cela ne fonctionne qu'avec le MPM prefork d'Apache, le moins efficace. PHP-FPM exécute plutôt PHP comme son propre pool de processus auquel Apache parle via un socket. Apache peut alors utiliser le MPM event à threads pour les fichiers statiques et ne transmettre que les requêtes PHP, le pool est réglé indépendamment du serveur web, et exactement la même configuration FPM fonctionnera plus tard si vous placez nginx devant à la place. C'est le choix par défaut actuel, et pour de bonnes raisons.
Apache atteint FPM via le module proxy_fcgi. Activez-le, activez la configuration déposée par le paquet FPM, puis redémarrez :
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
sudo systemctl restart apache2a2enconf php8.3-fpm active /etc/apache2/conf-available/php8.3-fpm.conf, qui contient la règle acheminant les fichiers PHP vers le socket FPM. Son cœur correspond à tout fichier .php et le transmet au socket /run/php/php8.3-fpm.sock :
<FilesMatch ".+\.ph(ar|p|tml)$">
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>Vous n'éditez pas ce fichier ; il est livré correct. Mais connaître le chemin du socket est ce qui vous permet de diagnostiquer plus tard les pannes "PHP se télécharge au lieu de s'exécuter" et "Primary script unknown" : les deux se résument à un désaccord entre Apache et FPM sur ce socket ou sur le fichier qui se trouve derrière.
Étape 5 - Un hôte virtuel par nom pour votre application
L'hébergement virtuel par nom permet à une seule IP de servir plusieurs sites ; Apache choisit le site d'après l'en-tête Host: de la requête. Créez un répertoire pour l'application, bien à l'écart du /var/www/html par défaut :
sudo mkdir -p /var/www/testapp
sudo chown -R www-data:www-data /var/www/testapp
sudo chmod -R 755 /var/www/testappLe propriétaire des fichiers compte. Apache et PHP-FPM tournent tous deux sous l'utilisateur www-data sur Ubuntu, donc les fichiers que le serveur web doit lire, et les répertoires dans lesquels une application doit écrire, comme un dossier d'envois, doivent appartenir à www-data. Si vous comptez aussi éditer les fichiers avec votre utilisateur de connexion, un schéma courant consiste à être vous-même propriétaire des fichiers et à ajouter votre utilisateur au groupe www-data ; pour un déploiement simple, www-data:www-data est le moins surprenant.
Créez l'hôte virtuel dans /etc/apache2/sites-available/testapp.conf :
<VirtualHost *:80>
ServerName app.example.com
DocumentRoot /var/www/testapp
<Directory /var/www/testapp>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/testapp-error.log
CustomLog ${APACHE_LOG_DIR}/testapp-access.log combined
</VirtualHost>Réglez ServerName sur votre vrai domaine. Options -Indexes empêche Apache de lister le répertoire lorsqu'il n'y a pas de fichier d'index, sinon les visiteurs parcourent votre arborescence de code source. AllowOverride All permet à un fichier .htaccess de fonctionner, ce que la plupart des applications PHP attendent pour des URL propres ; passez-le à None pour un léger gain de vitesse si votre application n'en a pas besoin. Activez ce site, désactivez le site par défaut, vérifiez la configuration et rechargez :
sudo a2ensite testapp
sudo a2dissite 000-default
sudo apache2ctl configtest
sudo systemctl reload apache2apache2ctl configtest devrait afficher Syntax OK. La ligne a2dissite 000-default est celle qu'on oublie, et c'est pourquoi la page par défaut semble ensuite bloquée : voir la section des pannes.
Étape 6 - Prouver que PHP s'exécute, puis supprimer la preuve
Déposez un fichier PHP d'une ligne à la racine de l'application :
echo "<?php phpinfo();" | sudo tee /var/www/testapp/info.phpVisitez http://app.example.com/info.php. Un résultat correct est le long tableau violet et gris PHP Version 8.3.x listant vos modules chargés, avec la ligne Server API indiquant FPM/FastCGI. Cette dernière ligne confirme que les requêtes passent par PHP-FPM, et non par mod_php.
Supprimez-le maintenant, immédiatement :
sudo rm /var/www/testapp/info.phpphpinfo() expose votre version exacte de PHP, chaque extension chargée, les chemins de fichiers et des détails d'environnement : un cadeau pour quiconque sonde le serveur à la recherche d'une version comportant une faille connue. C'est un test, pas une fonctionnalité. Supprimez-le dès que vous avez vu la page. Si, au lieu du tableau, votre navigateur a proposé de télécharger info.php, c'est que PHP n'est pas relié à Apache ; passez à la section des pannes avant de faire quoi que ce soit d'autre.
Étape 7 - Créer la base de données de l'application et un utilisateur à privilèges minimaux
Ouvrez la base en tant que root authentifié par socket :
sudo mysqlCréez ensuite une base de données et un utilisateur limité exactement à cette base :
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;Trois choix délibérés ici. utf8mb4 est du vrai UTF-8 sur quatre octets : l'ancien alias utf8 tronque silencieusement les emoji et certains caractères CJC (chinois, japonais, coréen), utilisez donc toujours utf8mb4. Le privilège porte sur appdb.*, et non *.* : cet utilisateur peut agir sur sa propre base et rien d'autre, si bien qu'une faille d'injection SQL dans l'application ne peut pas lire les tables de tous les autres sites. Et 'appuser'@'localhost' restreint le compte aux connexions provenant de la machine elle-même.
Testez-le avec cet utilisateur :
mysql -u appuser -p appdbElle demande le mot de passe et vous arrivez à une invite MariaDB [appdb]>. Notez l'absence de l'option -h : laissez-la de côté et le client se connecte via le socket Unix local, ce que MariaDB considère précisément comme localhost. Un piège à connaître : pour MySQL et MariaDB, localhost signifie le socket Unix et 127.0.0.1 signifie une connexion TCP. Sur une MariaDB Ubuntu 24.04 standard, le serveur résout encore une connexion TCP depuis 127.0.0.1 en localhost, si bien que les deux correspondent au compte ; mais sur les serveurs où skip-name-resolve est activé (un réglage de performance courant, et la norme dans de nombreuses images de conteneurs), les deux sont traités comme des hôtes différents, et une application qui compose 127.0.0.1 est refusée avec ERROR 1045 (28000): Access denied for user 'appuser'@'127.0.0.1' (using password: YES) même quand le mot de passe est correct.
Pointez donc votre application vers l'hôte localhost, l'utilisateur appuser, la base appdb : jamais vers root. Le mysqli et le PDO de PHP basculent tous deux sur le socket Unix quand l'hôte est la chaîne littérale localhost, ce qui correspond au compte que vous venez de créer. Si un framework insiste sur un hôte TCP numérique, créez l'utilisateur pour correspondre à sa manière réelle de se connecter : 'appuser'@'127.0.0.1', ou @'%' (associé à une règle de pare-feu) uniquement s'il doit joindre la base depuis une autre machine.
Étape 8 - Ajouter HTTPS avec Certbot
Servir un formulaire de connexion en HTTP simple envoie les mots de passe en clair, et tous les navigateurs modernes signalent la page comme "Not secure". Certbot corrige cela en une seule commande. Installez-le avec le plugin Apache :
sudo apt install -y certbot python3-certbot-apache
sudo certbot --apacheCertbot utilise ici deux plugins. L'authenticator apache prouve que vous contrôlez le domaine en servant brièvement un fichier de défi via votre Apache en cours d'exécution, et l'installer apache réécrit ensuite votre hôte virtuel pour ajouter le bloc 443, le pointe vers le nouveau certificat et redirige par défaut tout le trafic HTTP vers HTTPS : depuis Certbot 2.0, il n'y a plus de question sur la redirection ; passez --no-redirect s'il vous faut continuer à servir en HTTP simple. Comme vous avez défini un vrai ServerName à l'Étape 5, Certbot détecte le domaine automatiquement. Les certificats durent 90 jours et le paquet installe un timer systemd qui les renouvelle ; vérifiez le timer avec sudo certbot renew --dry-run, qui devrait se terminer par Congratulations, all simulated renewals succeeded.
Pour le déroulé complet du défi, du timer de renouvellement et des exigences DNS et pare-feu, consultez le guide compagnon sur l'émission de certificats TLS Let's Encrypt gratuits avec Certbot sur Apache.
Sauvegardes, mises à jour et durcissement
Sauvegardez les deux choses qui détiennent votre état : les bases de données et la racine web. Un dump logique nocturne est l'approche fiable la plus simple : sudo sh -c 'mysqldump --all-databases --single-transaction | gzip > /root/db-$(date +%F).sql.gz', puis copié hors de la machine. Envelopper tout le pipeline dans sudo sh -c a son importance : sans cela, le shell exécute la redirection > /root/... sous votre propre utilisateur et échoue avec Permission denied, car seul mysqldump a hérité du sudo. --single-transaction donne un instantané cohérent des tables InnoDB sans les verrouiller. Associez-le à un tar de /var/www et /etc/apache2/sites-available, et vous pourrez reconstruire toute la stack sur un VPS neuf à partir de ces fichiers.
Les mises à jour sont un simple sudo apt update && sudo apt upgrade. Celle qui pose problème est un changement de version de PHP : quand un futur Ubuntu fera passer le défaut à PHP 8.4, apt peut installer php8.4-fpm à côté de la 8.3, le socket devient /run/php/php8.4-fpm.sock, et votre configuration Apache pointe toujours vers le socket 8.3. Activez la nouvelle conf (sudo a2enconf php8.4-fpm) et désactivez l'ancienne, sinon votre site se met à renvoyer Primary script unknown après une mise à jour pourtant routinière. Comme les versions de PHP évoluent plus vite qu'une distribution LTS, consultez les notes de version PHP actuelles plutôt que de figer une version corrective.
Deux étapes de durcissement valent la peine dès le premier jour. D'abord, mettez Fail2ban à surveiller SSH sur la machine : un VPS public reçoit des tentatives de connexion automatisées en quelques minutes, et une petite prison (jail) transforme des milliers d'essais en une poignée avant un bannissement. Ensuite, si vous préférez gérer les hôtes virtuels Apache, les bases MariaDB et les utilisateurs via un navigateur plutôt qu'en éditant des fichiers à la main, le panneau de contrôle web Webmin se pose sur cette stack exacte et pilote les mêmes fichiers de configuration que vous venez d'écrire. Aucun ne remplace la compréhension des pièces, mais les deux réduisent les frictions quotidiennes.
Modes de défaillance, avec les messages que vous verrez
La page par défaut ne disparaît pas. Vous avez édité votre hôte virtuel, rechargé, et le navigateur affiche toujours la "Apache2 Ubuntu Default Page" et sa bannière "It works!". Apache sert le premier hôte virtuel correspondant, et quand aucun ServerName ne correspond à la requête, la première configuration par ordre alphabétique l'emporte : 000-default.conf se classe avant testapp.conf. Soit le nom d'hôte de la requête ne correspond pas à votre ServerName, soit vous n'avez jamais lancé sudo a2dissite 000-default. Désactivez le site par défaut, sudo systemctl reload apache2, et confirmez avec apache2ctl -S, qui affiche la carte des vhosts et montre quelle configuration détient le site par défaut. Videz aussi le cache du navigateur ; un 200 mis en cache depuis l'ancienne page persiste allègrement.
Un fichier .php se télécharge au lieu de s'exécuter. Vous ouvrez info.php et le navigateur télécharge un fichier contenant le code source <?php brut, ou l'affiche en texte clair, au lieu de l'exécuter. Apache sert le fichier comme un actif statique parce que le gestionnaire PHP n'est pas attaché : vous avez sauté sudo a2enmod proxy_fcgi, ou sudo a2enconf php8.3-fpm, ou vous n'avez pas redémarré Apache ensuite. Lancez les trois (Étape 4) et rechargez. Confirmez que le module est chargé avec apache2ctl -M | grep fcgi, qui devrait lister proxy_fcgi_module. C'est une fuite de code source, pas un défaut cosmétique, alors corrigez-le avant de mettre quoi que ce soit de réel sur le serveur.
ERROR 1698 (28000): Access denied for user 'root'@'localhost'. Vous avez lancé mysql -u root ou mariadb -u root sans sudo. Le compte root utilise l'authentification unix_socket, il ne vous accepte donc que lorsque votre utilisateur système est réellement root. La solution est sudo mysql : sans -u root, sans mot de passe. Ce message est le comportement attendu de l'authentification par socket qui fonctionne correctement, pas une installation cassée.
ERROR 1045 (28000): Access denied for user 'appuser'@'127.0.0.1' depuis l'application, avec le bon mot de passe. Le compte existe en tant que 'appuser'@'localhost', mais votre application se connecte en TCP vers 127.0.0.1 sur un serveur où la résolution des noms d'hôtes est désactivée (skip-name-resolve), si bien que MariaDB traite les deux comme des hôtes différents : localhost est le socket Unix, 127.0.0.1 est du TCP. Pointez l'application vers l'hôte localhost pour qu'elle utilise le socket et corresponde au compte, ou créez un second compte 'appuser'@'127.0.0.1' si le framework ne parle que TCP.
AH01071: Got error 'Primary script unknown' dans /var/log/apache2/testapp-error.log, avec le navigateur affichant File not found.. Apache a transmis la requête à PHP-FPM, mais FPM n'a pas trouvé le script au chemin indiqué par Apache. Deux causes habituelles : le socket FPM de votre configuration pointe vers une version de PHP qui n'est pas installée (un socket php8.4 après une mise à jour alors que seule la 8.3 tourne), ou le fichier n'est réellement pas là parce que DocumentRoot et le vrai répertoire ne concordent pas. Vérifiez que le socket existe avec ls -l /run/php/, confirmez que DocumentRoot correspond à l'emplacement du fichier, et redémarrez à la fois php8.3-fpm et apache2.
AH00558: apache2: Could not reliably determine the server's fully qualified domain name à chaque redémarrage. C'est un avertissement inoffensif, pas une erreur : Apache vous indique qu'aucun ServerName global n'est défini. Faites-le taire en écrivant ServerName your.domain dans /etc/apache2/conf-available/servername.conf et en lançant sudo a2enconf servername.
(98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:80 au démarrage d'Apache. Un autre serveur web occupe déjà le port 80 : souvent un nginx égaré d'une expérience précédente. Trouvez-le avec sudo ss -ltnp | grep :80, puis arrêtez et désactivez l'autre service avant de démarrer Apache.
FAQ
mod_php ou PHP-FPM : lequel choisir ?
Utilisez PHP-FPM. mod_php intègre un interpréteur dans chaque processus Apache et impose le MPM prefork lent, si bien qu'Apache porte la surcharge de PHP même lorsqu'il sert une image statique. PHP-FPM exécute PHP comme un pool séparé et réglé indépendamment qu'Apache atteint via un socket, fonctionne avec le MPM event à threads plus rapide, et migre sans changement vers nginx plus tard. C'est le choix par défaut moderne ; mod_php n'a de sens que pour une application ancienne qui dépend d'un comportement en cours de processus.
Pourquoi mon navigateur télécharge-t-il le fichier PHP au lieu de l'exécuter ?
Apache traite le fichier .php comme un téléchargement statique parce qu'aucun gestionnaire PHP ne lui est attaché. Sur Ubuntu 24.04 avec FPM, cela signifie que vous avez manqué l'un des sudo a2enmod proxy_fcgi, sudo a2enconf php8.3-fpm, ou le redémarrage d'Apache ensuite. Lancez les trois et rechargez, puis vérifiez avec apache2ctl -M | grep fcgi que proxy_fcgi_module est bien listé. Tant que vous ne l'avez pas corrigé, le serveur fuit du code source, alors traitez cela comme urgent.
Pourquoi l'accès root est-il refusé dans MariaDB même avec le bon mot de passe ?
Parce qu'il n'y a pas de mot de passe : MariaDB d'Ubuntu authentifie le compte root par unix_socket, le liant à l'utilisateur root du système d'exploitation. mysql -u root depuis un shell normal renvoie ERROR 1698 (28000): Access denied for user 'root'@'localhost' par conception. Connectez-vous plutôt avec sudo mysql, et créez un utilisateur distinct authentifié par mot de passe pour toute application plutôt que de réutiliser root.
Comment ajouter HTTPS à mon site LAMP ?
Installez certbot et python3-certbot-apache, pointez l'enregistrement A d'un domaine vers le serveur, puis lancez sudo certbot --apache. L'authenticator apache prouve le contrôle du domaine via votre Apache en cours d'exécution et l'installer réécrit l'hôte virtuel pour le port 443 et met en place le renouvellement automatique. Le déroulé complet Certbot et Apache couvre le défi, le timer de renouvellement et les modes de défaillance courants.