Linux•29 agosto 2026• 99 visualizzazioni

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

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

Questa guida vale per qualsiasi server Debian o Ubuntu con accesso root, per esempio un VPS Linux OuiHeberg.

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.