Linux29 agosto 2026 6 visualizzazioni

Ospitare più siti su un VPS con Nginx (2026)

Ospitare più siti su un VPS con Nginx (2026)

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.