Cosa implica davvero "più siti su un VPS"
Ospitare più siti su un VPS non è solo una questione di configurazione Nginx. Tre punti da anticipare prima di iniziare:
- RAM: ogni sito attivo consuma memoria (worker PHP-FPM, database, cache). Un VPS con 4 GB di RAM può ospitare più siti a basso traffico, ma non decine di siti WordPress con traffico simultaneo. Dimensionate in base al traffico reale, non al numero di siti.
- Isolamento: su una stessa istanza PHP-FPM senza configurazione specifica, i siti non sono realmente isolati. Un sito compromesso può leggere i file degli altri. La soluzione è usare pool PHP-FPM separati con utenti di sistema distinti (vedere la sezione dedicata qui sotto).
- Backup: con più siti, i backup manuali diventano rapidamente ingestibili. Automatizzate fin dall'inizio con un ciclo sulle directory e sui database.
Struttura delle directory raccomandata
Utilizzate una directory per sito sotto /var/www/, con il nome di dominio come identificativo. Questa convenzione rende gli script di backup e le configurazioni Nginx leggibili e prevedibili.
/var/www/
site-a.fr/
site-b.fr/
site-c.fr/ Creare le directory:
sudo mkdir -p /var/www/site-a.fr
sudo mkdir -p /var/www/site-b.fr
sudo mkdir -p /var/www/site-c.fr I permessi verranno regolati durante la creazione dei pool PHP-FPM (sezione successiva).
Un vhost Nginx per sito
Creare un file di configurazione per sito
Ogni sito ha il proprio file in /etc/nginx/sites-available/. Non mettete tutto in un unico file: un file per sito facilita la disattivazione, il debug e la lettura.
Esempio per 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;
}
} Ripetete per ogni sito sostituendo site-a.fr e il socket PHP-FPM (site-a.sock) con i valori corrispondenti.
Nota: il socket /run/php/site-a.sock non esiste ancora in questa fase, viene creato nella sezione dei pool PHP-FPM più avanti. nginx -t non verifica l'esistenza del socket, quindi la configurazione verrà validata, ma il sito restituirà un errore 502 finché il pool corrispondente non è in funzione.
Attivare i siti
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/ Testate sempre la configurazione prima di ricaricare:
sudo nginx -t
sudo systemctl reload nginx Fonte: nginx.org - direttiva server_name
La trappola del primo vhost servito e il vhost catch-all
Cosa succede senza un vhost predefinito esplicito
Quando Nginx riceve una richiesta per un dominio o un IP non dichiarato in nessun server_name, serve il primo vhost caricato in ordine alfabetico. Questo comportamento è documentato e prevedibile, ma può esporre un sito involontariamente: una richiesta sull'IP grezzo del VPS, o su un dominio che punta al VPS per errore, finisce sul primo sito configurato.
Fonte: nginx.org - How nginx processes a request
Il vhost catch-all con default_server
Create un vhost predefinito che intercetta tutte le richieste non riconosciute e restituisce una risposta vuota (codice 444, che chiude la connessione senza risposta 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 _;
# Certificato autofirmato per assorbire le richieste HTTPS non riconosciute
ssl_certificate /etc/nginx/ssl/self-signed.crt;
ssl_certificate_key /etc/nginx/ssl/self-signed.key;
return 444;
} Generate un certificato autofirmato per il blocco SSL del catch-all (Certbot non può emettere un certificato per _):
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" Attivate il catch-all e ricaricate:
sudo ln -s /etc/nginx/sites-available/default-catchall /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx Il codice 444 chiude la connessione TCP senza inviare una risposta. Gli scanner automatici e le richieste sull'IP grezzo non ottengono alcuna informazione sui siti ospitati.
Pool PHP-FPM separati per sito
Perché pool separati?
Senza pool separati, tutti i siti vengono eseguiti con lo stesso utente di sistema (spesso www-data). Un sito compromesso può leggere i file di configurazione degli altri (file .env, wp-config.php, chiavi API). I pool separati offrono:
- Isolamento dei permessi: ogni sito viene eseguito con il proprio utente
- Limiti di processo per sito: un sito che riceve un picco di traffico non consuma tutti i worker PHP disponibili
- Log separati: individuare rapidamente quale sito genera errori
Fonte: php.net - Configurazione PHP-FPM
Creare un utente di sistema per sito
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 Regolate i permessi delle directory:
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 (che gira con l'utente www-data) deve poter leggere i file. Aggiungete www-data ai gruppi degli utenti dei siti, oppure regolate i permessi delle directory:
sudo chmod 750 /var/www/site-a.fr
sudo usermod -aG site-a www-data Da sapere: aggiungere www-data al gruppo di ogni sito permette a Nginx di servire i file statici, ma significa anche che il processo Nginx può leggere i file di tutti i siti. L'isolamento offerto dai pool riguarda l'esecuzione PHP, non la lettura da parte del server web. Per un isolamento più stretto servono container o macchine virtuali distinte.
Creare un pool PHP-FPM per sito
Copiate il pool predefinito come 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 Modificate queste direttive nel file (sostituendo [www] con [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 Ripetete per ogni sito (site-b.conf, site-c.conf) adattando il nome del pool, l'utente e il socket.
Disattivate il pool predefinito se tutti i siti hanno il proprio pool:
sudo mv /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/www.conf.disabled Verificate prima che nessun vhost faccia ancora riferimento al socket predefinito /run/php/php8.4-fpm.sock: quei siti restituirebbero un errore 502 dopo la disattivazione del pool.
Riavviate PHP-FPM:
sudo systemctl restart php8.4-fpm Il socket di ogni sito (/run/php/site-a.sock) corrisponde alla direttiva fastcgi_pass nel vhost Nginx corrispondente.
Database separati per sito
Un database e un utente MariaDB dedicati per ogni sito. Non usate mai un utente condiviso tra più siti: la compromissione di un sito darebbe accesso a tutti i database.
sudo mariadb -u root -- Sito A
CREATE DATABASE site_a_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site_a_user'@'localhost' IDENTIFIED BY 'password_forte_a';
GRANT ALL PRIVILEGES ON site_a_db.* TO 'site_a_user'@'localhost';
-- Sito B
CREATE DATABASE site_b_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site_b_user'@'localhost' IDENTIFIED BY 'password_forte_b';
GRANT ALL PRIVILEGES ON site_b_db.* TO 'site_b_user'@'localhost';
FLUSH PRIVILEGES;
EXIT; Certificati SSL multi-dominio
Certbot gestisce i certificati di ogni dominio in modo indipendente. Emettete un certificato per dominio (o per coppia dominio/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 modifica automaticamente i vhost Nginx per aggiungere le direttive SSL e il reindirizzamento da HTTP a HTTPS.
Il rinnovo automatico è gestito da un timer systemd installato da Certbot. Verificate che sia attivo:
sudo systemctl status certbot.timer Guida completa (rinnovo, certificati wildcard, risoluzione degli errori): Guida Certbot OuiHeberg.
Dimensionamento: quanti siti su quanta RAM?
Non esiste una risposta universale: il numero di siti ospitabili dipende dal traffico di ciascun sito, dal CMS utilizzato e dai plugin attivi. Alcuni ordini di grandezza ragionevoli:
- Un sito WordPress con OPcache attivo e poco traffico simultaneo consuma tra 128 MB e 256 MB di RAM in carico normale.
- Un sito statico o un'applicazione leggera consuma nettamente meno.
- Un sito WooCommerce con traffico o plugin pesanti può consumare 512 MB o più.
Su un VPS con 4 GB di RAM è ragionevole ospitare più siti a basso traffico. Monitorate il consumo reale con free -h e top dopo la messa in produzione, e regolate di conseguenza i valori pm.max_children dei pool PHP-FPM.
Non saturate la RAM disponibile: lasciate un margine per il sistema operativo, MariaDB e i picchi di carico imprevisti.
Backup multi-sito
Con più siti, automatizzate i backup fin dall'inizio. Un ciclo sulle directory e sui database copre tutto in un'unica attività cron.
Script di backup
#!/bin/bash
BACKUP_DIR="/root/backups"
DATE=$(date +%F)
mkdir -p "$BACKUP_DIR"
# Backup dei file di ogni sito
for SITE in /var/www/*/; do
SITE_NAME=$(basename "$SITE")
tar -czf "$BACKUP_DIR/${SITE_NAME}-files-${DATE}.tar.gz" "$SITE"
done
# Backup dei database
for DB in site_a_db site_b_db site_c_db; do
mysqldump -u root "$DB" > "$BACKUP_DIR/${DB}-${DATE}.sql"
done
# Eliminare i backup più vecchi di 7 giorni
find "$BACKUP_DIR" -type f -mtime +7 -delete Salvate questo script in /usr/local/bin/backup-sites.sh, rendetelo eseguibile e aggiungetelo al crontab:
sudo chmod +x /usr/local/bin/backup-sites.sh
sudo crontab -e # Backup quotidiano alle 3 del mattino
0 3 * * * /usr/local/bin/backup-sites.sh Esternalizzare i backup
I backup archiviati sullo stesso VPS non proteggono da un guasto hardware o da una compromissione del server. Trasferite regolarmente gli archivi verso una destinazione remota (storage a oggetti, server terzo, macchina locale) tramite rsync o scp.
Domande frequenti
Si possono ospitare siti con versioni di PHP diverse sullo stesso VPS?
Sì. Installate più versioni di PHP-FPM in parallelo (per esempio php8.1-fpm e php8.4-fpm), poi assegnate a ogni pool la versione corrispondente. Il socket di ogni pool punta alla versione PHP desiderata e il vhost Nginx di ogni sito fa riferimento al socket giusto.
sudo apt install php8.1-fpm php8.4-fpm Ogni versione ha la propria directory di pool: /etc/php/8.1/fpm/pool.d/ e /etc/php/8.4/fpm/pool.d/.
Un sito che va in crash può influenzare gli altri?
Con pool PHP-FPM separati e limiti pm.max_children per pool, un sito che riceve un picco di traffico o che genera errori PHP non consuma i worker degli altri siti. Se invece MariaDB è saturo (troppe connessioni simultanee), tutti i siti che ne dipendono possono risentirne. Monitorate le connessioni MariaDB con SHOW PROCESSLIST;.
Come disattivare temporaneamente un sito senza eliminarlo?
Rimuovete il link simbolico in sites-enabled e ricaricate Nginx:
sudo rm /etc/nginx/sites-enabled/site-a.fr
sudo nginx -t && sudo systemctl reload nginx Il file di configurazione in sites-available viene conservato. Per riattivarlo basta ricreare il link simbolico.
Come aggiungere un nuovo sito dopo la configurazione iniziale?
Create la directory, l'utente di sistema, il pool PHP-FPM, il database, il vhost Nginx e il certificato SSL seguendo le sezioni di questa guida nell'ordine. Ogni aggiunta è indipendente e non influisce sui siti esistenti.
I pool PHP-FPM separati sono obbligatori?
No, ma sono fortemente raccomandati non appena più siti coesistono sullo stesso VPS. Senza pool separati, tutti i siti vengono eseguiti con www-data e possono accedere ai file degli altri. Per un VPS personale con progetti di fiducia può bastare un pool unico. Per siti di clienti o progetti di terzi, i pool separati sono indispensabili.
