Caddy + Drupal 7 на Rocky Linux 8

Предлагаю вашему вниманию инструкцию по развертыванию Drupal 7 на VDS/VPS с применением веб-сервера Caddy. Предполагается, что серверу доступно примерно 500 мегабайт оперативки и предустановлена Rocky Linux 8 (скорее всего аналогично будет и в Alma с Oracle). 9-е семейство RHEL для такой ситуации по-моему уже слишком тяжеловесное.

Поскольку речь идет о VDS/VPS, консольные команды скорее всего будут выполняться от имени root, поэтому в начале будет стоять решетка. Если не root, то большинство (если не все) команды придется предварять sudo.

Подготовка ОС

Файл подкачки

Обычно на VDS подкачки нет (проверить, например, free -m). В этом случае создадим файл подкачки на 512 “метров”:

# fallocate -l 512M /swapfile
# chmod 600 /swapfile
# mkswap /swapfile
# swapon /swapfile

Сейчас скорее всего можно установить mc.

В /etc/fstab добавить:

/swapfile none swap defaults 0 0

в /etc/sysctl.conf добавить (якобы чтобы поменьше "свопился"):

vm.swappiness=10

Перезагрузить для пущей важности. Вот теперь можно dnf update.

fail2ban

# dnf install fail2ban

Должен подтянуться и fail2ban-firewalld.

Файл /etc/fail2ban/jail.d/01-sshd.conf

[sshd]
enabled = true
bantime = 12h
maxretry = 3

Перезапустить сервис:

# systemctl restart fail2ban

Проверить статус:

# fail2ban-client status

firewalld

Проверить список правил:

# firewall-cmd --list-all

Например, помимо прочего:

services: cockpit dhcpv6-client ssh

Удалить cockpit, добавить http и https:

# firewall-cmd --permanent --remove-service=cockpit
# firewall-cmd --permanent --add-service=http
# firewall-cmd --permanent --add-service=https
# firewall-cmd --reload

kdump

Обычно включен, но не нужен.

Проверка состояния сервиса и отключение:

# systemctl status kdump
# systemctl disable kdump

Проверка наличия параметра (аргумента) ядра:

# dir /boot/vmlinuz*
# grubby --info /boot/vmlinuz-4.18.0-553.22.1.el8_10.x86_64 | grep args

(подставить нужный файл из вывода dir). Пример вывода:

args="ro crashkernel=auto rhgb quiet $tuned_params"

Удалить:

# grubby --remove-args="crashkernel" --update=ALL

Хотя особой разницы что-то не заметил. Подозреваю, что crashkernel=auto и так не резервировал память, видя менее 500 мегабайт.

MariaDB

Установим какую-нибудь не самую новую, например 10.6. Более привычная система сохранилась на mariadb.org. Более новая:

https://mariadb.com/docs/server/deploy/deployment-methods/repo/#Configur...

Почему бы и нет.

# curl -LsSO https://r.mariadb.com/downloads/mariadb_repo_setup
# chmod +x mariadb_repo_setup
# ./mariadb_repo_setup --mariadb-server-version=10.6

https://mariadb.com/docs/server/deploy/topologies/single-node/community-...

Устанавливаем:

# dnf install MariaDB-server

Включаем:

# systemctl enable --now mysql

Защищаем установку:

# mariadb-secure-installation

Холивар по поводу авторизации по сокету vs пароль root. Лично я предпочитаю сокет, чтобы не добавлять параметры -u и -p в команды. Поэтому везде ответ по умолчанию, кроме изменения пароля root:

Enter current password for root (enter for none):
Switch to unix_socket authentication [Y/n]
Change the root password? [Y/n] n
Remove anonymous users? [Y/n]
Disallow root login remotely? [Y/n]
Remove test database and access to it? [Y/n]
Reload privilege tables now? [Y/n]

Заходим в командную строку MariaDB:

# mysql

Запросом SHOW VARIABLES LIKE 'performance_schema'; проверяем, что Performance Schema выключена (не хватало ее еще на 500 метрах оперативки):

MariaDB [(none)]> SHOW VARIABLES LIKE 'performance_schema';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| performance_schema | OFF   |
+--------------------+-------+

Серией запросов создаем БД drupal, пользователя drupal (пароль лучше назначить более надежный) и назначаем привилегии:

CREATE DATABASE drupal;
CREATE USER drupal@localhost IDENTIFIED BY 'drupal_password';
GRANT ALL PRIVILEGES ON drupal.* to drupal@localhost;
FLUSH PRIVILEGES;

Выходим exit и загружаем дамп (если он есть) (удобно же с сокетом?):

# zcat drupal.sql.gz | mysql drupal

PHP-FPM

https://docs.rockylinux.org/ru/guides/web/php/

epel-release скорее всего уже каким-то образом установлен, но на всякий случай:

# dnf install epel-release

Подключаем remi:

# dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm

Смотрим, какие версии доступны:

# dnf module list php

https://www.drupal.org/docs/7/system-requirements/php-requirements-for-d...

Drupal 7, конечно, история больше для 7-го же PHP, так что можно было бы взять 7.2 (по умолчанию) из AppStream, но актуальная версия PHP 8.3, которой в AppStream нет. В зависимости от проекта может понадобиться минимум правок (или они вообще не понадобятся). С другой стороны, рекомендуют 8.1 или 8.2, они есть в AppStream. Для определенности - я все-таки взял 8.3 из remi.

# dnf module enable php:remi-8.3

Устанавливаем php-fpm и командную строку (“просто” php не нужен, чтобы не тянуть Apache) и минимальный набор расширений.

# dnf install php-fpm php-cli php-gd php-xml php-mbstring php-mysqlnd

С пользователями и группами разберемся позже, а пока немного “приструним” пул процессов.

В /etc/php-fpm.d/www.conf правим (уменьшаем) параметры, например как в образе Докера:

pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Если и этого будет недостаточно, то можно отрегулировать следующие параметры в файле /etc/php-fpm.conf, например:

emergency_restart_threshold = 10
emergency_restart_interval = 1m
process_control_timeout = 10s

Включаем сервис:

# systemctl enable --now php-fpm

Caddy

https://docs.rockylinux.org/ru/guides/web/caddy/

Раз у нас и так есть EPEL, то установим Caddy из него:

# dnf install caddy

Справедливости ради, он там не самый новый, ну да ладно.

Включим сервис:

# systemctl enable --now caddy

И посмотрим, что сервер как-то отзывается:

HTTP Server Test Page

Теперь, если проанализировать определение сервиса, то мы заметим, что Caddy запускается под одноименным пользователем, в то время как php-fpm - от имени apache. Заменим nginx на caddy в параметре listen.acl_users файла /etc/php-fpm.d/www.conf:

listen.acl_users = apache,caddy

На данном этапе не обязательно, так что по желанию:

# systemctl restart php-fpm

Drupal

Проверка

Разворачиваем “исходники” в /var/www/html, меняем владельца на apache (в соответствии с php-fpm).

# chown -R apache:apache /var/www/html

Так как по умолчанию права доступа 755 / 644, то веб-сервер (пользователь caddy) также сможет читать файлы. Корректируем, при необходимости, sites/default/settings.php.

Далее или исправляем основную конфигурацию Caddy (/etc/caddy/Caddyfile), или добавляем файл в /etc/caddy/Caddyfile.d. Во втором варианте при заходе на сервер “по IP” или неизвестным именем хоста будет отображаться заглушка, что скорее всего неправильно.

Итак, номинально (ВНИМАНИЕ! НЕ БЕЗОПАСНО!) правим Caddyfile:

# The Caddyfile is an easy way to configure your Caddy web server.
# https://caddyserver.com/docs/caddyfile

# The configuration below serves a welcome page over HTTP on port 80.  To use
# your own domain name with automatic HTTPS, ensure your A/AAAA DNS record is
# pointing to this machine's public IP, then replace `http://` with your domain
# name.  Refer to the documentation for full instructions on the address
# specification.
#
# https://caddyserver.com/docs/caddyfile/concepts#addresses
http:// {

    # Set this path to your site's directory.
    #root * /usr/share/caddy
    root * /var/www/html

    # Enable the static file server.
    file_server

    # Another common task is to set up a reverse proxy:
    # reverse_proxy localhost:8080

    # Or serve a PHP site through php-fpm:
    # php_fastcgi localhost:9000
    php_fastcgi unix//run/php-fpm/www.sock

    # Refer to the directive documentation for more options.
    # https://caddyserver.com/docs/caddyfile/directives

}

# As an alternative to editing the above site block, you can add your own site
# block files in the Caddyfile.d directory, and they will be included as long
# as they use the .caddyfile extension.
import Caddyfile.d/*.caddyfile

Перезапускаем сервисы (кстати можно делать caddy reload вместо systemctl restart caddy), проверяем. В основном должно работать, а всякими нюансами займемся далее.

Права на запись

Необходимо обеспечить права на запись в sites/default/files. Вспоминаем, что у нас Rocky Linux, а следовательно включен SELinux (проверка - sestatus).

[root@vm876632 ~]# sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33
[root@vm876632 ~]# cd /var/www/html/sites/default/files
[root@vm876632 files]# ls -Z
unconfined_u:object_r:httpd_sys_content_t:s0 bootstrap
unconfined_u:object_r:httpd_sys_content_t:s0 css
unconfined_u:object_r:httpd_sys_content_t:s0 css_injector
unconfined_u:object_r:httpd_sys_content_t:s0 ctools
unconfined_u:object_r:httpd_sys_content_t:s0 imagecache
unconfined_u:object_r:httpd_sys_content_t:s0 imagecache_sample.png
unconfined_u:object_r:httpd_sys_content_t:s0 imagefield_default_images
unconfined_u:object_r:httpd_sys_content_t:s0 imagefield_thumbs
unconfined_u:object_r:httpd_sys_content_t:s0 images
unconfined_u:object_r:httpd_sys_content_t:s0 imports
unconfined_u:object_r:httpd_sys_content_t:s0 js
unconfined_u:object_r:httpd_sys_content_t:s0 languages
unconfined_u:object_r:httpd_sys_content_t:s0 module
unconfined_u:object_r:httpd_sys_content_t:s0 products
unconfined_u:object_r:httpd_sys_content_t:s0 styles
unconfined_u:object_r:httpd_sys_content_t:s0 xmlsitemap

Как видим, контекст httpd_sys_content_t, в то время как нужен httpd_sys_rw_content_t.

По идее fail2ban подтянул пакет policycoreutils-python-utils, из которого нам нужны команды semanage и restorecon. Посему:

# semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html/sites/default/files(/.*)?"

Установим этот же контекст и на каталог sites/all для работы функций обновления.

# semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html/sites/all(/.*)?"

Восстанавливаем (применяем) контекст:

# restorecon -r /var/www/html/sites

На всякий случай стоит проверить эту настройку SELinux:

# getsebool httpd_can_network_connect

Если она выключена, то функции обновления Drupal скорее всего не будут работать. Включить (-P - на постоянной основе):

# setsebool -P httpd_can_network_connect on

Конфигурация PHP

Мы не трогали /etc/php.ini, а в нем желательно бы кое-что подправить. Согласно все той же странице системных требований, вносим изменения:

expose_php = off
allow_url_fopen = off

Остальные перечисленные параметры или совпадают с рекомендованными значениями, либо вовсе отсутствуют.

По желанию можно также скорректировать настройки ограничения памяти, размера POST-запроса, а также количества и размера загружаемых файлов (ниже приведены значения из php.ini).

memory_limit = 128M
post_max_size = 8M
upload_max_filesize = 2M
max_file_uploads = 20

После внесения изменений в конфигурацию необходимо перезапустить сервис php-fpm.

Конфигурация Caddy

Поскольку в Drupal 7 index.php располагается прямо в корне, во избежание всяких неприятностей он поставляется с огромным файлом .htaccess (для Apache). С этой точки зрения, конечно, лучше Drupal так и использовать с Apache, но теперь наша задача попытаться все это дело воспроизвести.

Первый важный для нас фрагмент:

# Protect files and directories from prying eyes.
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$">
  <IfModule mod_authz_core.c>
    Require all denied
  </IfModule>
  <IfModule !mod_authz_core.c>
    Order allow,deny
  </IfModule>
</FilesMatch>

Лютейшая регулярка, можно воспользоваться сервисом regex101.com, чтобы все равно ничего в ней не понять (хотя вообще-то она проверяет различные расширения файлов или некоторые файлы и директории в корне). Да еще и несовместимая с языком RE2, используемом в Caddy - (?!well-known) поддерживается в PCRE, но не в RE2. Смысл в том, чтобы файл (путь) начинался с точки, кроме .well-known. В принципе у нас такие URL (пока?) не используются, так что предлагаю просто исключить несовместимый фрагмент из выражения. Еще надо добавить / (для Caddy не надо экранировать) в случае, когда регулярка сравнивает начало строки.

https://caddyserver.com/docs/caddyfile/matchers#path-regexp

    # Protect files and directories from prying eyes.
    @protect_files {
        path_regexp \.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^/(\..*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^/#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$
    }
    error @protect_files 403

Честно говоря, я бы еще дополнительно поставил запрет на php-файлы (они иногда попадаются, помимо корня).

    # Deny acces to .php files outside root directory
    @outer_php {
        path_regexp ^/.*/.*\.php$
    }
    error @outer_php 403

Честно говоря, а дальше-то уже объявлениями file_server и php_fastcgi все сделано. В принципе можно насыпать “ведро компрессии”:

    encode gzip

Стоит ли “конвертировать” mod_expires? Казалось бы аналогичный not path_regexp \.php$ работает некорректно (появился двойной заголовок Cache-Control у самих документов). Можно явно перечислить расширения статических файлов:

https://caddy.community/t/correct-way-to-set-expires-on-caddy-2/7914/8

    # Cache static files for 2 weeks after access
    @static {
        path_regexp \.(ico|css|js|gif|jpg|jpeg|png|svg|woff)$
    }
    header @static Cache-Control max-age=1209600

Но в документации предлагается другой способ, который я и оставлю:

    header ?Cache-Control "max-age=1209600"

Смысл в том, чтобы добавить этот заголовок, если “бэкенд” его не установил. А для документов (страниц) его устанавливает Drupal - что и требуется. Честно говоря, я бы уменьшил срок с двух недель до 1-2 часов, хотя тут уже надо смотреть на посещаемость и частоту обновления сайта.

Заголовок X-Content-Type-Options nosniff вроде и так есть, насчет unset Proxy…

    header -Proxy

Вот только как проверить? Заодно можно исключить Server (чтобы никто не догадался, что у нас Caddy):

    header -Server

Итого конфигурация:

http:// {
    # Set this path to your site's directory.
    root * /var/www/html

    header -Proxy
    header -Server
    header ?Cache-Control "max-age=1209600"

    # Protect files and directories from prying eyes.
    @protect_files {
        path_regexp \.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^/(\..*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^/#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$
    }
    error @protect_files 403

    # Deny acces to .php files outside root directory
    @outer_php {
        path_regexp ^/.*/.*\.php$
    }
    error @outer_php 403

    # Enable the static file server.
    encode gzip
    file_server

    # Serve a PHP site through php-fpm
    php_fastcgi unix//run/php-fpm/www.sock
}

Let’s Encrypt

После всех проверок можно воспользоваться функцией автоматического https. Для этого в начало /etc/caddy/Caddyfile добавляем глобальный блок с указанием электронной почты:

{
    email admin@example.com
}

После чего вместо http:// указываем домен сайта. Все :)

API

По умолчанию у Caddy есть API на 2019-м порту. Благодаря этому, в частности, работает caddy reload. Поскольку у нас работает firewalld, в принципе можно так все и оставить (порт извне недоступен). Но если ничего такого нет, то лучше переключить прослушивание API на сокет или отключить вовсе.

В глобальном блоке, для прослушивания сокета:

    admin unix//run/caddy-admin.sock

Для полного отключения:

    admin off

Но в этом случае после изменения конфигурации надо будет именно перезапускать сервис.

OPcache

Посмотрим, удастся ли выжать больше производительности.

Установим расширение:

# dnf install php-opcache

В документации Rocky Linux рекомендуют скорректировать параметры в /etc/php.d/10-opcache.ini: потребление памяти - общее и буфер неких “интернированных” строк, и количество ускоряемых файлов. Последнее значение предложено сопоставить с общим количеством php-файлов, но из-за специфической структуры Drupal подсчитать их довольно сложно. Вот значения по умолчанию:

opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000

Я бы только убавил первый параметр, все-таки оперативки у нас “ограничено очень”.

Честно говоря, по ощущениям не то, чтобы ускорилось - основной “тормоз” все же СУБД. Можно было бы пойти дальше и включить JIT - для этого надо установить размер соответствующего буфера (например 8 мегабайт):

; The amount of shared memory to reserve for compiled JIT code. A zero value disables the JIT.
; When an int is used, the value is measured in bytes. Shorthand notation may also be used. 
opcache.jit_buffer_size=8M

Да вот только сайт после этого перестает работать (502 Bad Gateway). На всякий случай оставил для потомков. https://sergeymukhin.com/blog/php-8-kak-vklyucit-jit

Как обычно, после изменения конфигурации systemctl restart php-fpm.

Доступ по FTP

Возможно заливать обновления самого Drupal или какие-то правки будет удобнее по FTP. Установим “стандартный” сервер ProFTPD (из EPEL):

# dnf install proftpd proftpd-utils

Создадим FTP-пользователя www-data, соответствующего системному пользователю apache (чтобы всех запутать). Сначала выясним uid и gid системного пользователя - это 48.

[root@vm876632 ~]# cat /etc/passwd | grep apache
apache:x:48:48:Apache:/usr/share/httpd:/sbin/nologin

Подставим соответствующие параметры в команду:

# ftpasswd --passwd --file=/etc/proftpd/ftpd.passwd --name=www-data --uid=48 --gid=48 --home=/var/www/html --shell=/bin/false

Поскольку указали параметр --passwd, будет запрошен ввод пароля.

Правим основной файл /etc/proftpd.conf.

Существующие параметры закомментировать/удалить:

  • AuthPAMConfig
  • AuthOrder

“Рядом” добавить:

AuthUserFile /etc/proftpd/ftpd.passwd
AuthPAM off
AuthOrder mod_auth_file.c

Тем самым переключаем режим аутентификации на пользователей из файла. В секцию <Global> добавить:

  RequireValidShell off

Выше мы указали “неправильную” оболочку /bin/false, так что теперь придется ее разрешить.

Добавляем FTP в firewall:

# firewall-cmd --permanent --add-service=ftp
# firewall-cmd --reload

Разрешаем доступ к файлам сервису FTP на уровне SELinux:

# setsebool -P ftpd_full_access on

И только теперь можем стартовать:

# systemctl enable --now proftpd

По хорошему надо бы еще настроить пассивный режим, но уже и так что-то с FTP провозились. Так что ну его.

Заключение

В принципе, и на 500 “метрах” оперативки жить можно. Если страница есть в кэше Друпала, то она даже довольно быстро “выплевывается”. Под пользователем или “на холодную” это все-таки в районе секунды “±километр”.

Дальнейшие направления оптимизации - использование более легковесной ОС (например популярной в Docker-среде Alpine Linux) или перевод Drupal на SQLite. Или не на Друпал. :)