Перейти до вмісту
EuroVPSEuroVPS

Налаштування WordPress Multisite з nginx і PHP-FPM на RHEL 9 і 10

Повна, робоча конфігурація WordPress Multisite на nginx і PHP-FPM на Red Hat Enterprise Linux 9 і 10 та сумісних системах: пакети, pool PHP-FPM, server block nginx для мереж subdirectory і subdomain, TLS, SELinux, firewalld, wp-config.php і перевірка.

Автор EuroVPS Engineering

Оновлено 22 жовтня 2026 р.11 хв читання
WordPress Multisite, EuroVPS 2024

p

WordPress Multisite запускає багато сайтів з однієї інсталяції WordPress: один код, один набір plugins і themes для оновлення, одна база даних і один network admin, який керує всім. Проблема в тому, що майже кожен посібник в інтернеті написано для Debian або Ubuntu, зі шляхами і пакетами, яких немає на Red Hat Enterprise Linux. Цей посібник збирає весь stack на RHEL 9 і 10, і працює так само на Oracle Linux, AlmaLinux і Rocky Linux.

Усюди в посібнику замінюйте example.com на ваш домен і виконуйте команди від root.

h2: Subdirectory чи subdomain?

  • Мережі subdirectory дають кожному сайту шлях: example.com/shop/, example.com/blog/. Достатньо одного імені DNS і одного звичайного сертифіката. nginx потребує кількох правил rewrite, щоб кожен сайт діставався до спільних файлів wp-admin, wp-includes і wp-content.
  • Мережі subdomain дають кожному сайту hostname: shop.example.com. Вам потрібен wildcard запис DNS (*.example.com) і wildcard сертифікат, але nginx не потребує особливих rewrite.

WordPress пропонує варіант subdirectory лише на свіжій інсталяції, бо на наявному сайті шляхи можуть конфліктувати з permalinks дописів. Вирішіть до того, як почнете.

h2: Крок 1: Встановіть пакети

h3: RHEL 10

RHEL 10 не має module streams. Пакети nginx, PHP і MariaDB з AppStream встановлюються напряму:

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 з module streams AppStream

RHEL 9 постачається зі стандартними PHP і nginx, плюс новішими версіями як module streams. Виведіть їх список, потім увімкніть найновіший stream, який показує список (назви stream нижче є прикладами):

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 з репозиторієм Remi

Якщо вам потрібна гілка PHP, якої AppStream не пропонує, репозиторій Remi надає її як module streams. Remi залежить від EPEL, якому на RHEL потрібен репозиторій CodeReady Builder (на AlmaLinux і Rocky Linux використовуйте dnf config-manager --set-enabled crb замість рядка 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

Module streams Remi заміщують базові пакети PHP, тож кожен шлях у цьому посібнику залишається тим самим. Для RHEL 10 скористайтеся майстром налаштування Remi на rpms.remirepo.net, який дає відповідні команди для того випуску.

Вам також знадобляться інструменти керування SELinux для Кроку 5:

dnf install policycoreutils-python-utils

h2: Крок 2: Налаштуйте pool PHP-FPM

Пакет php-fpm у RHEL запускає свій стандартний pool від користувача apache, бо він налаштований для httpd. З nginx ми запускаємо його від nginx. Відредагуйте /etc/php-fpm.d/www.conf і задайте ці рядки (вони вже існують, змініть значення):

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

Розрахуйте pm.max_children за вашою RAM: виміряйте пам’ять, яку використовує середній PHP worker під реальним трафіком, і переконайтеся, що максимальна кількість workers, помножена на це значення, вміщується в пам’ять із запасом для бази даних і page cache.

h3: Дайте nginx каталоги сесій і логів PHP

Пакет створює /var/lib/php/session, /var/lib/php/opcache і /var/lib/php/wsdlcache з групою apache, а /var/log/php-fpm з власником apache. Pool, який працює від nginx, не може туди писати, і входи в систему збоять заплутаними способами. Оновлення пакетів скидають цих власників, тож задайте їх через systemd-tmpfiles і застосовуйте знову щоразу, коли стартує PHP-FPM. Створіть /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  -

Потім створіть /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: Крок 3: Створіть базу даних

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;

Коли база даних на тому самому сервері, а DB_HOST задано як localhost, PHP підключається через локальний socket і SELinux не потребує змін. Якщо база даних працює на іншому сервері, див. Крок 5.

h2: Крок 4: Встановіть 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 {} +

Відредагуйте wp-config.php: задайте DB_NAME, DB_USER, DB_PASSWORD і DB_HOST, і замініть ключі та salts із заповнювачів на свіжі з сервісу секретних ключів WordPress. Над рядком «That's all, stop editing!» додайте:

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

Поки що додайте лише WP_ALLOW_MULTISITE. Константи мережі з’являться пізніше, від самого WordPress.

Встановіть також WP-CLI. Це найбезпечніший спосіб оновлювати захищену інсталяцію, і він спрощує перевірки наприкінці:

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: Крок 5: Мітки і booleans SELinux

Тримайте SELinux у режимі enforcing. /var/www уже має мітку httpd_sys_content_t, яку nginx і PHP-FPM можуть читати, але не писати. Саме це вам потрібно для коду WordPress. Лише каталоги, у які WordPress мусить писати, отримують мітку читання і запису:

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 тримає медіа кожного сайту під wp-content/uploads/sites/<id>/, тож одне правило покриває всю мережу. Якщо ви використовуєте plugin кешування, який пише у wp-content/cache, дайте тому каталогу таку саму мітку.

Потім увімкніть лише ті booleans, які вам потрібні:

# 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

Оскільки сам код доступний PHP лише для читання, встановлення plugins або оновлення WordPress з панелі керування буде відхилено. Робіть це натомість через WP-CLI, від імені власника сайту, що також дає вам чітку історію того, що змінилося:

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

Якщо ви віддаєте перевагу оновленням з панелі керування, дайте мітку httpd_sys_rw_content_t також wp-content/plugins і wp-content/themes, приймаючи, що PHP тоді зможе писати код.

h2: Крок 6: Відкрийте firewall

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

h2: Крок 7: Отримайте сертифікат TLS

На RHEL конфігурація сайтів nginx живе в /etc/nginx/conf.d/: кожен файл *.conf там завантажується. Немає ні sites-available, ні sites-enabled. Почніть із простого server block HTTP, щоб сертифікат можна було видати, у /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: Мережа subdirectory: звичайний сертифікат

certbot є в EPEL. На RHEL увімкніть CodeReady Builder і встановіть пакет EPEL release для вашої основної версії (на AlmaLinux і Rocky Linux натомість увімкніть crb через dnf config-manager. На Oracle Linux встановіть oracle-epel-release-el9 або 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: Мережа subdomain: wildcard сертифікат

Wildcard сертифікати перевіряються лише через DNS. Використовуйте DNS plugin certbot для вашого провайдера DNS, щоб поновлення відбувалися без нагляду (наприклад, python3-certbot-dns-rfc2136 для сервера, який приймає динамічні оновлення), або встановіть wildcard сертифікат від вашого власного центру сертифікації. Сертифікати, якими ви керуєте самі, належать до /etc/pki/tls/certs/, а ключі до /etc/pki/tls/private/, де SELinux уже дозволяє nginx їх читати.

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

Для мережі subdomain також змініть server_name у наведеному вище block HTTP на example.com *.example.com.

h2: Крок 8: Конфігурація nginx

Щойно сертифікат існує, замініть /etc/nginx/conf.d/example.com.conf повною конфігурацією для вашого типу мережі. Regex location перевіряються в порядку появи, тож тримайте правила deny над block PHP.

h3: Мережа 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: Мережа subdomain

Та сама структура без block rewrite, з wildcard server_name і wildcard сертифікатом. Стандартного правила try_files достатньо для WordPress, бо кожен сайт розрізняється за своїм 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;
    }
}

Кілька приміток до цих файлів:

  • ssl_ciphers PROFILE=SYSTEM передає вибір шифрів загальносистемній криптополітиці RHEL, тож сервер дотримується update-crypto-policies замість переліку шифрів, складеного вручну.
  • На nginx 1.25.1 і новіших listen ... http2 досі працює, але виводить попередження про застарілість. Можна натомість написати listen 443 ssl; плюс http2 on;. Старіші збірки nginx у RHEL 9 потребують показаної форми.
  • try_files $uri =404 у block PHP не дає nginx передавати до PHP-FPM запити на файли PHP, яких не існує.
  • Формат логу main визначено у стандартному /etc/nginx/nginx.conf. Стандартний server block у тому файлі продовжує відповідати на запити до невідомих hostname. Ваш бере на себе example.com.
nginx -t && systemctl reload nginx

h2: Крок 9: Встановіть WordPress і ввімкніть мережу

Відкрийте https://example.com/ і пройдіть звичайний інсталятор WordPress, або зробіть це з 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

Увійдіть, деактивуйте всі plugins, потім перейдіть у Tools, Network Setup. Оскільки WP_ALLOW_MULTISITE задано, WordPress пропонує вибір між subdomains і subdirectories (на свіжій інсталяції). Оберіть, натисніть Install, і WordPress покаже block констант для додавання у wp-config.php, над рядком «stop editing». Для мережі subdirectory він виглядає так:

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 );

Для мережі subdomain SUBDOMAIN_INSTALL дорівнює true. Копіюйте block, який дає вам WordPress, а не цей приклад. WordPress також показує правила .htaccess. Вони для Apache і нічого не роблять на nginx, бо конфігурація nginx з Кроку 8 уже виконує цю роботу. Увійдіть знову, і з’явиться меню My Sites, Network Admin.

Те саме перетворення робиться через WP-CLI за один крок: sudo -u nginx wp --path=/var/www/example.com core multisite-convert, з додаванням --subdomains для мережі subdomain. Він записує константи у wp-config.php за вас. Потім за бажанням приберіть рядок WP_ALLOW_MULTISITE, він більше не потрібен.

h2: Крок 10: Перевірте

Створіть тестовий сайт і перевірте, що кожна частина шляху працює:

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

Для мережі subdomain використовуйте https://test.example.com/ у тих самих перевірках. Потім завантажте зображення в медіатеку тестового сайту і переконайтеся, що воно потрапляє у wp-content/uploads/sites/2/. Якщо щось не працює, ось куди дивитися:

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"

«Permission denied» у лозі PHP разом із записом AVC вказує на відсутню мітку або boolean SELinux (Крок 5). 502 від nginx означає, що він не зміг дістатися до socket PHP-FPM: перевірте, що php-fpm працює і що listen.acl_users містить nginx. Повернення на сторінку входу після правильного пароля зазвичай означає, що каталог сесій недоступний для запису користувачем pool (Крок 2).

h2: Підтримання в здоровому стані

  • Оновлюйте stack через dnf upgrade, а WordPress через WP-CLI, і після цього перевіряйте сайт наведеними вище тестами curl.
  • Робіть резервні копії бази даних і wp-content/uploads разом, з окремого сервера резервного копіювання, який сам забирає дані, щоб вебсервер не тримав облікових даних до резервних копій.
  • Додайте page cache (nginx FastCGI cache або plugin кешування), щойно мережа отримає реальний трафік, і задайте request_slowlog_timeout у pool, щоб бачити, які запити повільні.

p

Інженери EuroVPS збирають і експлуатують такі stack WordPress Multisite на nginx і PHP-FPM для клієнтів на наших managed servers.

З архіву блогу EuroVPS, уперше опубліковано 27 січня 2024 р..

Усі нотатки з практики