Servire un sito in WordPress con nginx

nginx (pronunciato "engine-x") è un web server ad alte prestazioni, proxy inverso e bilanciatore di carico. A differenza di Apache, che adotta un modello basato su thread o processi per gestire le connessioni, nginx utilizza un'architettura event-driven e asincrona che lo rende particolarmente efficiente sotto carico elevato, con un consumo di memoria notevolmente inferiore.

WordPress, il sistema di gestione dei contenuti più diffuso al mondo, nasce con un supporto nativo per Apache e il suo file .htaccess. Tuttavia, con la giusta configurazione, nginx è in grado di servire WordPress in modo eccellente, spesso con prestazioni superiori. In questa guida vedremo come installare, configurare e ottimizzare nginx per un sito WordPress su un server Linux (Debian/Ubuntu).

Prerequisiti

Prima di procedere è necessario disporre di:

  • Un server con Debian 11/12 o Ubuntu 22.04/24.04
  • Accesso root o un utente con privilegi sudo
  • Un nome di dominio puntato sull'IP del server
  • PHP-FPM installato (versione 8.1 o superiore consigliata)
  • Un database MySQL o MariaDB

La scelta di PHP-FPM (FastCGI Process Manager) è fondamentale: nginx non può eseguire PHP nativamente come fa Apache con mod_php, quindi si affida a un processo esterno tramite il protocollo FastCGI. PHP-FPM è la soluzione standard e più performante per questo scopo.

Installazione di nginx

Su sistemi Debian/Ubuntu l'installazione avviene tramite il gestore di pacchetti apt:

# Aggiornamento dell'indice dei pacchetti
sudo apt update

# Installazione di nginx
sudo apt install nginx -y

# Avvio del servizio e abilitazione all'avvio automatico
sudo systemctl start nginx
sudo systemctl enable nginx

Per verificare che nginx sia in esecuzione:

# Controllo dello stato del servizio
sudo systemctl status nginx

Visitando l'indirizzo IP del server da un browser si dovrebbe visualizzare la pagina di benvenuto predefinita di nginx.

Struttura delle directory di nginx

Prima di procedere con la configurazione è utile comprendere la struttura delle directory di nginx su sistemi Debian/Ubuntu:

  • /etc/nginx/nginx.conf — file di configurazione principale
  • /etc/nginx/sites-available/ — directory dove si definiscono i blocchi server (virtual host)
  • /etc/nginx/sites-enabled/ — link simbolici ai siti attivi
  • /etc/nginx/snippets/ — frammenti di configurazione riutilizzabili
  • /var/log/nginx/ — log di accesso e di errore
  • /var/www/ — root predefinita per i contenuti web

Installazione di PHP-FPM

WordPress richiede PHP con alcune estensioni specifiche. Installiamo PHP-FPM e le dipendenze necessarie:

# Installazione di PHP-FPM e delle estensioni richieste da WordPress
sudo apt install php8.2-fpm php8.2-mysql php8.2-curl php8.2-gd \
  php8.2-mbstring php8.2-xml php8.2-xmlrpc php8.2-zip \
  php8.2-intl php8.2-imagick php8.2-bcmath -y

Verifichiamo che PHP-FPM sia in ascolto sul socket Unix (preferibile al socket TCP per prestazioni migliori sullo stesso host):

# Controllo della configurazione del pool www
sudo cat /etc/php/8.2/fpm/pool.d/www.conf | grep "^listen"

L'output atteso è qualcosa come:

listen = /run/php/php8.2-fpm.sock

Configurazione del blocco server per WordPress

La configurazione di nginx per WordPress si articola in un blocco server che gestisce le richieste HTTP. Creiamo un nuovo file di configurazione:

# Creazione del file di configurazione per il sito
sudo nano /etc/nginx/sites-available/example.com

Di seguito la configurazione completa e commentata:

# Blocco server per il sito WordPress
server {
    # Porta di ascolto per le connessioni HTTP
    listen 80;
    listen [::]:80;

    # Nome del dominio servito
    server_name example.com www.example.com;

    # Directory radice del sito
    root /var/www/example.com;

    # File indice predefiniti
    index index.php index.html index.htm;

    # Lunghezza massima del corpo della richiesta (per upload di file)
    client_max_body_size 64M;

    # Regola principale per WordPress: tenta file, poi directory, poi passa a index.php
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # Gestione dei file PHP tramite PHP-FPM
    location ~ \.php$ {
        # Verifica che il file esista prima di passarlo a PHP-FPM
        try_files $uri =404;

        # Separazione dello script dall'informazione di percorso
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        # Socket Unix di PHP-FPM (più veloce del socket TCP)
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;

        # File indice per le richieste FastCGI
        fastcgi_index index.php;

        # Inclusione dei parametri FastCGI standard
        include fastcgi_params;

        # Parametro SCRIPT_FILENAME necessario per PHP
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;

        # Timeout per le risposte di PHP-FPM
        fastcgi_read_timeout 300;
    }

    # Blocco dei file sensibili di WordPress
    location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
    }

    # Nega l'accesso ai file nascosti (es. .htaccess, .git)
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Cache per i file statici (immagini, CSS, JS, font)
    location ~* \.(css|gif|ico|jpeg|jpg|js|png|svg|woff|woff2|ttf|eot)$ {
        expires max;
        log_not_found off;
        add_header Cache-Control "public, immutable";
    }

    # Blocco dell'accesso al file xmlrpc.php (vettore comune di attacchi)
    location = /xmlrpc.php {
        deny all;
        access_log off;
        log_not_found off;
    }

    # File di log specifici per questo virtual host
    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;
}

Abilitiamo il sito creando un link simbolico in sites-enabled:

# Abilitazione del sito tramite link simbolico
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

# Verifica della sintassi della configurazione
sudo nginx -t

# Ricaricamento di nginx per applicare le modifiche
sudo systemctl reload nginx

Installazione di WordPress

Creiamo la directory del sito e scarichiamo WordPress:

# Creazione della directory radice
sudo mkdir -p /var/www/example.com

# Download dell'ultima versione di WordPress
cd /tmp
wget https://wordpress.org/latest.tar.gz

# Estrazione dell'archivio
tar xzf latest.tar.gz

# Copia dei file nella directory del sito
sudo cp -r wordpress/. /var/www/example.com/

# Assegnazione della proprietà all'utente di nginx/PHP-FPM
sudo chown -R www-data:www-data /var/www/example.com/

# Permessi corretti per le directory e i file
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;

Creazione del database

WordPress necessita di un database MySQL o MariaDB. Accediamo alla console del database e creiamo le risorse necessarie:

-- Connessione al server MySQL
-- sudo mysql -u root -p

-- Creazione del database per WordPress
CREATE DATABASE wordpress_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Creazione di un utente dedicato (evitare di usare root)
CREATE USER 'wordpress_user'@'localhost' IDENTIFIED BY 'password_sicura';

-- Assegnazione dei privilegi sul database
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wordpress_user'@'localhost';

-- Aggiornamento dei privilegi in memoria
FLUSH PRIVILEGES;

-- Uscita dalla console MySQL
EXIT;

Configurazione di wp-config.php

Copiamo il file di configurazione di esempio e lo modifichiamo:

# Copia del file di configurazione di esempio
sudo cp /var/www/example.com/wp-config-sample.php /var/www/example.com/wp-config.php

# Modifica del file di configurazione
sudo nano /var/www/example.com/wp-config.php

Le righe da modificare nel file sono:

<?php
// Configurazione del database
define( 'DB_NAME',     'wordpress_db' );
define( 'DB_USER',     'wordpress_user' );
define( 'DB_PASSWORD', 'password_sicura' );
define( 'DB_HOST',     'localhost' );
define( 'DB_CHARSET',  'utf8mb4' );

// Prefisso delle tabelle del database (cambiarlo migliora la sicurezza)
$table_prefix = 'wp_';

// Modalità debug (disabilitata in produzione)
define( 'WP_DEBUG', false );

// Forza l'uso di HTTPS per l'area amministrativa
define( 'FORCE_SSL_ADMIN', true );

I salt crittografici vanno generati tramite il servizio ufficiale di WordPress e inseriti nel file. Possono essere ottenuti con:

# Generazione automatica dei salt tramite API di WordPress
curl -s https://api.wordpress.org/secret-key/1.1/salt/

Abilitazione di HTTPS con Let's Encrypt

Un sito WordPress in produzione deve sempre essere servito tramite HTTPS. Utilizziamo Certbot per ottenere un certificato gratuito da Let's Encrypt:

# Installazione di Certbot e del plugin per nginx
sudo apt install certbot python3-certbot-nginx -y

# Ottenimento e installazione automatica del certificato
sudo certbot --nginx -d example.com -d www.example.com

Certbot modificherà automaticamente il blocco server per aggiungere la configurazione SSL. Il risultato sarà simile a:

# Blocco server con SSL gestito da Certbot
server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.php index.html;

    # Certificato e chiave privata emessi da Let's Encrypt
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Parametri SSL consigliati da Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # ... resto della configurazione ...
}

# Reindirizzamento da HTTP a HTTPS
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    # Reindirizzamento permanente verso HTTPS
    return 301 https://$host$request_uri;
}

Il rinnovo automatico del certificato viene configurato da Certbot tramite un timer systemd o un cron job. Per verificare che funzioni:

# Simulazione del rinnovo per verificare la configurazione
sudo certbot renew --dry-run

Ottimizzazione delle prestazioni

Configurazione di Gzip

La compressione Gzip riduce significativamente la dimensione delle risorse trasferite. Nel file /etc/nginx/nginx.conf, all'interno del blocco http:

# Configurazione della compressione Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1000;

# Tipi MIME da comprimere
gzip_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    image/svg+xml
    font/woff2;

Configurazione dei timeout e dei buffer FastCGI

Una corretta configurazione dei buffer FastCGI migliora la risposta del server sotto carico. Aggiungere nel blocco http del file principale o nel blocco server:

# Ottimizzazione dei buffer FastCGI per PHP-FPM
fastcgi_buffers          16 16k;
fastcgi_buffer_size      32k;
fastcgi_connect_timeout  60;
fastcgi_send_timeout     180;
fastcgi_read_timeout     180;

Caching con FastCGI Cache

nginx include un sistema di cache per le risposte FastCGI che può ridurre drasticamente il carico su PHP e sul database. La configurazione richiede due passaggi: la dichiarazione della zona di cache nel blocco http e l'uso della cache nel blocco server.

Nel file /etc/nginx/nginx.conf, dentro il blocco http:

# Definizione della zona di cache FastCGI
# Percorso, nome, livelli di directory, zona di memoria condivisa e dimensione massima
fastcgi_cache_path /var/cache/nginx/fastcgi
    levels=1:2
    keys_zone=wordpress_cache:100m
    inactive=60m
    max_size=1g;

fastcgi_cache_key "$scheme$request_method$host$request_uri";

Creiamo la directory per la cache:

# Creazione della directory di cache con i permessi corretti
sudo mkdir -p /var/cache/nginx/fastcgi
sudo chown www-data:www-data /var/cache/nginx/fastcgi

Nel blocco server del virtual host aggiungiamo la logica di cache:

# Variabile per controllare se la cache deve essere bypassata
set $skip_cache 0;

# Non mettere in cache le richieste POST
if ($request_method = POST) {
    set $skip_cache 1;
}

# Non mettere in cache le URL con query string
if ($query_string != "") {
    set $skip_cache 1;
}

# Non mettere in cache le pagine di amministrazione e le pagine sensibili
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|/wp-login.php|wp-.*.php|/feed/|sitemap(_index)?.xml") {
    set $skip_cache 1;
}

# Non mettere in cache se l'utente ha un cookie di sessione WordPress attivo
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
    set $skip_cache 1;
}

# Blocco per la gestione di PHP con cache abilitata
location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # Attivazione della cache FastCGI
    fastcgi_cache wordpress_cache;
    fastcgi_cache_valid 200 301 302 60m;
    fastcgi_cache_use_stale error timeout updating invalid_header http_500;
    fastcgi_cache_background_update on;
    fastcgi_cache_lock on;

    # Utilizzo della variabile per bypassare la cache quando necessario
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache      $skip_cache;

    # Header informativo per il debug (HIT = dalla cache, MISS = generato da PHP)
    add_header X-FastCGI-Cache $upstream_cache_status;
}

Hardening della sicurezza

Oltre ai blocchi già presenti nella configurazione base, esistono ulteriori misure di sicurezza da applicare.

Nascondere la versione di nginx

Nel blocco http del file nginx.conf:

# Nasconde il numero di versione di nginx negli header HTTP e nelle pagine di errore
server_tokens off;

Header HTTP di sicurezza

Aggiungere nel blocco server i seguenti header:

# Header di sicurezza per mitigare attacchi comuni
add_header X-Frame-Options           "SAMEORIGIN"           always;
add_header X-Content-Type-Options    "nosniff"              always;
add_header X-XSS-Protection          "1; mode=block"        always;
add_header Referrer-Policy           "strict-origin-when-cross-origin" always;
add_header Permissions-Policy        "camera=(), microphone=(), geolocation=()" always;

# Politica di sicurezza dei contenuti (adattare alle esigenze del sito)
add_header Content-Security-Policy
    "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
    always;

Rate limiting

Il rate limiting protegge dalla pagina di login e dall'XML-RPC da attacchi di forza bruta. Nel blocco http:

# Definizione della zona di limitazione delle richieste
# Massimo 1 richiesta al secondo per IP sul login di WordPress
limit_req_zone $binary_remote_addr zone=wordpress_login:10m rate=1r/s;

Nel blocco server:

# Applicazione del rate limiting alla pagina di login
location = /wp-login.php {
    limit_req zone=wordpress_login burst=3 nodelay;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Configurazione completa finale

Di seguito la configurazione completa del virtual host che integra tutti gli elementi descritti:

# Reindirizzamento da HTTP a HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# Blocco server principale con SSL
server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name example.com www.example.com;
    root        /var/www/example.com;
    index       index.php index.html;

    # Certificati SSL emessi da Let's Encrypt
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Dimensione massima per il corpo della richiesta (upload file)
    client_max_body_size 64M;

    # Nasconde la versione di nginx
    server_tokens off;

    # Header di sicurezza
    add_header X-Frame-Options        "SAMEORIGIN"  always;
    add_header X-Content-Type-Options "nosniff"     always;
    add_header X-XSS-Protection       "1; mode=block" always;
    add_header Referrer-Policy        "strict-origin-when-cross-origin" always;

    # Regola principale di WordPress
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # Variabile di controllo per il bypass della cache
    set $skip_cache 0;

    if ($request_method = POST)   { set $skip_cache 1; }
    if ($query_string != "")       { set $skip_cache 1; }
    if ($request_uri ~* "/wp-admin/|/wp-login.php|wp-.*.php|/feed/") {
        set $skip_cache 1;
    }
    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wordpress_logged_in") {
        set $skip_cache 1;
    }

    # Rate limiting sulla pagina di login
    location = /wp-login.php {
        limit_req zone=wordpress_login burst=3 nodelay;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Gestione di PHP con cache FastCGI
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass  unix:/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO       $fastcgi_path_info;
        fastcgi_read_timeout 300;

        # Cache FastCGI
        fastcgi_cache             wordpress_cache;
        fastcgi_cache_valid        200 301 302 60m;
        fastcgi_cache_use_stale    error timeout updating invalid_header http_500;
        fastcgi_cache_background_update on;
        fastcgi_cache_lock         on;
        fastcgi_cache_bypass       $skip_cache;
        fastcgi_no_cache           $skip_cache;
        add_header X-FastCGI-Cache $upstream_cache_status;
    }

    # Blocco degli script PHP nelle cartelle degli upload
    location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
    }

    # Blocco del file xmlrpc.php
    location = /xmlrpc.php {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Blocco dei file nascosti
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Cache lato client per gli asset statici
    location ~* \.(css|gif|ico|jpeg|jpg|js|png|svg|woff|woff2|ttf|eot)$ {
        expires max;
        log_not_found off;
        add_header Cache-Control "public, immutable";
    }

    # Log specifici per questo virtual host
    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;
}

Ottimizzazione di PHP-FPM

La configurazione del pool PHP-FPM influisce direttamente sulle prestazioni di WordPress. Il file da modificare è /etc/php/8.2/fpm/pool.d/www.conf:

; Configurazione del pool www per WordPress

; Modalità di gestione dei processi figli
; dynamic: crea e termina i worker in base al carico
pm = dynamic

; Numero massimo di processi figli simultanei
pm.max_children = 20

; Numero di processi figli creati all'avvio
pm.start_servers = 5

; Numero minimo di processi inattivi
pm.min_spare_servers = 3

; Numero massimo di processi inattivi
pm.max_spare_servers = 7

; Numero massimo di richieste prima che un worker venga riavviato
; Utile per prevenire memory leak di PHP
pm.max_requests = 500

; Timeout per i processi bloccati (in secondi)
request_terminate_timeout = 300

I valori ottimali di pm.max_children dipendono dalla RAM disponibile e dal consumo medio di ogni processo PHP. Una formula approssimativa è: RAM disponibile / consumo medio per processo. Per un server con 2 GB di RAM e processi da circa 50-80 MB, un valore tra 20 e 30 è ragionevole.

Dopo ogni modifica alla configurazione di PHP-FPM, riavviare il servizio:

# Riavvio di PHP-FPM per applicare le modifiche
sudo systemctl restart php8.2-fpm

# Verifica dello stato
sudo systemctl status php8.2-fpm

Monitoraggio e debug

Per monitorare le prestazioni e individuare eventuali problemi, nginx mette a disposizione una pagina di stato (da abilitare) e log dettagliati.

Abilitazione della pagina di status

# Pagina di stato di nginx (accessibile solo da localhost)
location = /nginx_status {
    stub_status on;
    allow 127.0.0.1;
    deny all;
}

Analisi dei log

I log di nginx si trovano in /var/log/nginx/. Per monitorare i log in tempo reale:

# Monitoraggio in tempo reale del log degli errori
sudo tail -f /var/log/nginx/example.com.error.log

# Ricerca di errori 5xx nelle ultime richieste
sudo grep " 5[0-9][0-9] " /var/log/nginx/example.com.access.log | tail -20

# Conteggio delle richieste per codice di stato HTTP
sudo awk '{print $9}' /var/log/nginx/example.com.access.log | sort | uniq -c | sort -rn

Conclusioni

La configurazione di nginx per WordPress richiede più passaggi rispetto all'utilizzo di Apache, ma i vantaggi in termini di prestazioni, consumo di memoria e scalabilità sono significativi. I punti chiave da ricordare sono: l'uso di PHP-FPM come interprete PHP, la corretta configurazione della direttiva try_files per il routing di WordPress, il blocco degli accessi ai file sensibili e, per ambienti ad alto traffico, l'abilitazione della cache FastCGI.

Una volta completata la configurazione di base, è consigliabile eseguire test di carico con strumenti come ab (Apache Benchmark) o wrk per misurare le prestazioni effettive e, se necessario, affinare ulteriormente i parametri di PHP-FPM e della cache.