Vai al contenuto
EuroVPSEuroVPS

Configurare WordPress Multisite con nginx e PHP-FPM su RHEL 9 e 10

Un'installazione completa e funzionante di WordPress Multisite su nginx e PHP-FPM in Red Hat Enterprise Linux 9 e 10 e sistemi compatibili: pacchetti, pool PHP-FPM, server block nginx per reti subdirectory e subdomain, TLS, SELinux, firewalld, wp-config.php e verifica.

Di EuroVPS Engineering

Aggiornato il 22 ottobre 202611 min di lettura
WordPress Multisite, EuroVPS 2024

p

WordPress Multisite fa girare molti siti da una sola installazione WordPress: un solo codice, un solo insieme di plugin e temi da aggiornare, un solo database, e un network admin che controlla tutto. Il problema è che quasi ogni guida online è scritta per Debian o Ubuntu, con percorsi e pacchetti che su Red Hat Enterprise Linux non esistono. Questa guida costruisce l'intero stack su RHEL 9 e 10, e funziona allo stesso modo su Oracle Linux, AlmaLinux e Rocky Linux.

In tutta la guida, sostituite example.com con il vostro dominio ed eseguite i comandi come root.

h2: Subdirectory o subdomain?

  • Le reti subdirectory danno a ogni sito un percorso: example.com/shop/, example.com/blog/. Bastano un nome DNS e un certificato ordinario. nginx ha bisogno di poche regole di rewrite perché ogni sito raggiunga i file condivisi wp-admin, wp-includes e wp-content.
  • Le reti subdomain danno a ogni sito un hostname: shop.example.com. Vi servono un record DNS wildcard (*.example.com) e un certificato wildcard, ma nginx non ha bisogno di rewrite particolari.

WordPress offre la scelta subdirectory solo su un'installazione nuova, perché su un sito esistente i percorsi potrebbero entrare in conflitto con i permalink degli articoli. Decidete prima di cominciare.

h2: Passo 1: installate i pacchetti

h3: RHEL 10

RHEL 10 non ha module stream. I pacchetti nginx, PHP e MariaDB di AppStream si installano direttamente:

dnf install nginx php-fpm php-cli php-mysqlnd php-gd php-intl php-mbstring php-xml php-opcache php-pecl-zip mariadb-server

h3: RHEL 9 con i module stream di AppStream

RHEL 9 arriva con PHP e nginx predefiniti, più versioni più recenti come module stream. Guardate l'elenco, poi abilitate lo stream più recente che l'elenco mostra (i nomi degli stream qui sotto sono esempi):

dnf module list php nginx
dnf module reset php nginx
dnf module enable php:8.2 nginx:1.24
dnf install nginx php-fpm php-cli php-mysqlnd php-gd php-intl php-mbstring php-xml php-opcache php-pecl-zip mariadb-server

h3: RHEL 9 con il repository Remi

Se vi serve un ramo di PHP che AppStream non offre, il repository Remi lo fornisce come module stream. Remi dipende da EPEL, che su RHEL richiede il repository CodeReady Builder (su AlmaLinux e Rocky Linux usate dnf config-manager --set-enabled crb al posto della riga subscription-manager):

subscription-manager repos --enable codeready-builder-for-rhel-9-$(arch)-rpms
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
dnf install https://rpms.remirepo.net/enterprise/remi-release-9.rpm
dnf module reset php
dnf module enable php:remi-8.3
dnf install nginx php-fpm php-cli php-mysqlnd php-gd php-intl php-mbstring php-xml php-opcache php-pecl-zip mariadb-server

I module stream di Remi sostituiscono i pacchetti PHP di base, quindi ogni percorso di questa guida resta uguale. Per RHEL 10, usate la guida di configurazione di Remi su rpms.remirepo.net, che fornisce i comandi corrispondenti per quella versione.

Vi serviranno anche gli strumenti di gestione di SELinux per il Passo 5:

dnf install policycoreutils-python-utils

h2: Passo 2: configurate il pool PHP-FPM

Il pacchetto php-fpm di RHEL esegue il pool predefinito come utente apache, perché è pensato per httpd. Con nginx lo eseguiamo come nginx. Modificate /etc/php-fpm.d/www.conf e impostate queste righe (esistono già, cambiate i valori):

user = nginx
group = nginx
listen = /run/php-fpm/www.sock
listen.acl_users = nginx

pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

php_value[upload_max_filesize] = 64M
php_value[post_max_size] = 64M
php_value[memory_limit] = 256M

Dimensionate pm.max_children in base alla vostra RAM: misurate la memoria usata da un worker PHP medio sotto traffico reale, e assicuratevi che il numero massimo di worker moltiplicato per quel valore stia in memoria con margine per il database e la page cache.

h3: Date a nginx le directory di sessione e di log di PHP

Il pacchetto crea /var/lib/php/session, /var/lib/php/opcache e /var/lib/php/wsdlcache con gruppo apache, e /var/log/php-fpm con proprietario apache. Un pool che gira come nginx non può scrivervi, e i login falliscono in modi confusi. Gli aggiornamenti dei pacchetti ripristinano quelle proprietà, quindi impostatele con systemd-tmpfiles e riapplicatele ogni volta che PHP-FPM si avvia. Create /etc/tmpfiles.d/php-fpm-nginx.conf:

z /var/lib/php/session   0770 root  nginx -
z /var/lib/php/opcache   0770 root  nginx -
z /var/lib/php/wsdlcache 0770 root  nginx -
z /var/log/php-fpm       0770 nginx root  -

Poi create /etc/systemd/system/php-fpm.service.d/10-nginx-perms.conf:

[Service]
ExecStartPre=/usr/bin/systemd-tmpfiles --create /etc/tmpfiles.d/php-fpm-nginx.conf
systemctl daemon-reload
systemctl enable --now php-fpm
ls -ld /var/lib/php/session /var/log/php-fpm
ls -l /run/php-fpm/www.sock

h2: Passo 3: create il database

systemctl enable --now mariadb
mariadb-secure-installation
mariadb
CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'use-a-long-random-password';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Con il database sullo stesso server e DB_HOST impostato a localhost, PHP si connette tramite il socket locale e SELinux non richiede modifiche. Se il database gira su un altro server, vedete il Passo 5.

h2: Passo 4: installate WordPress

cd /var/www
curl -O https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
mv wordpress example.com
cd example.com
cp wp-config-sample.php wp-config.php
mkdir -p wp-content/uploads wp-content/upgrade
chown -R nginx:nginx /var/www/example.com
find /var/www/example.com -type d -exec chmod 750 {} +
find /var/www/example.com -type f -exec chmod 640 {} +

Modificate wp-config.php: impostate DB_NAME, DB_USER, DB_PASSWORD e DB_HOST, e sostituite le chiavi e i salt segnaposto con valori nuovi presi dal servizio secret key di WordPress. Sopra la riga che dice «That's all, stop editing!», aggiungete:

define( 'FORCE_SSL_ADMIN', true );
define( 'WP_ALLOW_MULTISITE', true );

Per ora aggiungete solo WP_ALLOW_MULTISITE. Le costanti della rete arrivano più tardi, da WordPress stesso.

Installate anche WP-CLI. È il modo più sicuro per aggiornare un'installazione blindata e rende facili i controlli alla fine:

curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod 755 /usr/local/bin/wp
wp --info

h2: Passo 5: etichette e boolean di SELinux

Tenete SELinux in enforcing. /var/www ha già l'etichetta httpd_sys_content_t, che nginx e PHP-FPM possono leggere ma non scrivere. È ciò che volete per il codice di WordPress. Solo le directory in cui WordPress deve scrivere ricevono l'etichetta di lettura e scrittura:

semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example.com/wp-content/uploads(/.*)?"
semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example.com/wp-content/upgrade(/.*)?"
restorecon -Rv /var/www/example.com
ls -Zd /var/www/example.com/wp-content/uploads

Multisite tiene i media di ogni sito sotto wp-content/uploads/sites/<id>/, quindi quell'unica regola copre l'intera rete. Se usate un plugin di caching che scrive in wp-content/cache, date a quella directory la stessa etichetta.

Poi abilitate solo i boolean di cui avete bisogno:

# WordPress fetches updates, talks to APIs and runs wp-cron over HTTP
setsebool -P httpd_can_network_connect on

# only if the database is on another server
setsebool -P httpd_can_network_connect_db on

# only if WordPress sends mail through the local MTA or an SMTP server
setsebool -P httpd_can_sendmail on

Poiché il codice stesso è in sola lettura per PHP, l'installazione di plugin o l'aggiornamento di WordPress dalla dashboard verranno rifiutati. Fatelo invece con WP-CLI, come proprietario del sito, il che vi dà anche uno storico pulito di ciò che è cambiato:

sudo -u nginx wp --path=/var/www/example.com core update
sudo -u nginx wp --path=/var/www/example.com plugin update --all --network

Se preferite gli aggiornamenti dalla dashboard, date anche a wp-content/plugins e wp-content/themes l'etichetta httpd_sys_rw_content_t, accettando che PHP possa allora scrivere codice.

h2: Passo 6: aprite il firewall

firewall-cmd --permanent --add-service=http --add-service=https
firewall-cmd --reload
firewall-cmd --list-services

h2: Passo 7: ottenete un certificato TLS

Su RHEL, la configurazione dei siti di nginx vive in /etc/nginx/conf.d/: ogni file *.conf lì dentro viene caricato. Non esistono sites-available o sites-enabled. Cominciate con un semplice server block HTTP in modo che il certificato possa essere emesso, in /etc/nginx/conf.d/example.com.conf:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}
nginx -t
systemctl enable --now nginx

h3: Rete subdirectory: un certificato ordinario

certbot si trova in EPEL. Su RHEL, abilitate CodeReady Builder e installate il pacchetto EPEL release per la vostra versione principale (su AlmaLinux e Rocky Linux abilitate invece crb con dnf config-manager. Su Oracle Linux installate oracle-epel-release-el9 o oracle-epel-release-el10):

# RHEL 9
subscription-manager repos --enable codeready-builder-for-rhel-9-$(arch)-rpms
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm

# RHEL 10
subscription-manager repos --enable codeready-builder-for-rhel-10-$(arch)-rpms
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
dnf install certbot
certbot certonly --webroot -w /var/www/example.com -d example.com -d www.example.com --deploy-hook "systemctl reload nginx"
systemctl enable --now certbot-renew.timer

h3: Rete subdomain: un certificato wildcard

I certificati wildcard si convalidano solo via DNS. Usate il plugin DNS di certbot per il vostro provider DNS, così i rinnovi girano senza supervisione (per esempio python3-certbot-dns-rfc2136 per un server che accetta aggiornamenti dinamici), oppure installate un certificato wildcard della vostra autorità di certificazione. I certificati che gestite da soli vanno in /etc/pki/tls/certs/ e le chiavi in /etc/pki/tls/private/, dove SELinux permette già a nginx di leggerli.

certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /etc/letsencrypt/rfc2136.ini -d example.com -d "*.example.com" --deploy-hook "systemctl reload nginx"

Per una rete subdomain, cambiate anche il server_name nel block HTTP qui sopra in example.com *.example.com.

h2: Passo 8: la configurazione di nginx

Una volta ottenuto il certificato, sostituite /etc/nginx/conf.d/example.com.conf con la configurazione completa per il vostro tipo di rete. Le location con regex vengono valutate nell'ordine in cui compaiono, quindi tenete le regole deny sopra il block di PHP.

h3: Rete subdirectory

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;

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

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         PROFILE=SYSTEM;
    ssl_prefer_server_ciphers on;
    ssl_session_cache   shared:SSL:10m;

    client_max_body_size 64m;

    access_log /var/log/nginx/example.com.access.log main;
    error_log  /var/log/nginx/example.com.error.log;

    # Multisite subdirectory rules: map /site/wp-admin, /site/wp-includes,
    # /site/wp-content and /site/*.php to the shared WordPress files.
    if (!-e $request_filename) {
        rewrite /wp-admin$ $scheme://$host$uri/ permanent;
        rewrite ^(/[^/]+)?(/wp-.*) $2 last;
        rewrite ^(/[^/]+)?(/.*\.php) $2 last;
    }

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # never serve hidden files or run PHP from uploads
    location ~ /\.(?!well-known) {
        deny all;
    }
    location ~* ^/wp-content/uploads/.*\.php$ {
        deny all;
    }

    location ~* \.(?:css|js|gif|ico|jpe?g|png|svg|webp|avif|woff2?)$ {
        expires 30d;
        access_log off;
        try_files $uri =404;
    }

    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_read_timeout 300;
    }
}

h3: Rete subdomain

La stessa struttura senza il block dei rewrite, con server_name wildcard e il certificato wildcard. La regola standard try_files è tutto ciò di cui WordPress ha bisogno, perché ogni sito si distingue dal suo hostname:

server {
    listen 80;
    listen [::]:80;
    server_name example.com *.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com *.example.com;

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

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         PROFILE=SYSTEM;
    ssl_prefer_server_ciphers on;
    ssl_session_cache   shared:SSL:10m;

    client_max_body_size 64m;

    access_log /var/log/nginx/example.com.access.log main;
    error_log  /var/log/nginx/example.com.error.log;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
    location ~* ^/wp-content/uploads/.*\.php$ {
        deny all;
    }

    location ~* \.(?:css|js|gif|ico|jpe?g|png|svg|webp|avif|woff2?)$ {
        expires 30d;
        access_log off;
        try_files $uri =404;
    }

    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_read_timeout 300;
    }
}

Qualche nota su questi file:

  • ssl_ciphers PROFILE=SYSTEM delega la scelta dei cipher alla policy crittografica di sistema di RHEL, così il server segue update-crypto-policies invece di una lista di cipher scritta a mano.
  • Su nginx 1.25.1 e successivi, listen ... http2 funziona ancora ma stampa un avviso di deprecazione. Potete scrivere invece listen 443 ssl; più http2 on;. Le versioni più vecchie di nginx su RHEL 9 richiedono la forma che mostriamo.
  • try_files $uri =404 nel block di PHP impedisce a nginx di passare a PHP-FPM richieste per file PHP che non esistono.
  • Il log format main è definito nel file /etc/nginx/nginx.conf di partenza. Il server block predefinito in quel file continua a rispondere alle richieste per hostname sconosciuti. Il vostro si occupa di example.com.
nginx -t && systemctl reload nginx

h2: Passo 9: installate WordPress e attivate la rete

Aprite https://example.com/ e completate il normale installer di WordPress, oppure fatelo dalla shell:

sudo -u nginx wp --path=/var/www/example.com core install --url=https://example.com --title="Example Network" --admin_user=netadmin --admin_email=admin@example.com --prompt=admin_password

Accedete, disattivate eventuali plugin, poi andate in Tools, Network Setup. Poiché WP_ALLOW_MULTISITE è impostato, WordPress offre la scelta tra subdomain e subdirectory (su un'installazione nuova). Scegliete, premete Install, e WordPress mostra un blocco di costanti da aggiungere a wp-config.php, sopra la riga «stop editing». Per una rete subdirectory somiglia a questo:

define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', false );
define( 'DOMAIN_CURRENT_SITE', 'example.com' );
define( 'PATH_CURRENT_SITE', '/' );
define( 'SITE_ID_CURRENT_SITE', 1 );
define( 'BLOG_ID_CURRENT_SITE', 1 );

Per una rete subdomain, SUBDOMAIN_INSTALL è true. Copiate il blocco che WordPress vi fornisce, non questo esempio. WordPress mostra anche delle regole .htaccess. Sono per Apache e su nginx non fanno nulla, perché la configurazione nginx del Passo 8 fa già quel lavoro. Accedete di nuovo e compare il menu My Sites, Network Admin.

La stessa conversione si fa con WP-CLI in un solo passo: sudo -u nginx wp --path=/var/www/example.com core multisite-convert, aggiungendo --subdomains per una rete subdomain. Scrive le costanti in wp-config.php per voi. Rimuovete poi la riga WP_ALLOW_MULTISITE se volete, non serve più.

h2: Passo 10: verificate

Create un sito di prova e controllate che ogni tratto del percorso funzioni:

cd /var/www/example.com

# subdirectory network
sudo -u nginx wp site create --slug=test --title="Test site" --email=admin@example.com
# subdomain network: the same command creates test.example.com

sudo -u nginx wp site list
# home page of the new site: expect 200
curl -sI https://example.com/test/ | head -1

# shared assets through the site path: expect 200
curl -sI https://example.com/test/wp-includes/js/jquery/jquery.min.js | head -1

# admin without a trailing slash redirects: expect 301 to /test/wp-admin/
curl -sI https://example.com/test/wp-admin | grep -iE "^HTTP|^location"

# login page: expect 200
curl -sI https://example.com/test/wp-login.php | head -1

# PHP in uploads is refused: expect 403
curl -sI https://example.com/wp-content/uploads/x.php | head -1

# HTTP goes to HTTPS: expect 301
curl -sI http://example.com/ | head -1

Per una rete subdomain usate https://test.example.com/ negli stessi controlli. Poi caricate un'immagine nella libreria media del sito di prova e confermate che finisca in wp-content/uploads/sites/2/. Se qualcosa fallisce, è qui che guardate:

tail -50 /var/log/nginx/example.com.error.log
tail -50 /var/log/php-fpm/www-error.log
ausearch -m AVC -ts recent -i
journalctl -u php-fpm -u nginx --since "15 min ago"

Un «Permission denied» nel log di PHP insieme a una voce AVC indica un'etichetta o un boolean di SELinux mancante (Passo 5). Un 502 da nginx significa che non è riuscito a raggiungere il socket di PHP-FPM: controllate che php-fpm sia in esecuzione e che listen.acl_users includa nginx. Essere rimandati alla pagina di login dopo una password corretta di solito significa che la directory di sessione non è scrivibile dall'utente del pool (Passo 2).

h2: Mantenerlo in salute

  • Aggiornate con dnf upgrade per lo stack e con WP-CLI per WordPress, e controllate il sito dopo con i test curl qui sopra.
  • Fate il backup del database e di wp-content/uploads insieme, da un server di backup separato che preleva i dati, così il web server non custodisce credenziali per i backup.
  • Aggiungete una page cache (FastCGI cache di nginx o un plugin di caching) appena la rete ha traffico reale, e impostate request_slowlog_timeout nel pool per vedere quali richieste sono lente.

p

Gli ingegneri di EuroVPS costruiscono e gestiscono stack WordPress Multisite come questo su nginx e PHP-FPM per i clienti sui nostri managed server.

Dall'archivio del blog EuroVPS, pubblicato per la prima volta il 27 gennaio 2024.

Tutte le note dal campo