Ce que "plusieurs sites sur un VPS" implique
Héberger plusieurs sites sur un VPS n'est pas juste une question de configuration Nginx. Trois points à anticiper avant de commencer :
- RAM : chaque site actif consomme de la mémoire (PHP-FPM workers, base de données, cache). Un VPS avec 4 Go de RAM peut héberger plusieurs sites à faible trafic, mais pas des dizaines de sites WordPress avec du trafic simultané. Dimensionner en fonction du trafic réel, pas du nombre de sites.
- Isolation : sur une même instance PHP-FPM sans configuration spécifique, les sites ne sont pas réellement isolés. Un site compromis peut lire les fichiers des autres. La solution est d'utiliser des pools PHP-FPM séparés avec des utilisateurs système distincts (voir section dédiée ci-dessous).
- Sauvegardes : avec plusieurs sites, les sauvegardes manuelles deviennent vite ingérables. Automatiser dès le départ avec une boucle sur les répertoires et les bases de données.
Arborescence recommandée
Utiliser un répertoire par site sous /var/www/, avec le nom de domaine comme identifiant. Cette convention rend les scripts de sauvegarde et les configurations Nginx lisibles et prévisibles.
/var/www/
site-a.fr/
site-b.fr/
site-c.fr/ Créer les répertoires :
sudo mkdir -p /var/www/site-a.fr
sudo mkdir -p /var/www/site-b.fr
sudo mkdir -p /var/www/site-c.fr Les permissions seront ajustées lors de la création des pools PHP-FPM (section suivante).
Un vhost Nginx par site
Créer un fichier de configuration par site
Chaque site a son propre fichier dans /etc/nginx/sites-available/. Ne pas tout mettre dans un seul fichier : un fichier par site facilite la désactivation, le débogage et la lecture.
Exemple pour site-a.fr :
sudo nano /etc/nginx/sites-available/site-a.fr server {
listen 80;
listen [::]:80;
server_name site-a.fr www.site-a.fr;
root /var/www/site-a.fr;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/site-a.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(ht|git|env) {
deny all;
}
} Répéter pour chaque site en remplaçant site-a.fr et le socket PHP-FPM (site-a.sock) par les valeurs correspondantes.
Note : le socket /run/php/site-a.sock n'existe pas encore à ce stade, il est créé à l'étape des pools PHP-FPM plus bas. nginx -t ne vérifie pas l'existence du socket, la configuration sera donc validée, mais le site renverra une erreur 502 tant que le pool correspondant n'est pas en place.
Activer les sites
sudo ln -s /etc/nginx/sites-available/site-a.fr /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site-b.fr /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site-c.fr /etc/nginx/sites-enabled/ Toujours tester la configuration avant de recharger :
sudo nginx -t
sudo systemctl reload nginx Source : nginx.org - directive server_name
Le piège du premier vhost servi et le vhost catch-all
Ce qui se passe sans vhost par défaut explicite
Quand Nginx reçoit une requête pour un domaine ou une IP non déclarée dans aucun server_name, il sert le premier vhost chargé par ordre alphabétique. Ce comportement est documenté et prévisible, mais il peut exposer un site involontairement : une requête sur l'IP brute du VPS, ou sur un domaine pointant vers le VPS par erreur, atterrit sur le premier site configuré.
Source : nginx.org - How nginx processes a request
Le vhost catch-all avec default_server
Créer un vhost par défaut qui intercepte toutes les requêtes non reconnues et retourne une réponse vide (code 444, qui ferme la connexion sans réponse HTTP) :
sudo nano /etc/nginx/sites-available/default-catchall server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
# Certificat auto-signé pour absorber les requêtes HTTPS non reconnues
ssl_certificate /etc/nginx/ssl/self-signed.crt;
ssl_certificate_key /etc/nginx/ssl/self-signed.key;
return 444;
} Générer un certificat auto-signé pour le bloc SSL du catch-all (Certbot ne peut pas émettre un certificat pour _) :
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/self-signed.key \
-out /etc/nginx/ssl/self-signed.crt \
-subj "/CN=localhost" Activer le catch-all et recharger :
sudo ln -s /etc/nginx/sites-available/default-catchall /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx Le code 444 ferme la connexion TCP sans envoyer de réponse. Les scanners automatiques et les requêtes sur l'IP brute n'obtiennent aucune information sur les sites hébergés.
Pools PHP-FPM séparés par site
Pourquoi des pools séparés ?
Sans pools séparés, tous les sites s'exécutent sous le même utilisateur système (souvent www-data). Un site compromis peut lire les fichiers de configuration des autres (fichiers .env, wp-config.php, clés d'API). Les pools séparés apportent :
- Isolation des permissions : chaque site s'exécute sous son propre utilisateur
- Limites de processus par site : un site qui reçoit un pic de trafic ne consomme pas tous les workers PHP disponibles
- Logs séparés : identifier rapidement quel site génère des erreurs
Source : php.net - Configuration PHP-FPM
Créer un utilisateur système par site
sudo useradd -r -s /usr/sbin/nologin site-a
sudo useradd -r -s /usr/sbin/nologin site-b
sudo useradd -r -s /usr/sbin/nologin site-c Ajuster les permissions des répertoires :
sudo chown -R site-a:site-a /var/www/site-a.fr
sudo chown -R site-b:site-b /var/www/site-b.fr
sudo chown -R site-c:site-c /var/www/site-c.fr Nginx (qui tourne sous www-data) doit pouvoir lire les fichiers. Ajouter www-data aux groupes des utilisateurs de site, ou ajuster les permissions des répertoires :
sudo chmod 750 /var/www/site-a.fr
sudo usermod -aG site-a www-data À savoir : ajouter www-data au groupe de chaque site permet à Nginx de servir les fichiers statiques, mais cela signifie aussi que le processus Nginx peut lire les fichiers de tous les sites. L'isolation apportée par les pools porte sur l'exécution PHP, pas sur la lecture par le serveur web. Pour une isolation plus stricte, il faut passer par des conteneurs ou des machines virtuelles distinctes.
Créer un pool PHP-FPM par site
Copier le pool par défaut comme base :
sudo cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/site-a.conf
sudo nano /etc/php/8.4/fpm/pool.d/site-a.conf Modifier ces directives dans le fichier (remplacer [www] par [site-a]) :
[site-a]
user = site-a
group = site-a
listen = /run/php/site-a.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3 Répéter pour chaque site (site-b.conf, site-c.conf) en adaptant le nom du pool, l'utilisateur et le socket.
Désactiver le pool par défaut si tous les sites ont leur propre pool :
sudo mv /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/www.conf.disabled Vérifier auparavant qu'aucun vhost ne référence encore le socket par défaut /run/php/php8.4-fpm.sock : ces sites renverraient une erreur 502 après la désactivation du pool.
Redémarrer PHP-FPM :
sudo systemctl restart php8.4-fpm Le socket de chaque site (/run/php/site-a.sock) correspond à la directive fastcgi_pass dans le vhost Nginx correspondant.
Bases de données séparées par site
Une base de données et un utilisateur MariaDB dédiés par site. Ne jamais utiliser un utilisateur partagé entre plusieurs sites : une compromission d'un site donne accès à toutes les bases.
sudo mariadb -u root -- Site A
CREATE DATABASE site_a_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site_a_user'@'localhost' IDENTIFIED BY 'mot_de_passe_fort_a';
GRANT ALL PRIVILEGES ON site_a_db.* TO 'site_a_user'@'localhost';
-- Site B
CREATE DATABASE site_b_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site_b_user'@'localhost' IDENTIFIED BY 'mot_de_passe_fort_b';
GRANT ALL PRIVILEGES ON site_b_db.* TO 'site_b_user'@'localhost';
FLUSH PRIVILEGES;
EXIT; Certificats SSL multi-domaines
Certbot gère les certificats de chaque domaine indépendamment. Émettre un certificat par domaine (ou par paire domaine/www) :
sudo certbot --nginx -d site-a.fr -d www.site-a.fr
sudo certbot --nginx -d site-b.fr -d www.site-b.fr
sudo certbot --nginx -d site-c.fr -d www.site-c.fr Certbot modifie automatiquement les vhosts Nginx pour ajouter les directives SSL et la redirection HTTP vers HTTPS.
Le renouvellement automatique est géré par un timer systemd installé par Certbot. Vérifier qu'il est actif :
sudo systemctl status certbot.timer Guide complet (renouvellement, certificats wildcard, résolution d'erreurs) : Guide Certbot OuiHeberg.
Dimensionnement : combien de sites sur quelle RAM ?
Il n'existe pas de réponse universelle : le nombre de sites hébergeables dépend du trafic de chaque site, du CMS utilisé et des plugins actifs. Quelques ordres de grandeur raisonnables :
- Un site WordPress avec OPcache activé et peu de trafic simultané consomme entre 128 Mo et 256 Mo de RAM en charge normale.
- Un site statique ou une application légère consomme nettement moins.
- Un site WooCommerce avec du trafic ou des plugins lourds peut consommer 512 Mo ou plus.
Sur un VPS avec 4 Go de RAM, il est raisonnable d'héberger plusieurs sites à faible trafic. Surveiller la consommation réelle avec free -h et top après mise en production, et ajuster les valeurs pm.max_children des pools PHP-FPM en conséquence.
Ne pas saturer la RAM disponible : laisser une marge pour le système d'exploitation, MariaDB et les pics de charge imprévus.
Sauvegardes multi-sites
Avec plusieurs sites, automatiser les sauvegardes dès le départ. Une boucle sur les répertoires et les bases de données couvre l'ensemble en une seule tâche cron.
Script de sauvegarde
#!/bin/bash
BACKUP_DIR="/root/backups"
DATE=$(date +%F)
mkdir -p "$BACKUP_DIR"
# Sauvegarde des fichiers de chaque site
for SITE in /var/www/*/; do
SITE_NAME=$(basename "$SITE")
tar -czf "$BACKUP_DIR/${SITE_NAME}-files-${DATE}.tar.gz" "$SITE"
done
# Sauvegarde des bases de données
for DB in site_a_db site_b_db site_c_db; do
mysqldump -u root "$DB" > "$BACKUP_DIR/${DB}-${DATE}.sql"
done
# Supprimer les sauvegardes de plus de 7 jours
find "$BACKUP_DIR" -type f -mtime +7 -delete Enregistrer ce script dans /usr/local/bin/backup-sites.sh, le rendre exécutable et l'ajouter au crontab :
sudo chmod +x /usr/local/bin/backup-sites.sh
sudo crontab -e # Sauvegarde quotidienne à 3h du matin
0 3 * * * /usr/local/bin/backup-sites.sh Externaliser les sauvegardes
Les sauvegardes stockées sur le même VPS ne protègent pas contre une panne matérielle ou une compromission du serveur. Transférer régulièrement les archives vers un emplacement distant (stockage objet, serveur tiers, machine locale) via rsync ou scp.
Questions fréquentes
Peut-on héberger des sites avec des versions PHP différentes sur le même VPS ?
Oui. Installer plusieurs versions de PHP-FPM en parallèle (par exemple php8.1-fpm et php8.4-fpm), puis assigner à chaque pool la version correspondante. Le socket de chaque pool pointe vers la version PHP souhaitée, et le vhost Nginx de chaque site référence le bon socket.
sudo apt install php8.1-fpm php8.4-fpm Chaque version a son propre répertoire de pools : /etc/php/8.1/fpm/pool.d/ et /etc/php/8.4/fpm/pool.d/.
Un site qui plante peut-il affecter les autres ?
Avec des pools PHP-FPM séparés et des limites pm.max_children par pool, un site qui reçoit un pic de trafic ou qui génère des erreurs PHP ne consomme pas les workers des autres sites. En revanche, si MariaDB est saturé (trop de connexions simultanées), tous les sites qui en dépendent peuvent être affectés. Surveiller les connexions MariaDB avec SHOW PROCESSLIST;.
Comment désactiver temporairement un site sans le supprimer ?
Supprimer le lien symbolique dans sites-enabled et recharger Nginx :
sudo rm /etc/nginx/sites-enabled/site-a.fr
sudo nginx -t && sudo systemctl reload nginx Le fichier de configuration dans sites-available est conservé. Réactiver en recréant le lien symbolique.
Comment ajouter un nouveau site après la configuration initiale ?
Créer le répertoire, l'utilisateur système, le pool PHP-FPM, la base de données, le vhost Nginx et le certificat SSL en suivant les sections de ce guide dans l'ordre. Chaque ajout est indépendant et n'affecte pas les sites existants.
Les pools PHP-FPM séparés sont-ils obligatoires ?
Non, mais fortement recommandés dès que plusieurs sites coexistent sur le même VPS. Sans pools séparés, tous les sites s'exécutent sous www-data et peuvent accéder aux fichiers des autres. Pour un VPS personnel avec des projets de confiance, un pool unique peut suffire. Pour des sites clients ou des projets tiers, les pools séparés sont indispensables.
