====== Настройка PHP-FPM и Apache в Debian 12 ====== {{:linux:apache:debian_apache_fpm.png?nolink|}} Исходные данные * Debian 12 (Bookworm) * PHP из репозитория [[https://deb.sury.org/|Sury]] * Apache2 + [[https://httpd.apache.org/docs/2.4/mod/event.html|MPM Event]] Задача * 2 виртуальных хоста с PHP 8.1 и 8.2 с отдельными fpm-pool с отдельными пользователями * Сайт 1: пользователь batman, директория /srv/www/batman * Сайт 2: пользователь joker, директория /srv/www/joker * Ioncube для PHP 8.1 * http2 * SSL Let's Encrypt через модуль mod_md * realip для Cloudflare ===== Установка Apache ===== apt -y install apache2 Добавить в файл ''/etc/apache2/apache2.conf'' ServerName 127.0.0.1 У меня в Debian 12 по-умолчанию был **mpm_event**. Для других релизов Debian или для Ubuntu может быть потребуется выключить другие MPM и включить event a2dismod mpm_prefork a2dismod mpm_itk a2enmod mpm_event ===== SSL/TLS ===== [[linux:apache:mod_md|mod_md: сертификаты Let's Encrypt]] ===== http2 ===== Проверка через curl http/1.1 # curl -I https://foobar.com HTTP/1.1 200 OK Date: Thu, 10 Aug 2023 14:48:56 GMT Server: Apache/2.4.57 (Debian) Strict-Transport-Security: max-age=15768000 Last-Modified: Sat, 22 Jul 2023 17:39:30 GMT ETag: "c-60116dd1dd190" Accept-Ranges: bytes Content-Length: 12 Content-Type: text/html Включаем модуль http2 # a2enmod http2 Enabling module http2. To activate the new configuration, you need to run: systemctl restart apache2 В VirtualHost добавляем Protocols h2 http/1.1 Проверка через curl http/2 # curl -I https://foobar.com HTTP/2 200 strict-transport-security: max-age=15768000 last-modified: Sat, 22 Jul 2023 17:39:30 GMT etag: "c-60116dd1dd190" accept-ranges: bytes content-length: 12 content-type: text/html date: Thu, 10 Aug 2023 14:49:41 GMT server: Apache/2.4.57 (Debian) ===== Установка PHP ===== Следуем инструкции из [[https://packages.sury.org/php/README.txt|readme.txt]] wget -O sury.sh https://packages.sury.org/php/README.txt chmod +x sury.sh sh sury.sh Устанавливаем PHP 8.1 apt install -y php8.1-bcmath php8.2-bz2 php8.1-curl php8.1-fpm php8.1-gd php8.1-intl php8.1-mbstring php8.1-mcrypt php8.1-mysql php8.1-opcache php8.1-xml php8.1-xmlrpc php8.1-zip В конце установки будет предупреждение NOTICE: Not enabling PHP 8.1 FPM by default. NOTICE: To enable PHP 8.1 FPM in Apache2 do: NOTICE: a2enmod proxy_fcgi setenvif NOTICE: a2enconf php8.1-fpm NOTICE: You are seeing this message because you have apache2 package installed. Устанавливаем PHP 8.2 apt install -y php8.2-bcmath php8.2-bz2 php8.2-curl php8.2-fpm php8.2-gd php8.2-intl php8.2-mbstring php8.2-mcrypt php8.2-mysql php8.2-opcache php8.2-xml php8.2-xmlrpc php8.2-zip В конце установки будет предупреждение NOTICE: Not enabling PHP 8.2 FPM by default. NOTICE: To enable PHP 8.2 FPM in Apache2 do: NOTICE: a2enmod proxy_fcgi setenvif NOTICE: a2enconf php8.2-fpm NOTICE: You are seeing this message because you have apache2 package installed. ===== PHP-FPM ===== Проверяем статус php-fpm systemctl status php8.1-fpm systemctl status php8.2-fpm Для работы php-fpm в Apache нужны 2 модуля * [[https://httpd.apache.org/docs/2.4/mod/mod_proxy_fcgi.html|Apache Module mod_proxy_fcgi]] * [[https://httpd.apache.org/docs/2.4/mod/mod_setenvif.html|Apache Module mod_setenvif]] Включаем модуль **proxy_fcgi** и **setenvif** # a2enmod proxy_fcgi setenvif Considering dependency proxy for proxy_fcgi: Enabling module proxy. Enabling module proxy_fcgi. Module setenvif already enabled To activate the new configuration, you need to run: systemctl restart apache2 Для обработки PHP кода добавляем в VirtualHost Для 8.1 # # расширенный FilesMatch для .phar, phtml SetHandler "proxy:unix:/var/run/php/php8.1-fpm.sock|fcgi://localhost/" Для 8.2 SetHandler "proxy:unix:/var/run/php/php8.2-fpm.sock|fcgi://localhost/" Создаём пользователей и директории для PHP 8.1 и 8.2 useradd batman passwd batman useradd joker passwd joker mkdir -p /srv/www/batman mkdir -p /srv/www/joker chown batman:batman /srv/www/batman chown joker:joker /srv/www/joker Для дополнительной безопасности при создании пользователя можно добавить ''-s /bin/false'' или ''/usr/sbin/nologin'' в качестве шелла. Но из моего опыта 99% людей этого не делает т.к. надо или разворачивать git или придумывать ещё какие-то велосипеды, чтобы загружать/редактировать файлы сайта. Так что каждый решает сам, как лучше. Можно настроить авторизацию по ключам и уже будет лучше. Есть ещё вариант с [[https://www.flokoe.de/posts/mastering-acls-once-and-for-all/|ACL (setfacl)]]. Но это всё частные случаи. Деплой только через git. К сожалению я и сейчас часто наблюдаю внесение правок на корпоративные сайты Битрикс через FTP. ССЗБ :) Создаём файлы для вывода phpinfo(); echo '' > /srv/www/batman/info.php echo '' > /srv/www/joker/info.php Создаём виртуальные хосты с именами ''batman.conf'' и ''joker.conf'' в ''/etc/apache2/sites-available/'' Файл ''/etc/apache2/sites-available/batman.conf'' Protocols h2 http/1.1 DocumentRoot /srv/www/batman ServerAdmin admin@foobar.com ServerName batman.foobar.com ServerAlias bruce.foobar.com CustomLog "/var/log/apache2/batman_access.log" combined ErrorLog "/var/log/apache2/batman_error_log" # php-fpm handler SetHandler "proxy:unix:/var/run/php/php8.1-fpm.sock|fcgi://localhost" Options -Indexes +FollowSymLinks AllowOverride All Require all granted Файл ''/etc/apache2/sites-available/joker.conf'' Protocols h2 http/1.1 DocumentRoot /srv/www/joker ServerAdmin admin@foobar.com ServerName joker.foobar.com ServerAlias harley.foobar.com CustomLog "/var/log/apache2/joker_access.log" combined ErrorLog "/var/log/apache2/joker_error_log" # php-fpm handler SetHandler "proxy:unix:/var/run/php/php8.2-fpm.sock|fcgi://localhost" Options -Indexes +FollowSymLinks AllowOverride All Require all granted Включаем сайты через **a2ensite** a2ensite batman joker systemctl restart apache2 ===== FPM-POOL ===== Файл ''/etc/php/8.1/fpm/pool.d/batman-pool.conf'' [batman-pool] listen = /var/run/php/php8.1-fpm.sock listen.owner = batman listen.group = batman listen.mode = 0660 user = batman group = batman pm = dynamic Файл ''/etc/php/8.2/fpm/pool.d/joker-pool.conf'' [joker-pool] listen = /var/run/php/php8.2-fpm.sock listen.owner = joker listen.group = joker listen.mode = 0660 user = joker group = joker pm = dynamic Это минимальный конфиг для работы. Добавляем пользователя ''www-data'' в группу ''batman'' и ''joker''. usermod -a -G batman www-data usermod -a -G joker www-data Ещё раз про chown ❌ Неправильно: * www-data:www-data * batman:www-data * www-data:batman ✅ Правильно: * batman:batman * joker:joker Если не добавить в группу будет нечто подобное [proxy:error] [pid 50599:tid 140087278896832] (13)Permission denied: AH02454: FCGI: attempt to connect to Unix domain socket /var/run/php/php8.2-fpm.sock (*:80) failed [proxy_fcgi:error] [pid 50599:tid 140087278896832] [client 192.168.100.100:17782] AH01079: failed to make connection to backend: httpd-UDS На что обратить внимание * Каждый пул должен использовать отдельный сокет. Если несколько пулов используют один и тот же сокет будут проблемы. * Директивы ''user'' и ''group'' задают пользователя/группу, от имени которых будет запускаться процесс FPM. Они __не связаны__ с пользователем/группой для сокета. * Директивы ''listen.owner'' и ''listen.group'' задают пользователя/группу, которую сокет использует для этого пула. * Директивы пула ''listen.*'' работают только для пулов. Их нельзя использовать в глобальном конфиге, вы должны указать их для каждого пула. * Права к сокету 0660 Для примера и в качестве заметки для себя приложу свой 💥 боевой конфиг [rtfm-74] user = rtfm group = rtfm listen = /var/run/php7.4-fpm-rtfm.sock listen.owner = rtfm listen.group = rtfm listen.backlog = 65535 ;;listen.mode = 0660 pm = dynamic pm.max_children = 35 pm.start_servers = 5 pm.min_spare_servers = 1 pm.max_spare_servers = 25 slowlog = /home/rtfm/data/logs/php-fpm_slow.log request_slowlog_timeout = 5s request_terminate_timeout = 300s ;;chdir = / security.limit_extensions = .php .phar catch_workers_output = yes pm.status_path = /fpm-status ping.path = /ping php_admin_value[date.timezone] = UTC php_admin_value[disable_functions] = passthru,shell_exec,system php_admin_value[cgi.fix_pathinfo] = 0 php_admin_value[memory_limit] = 850M php_admin_value[post_max_size] = 100M php_admin_value[upload_max_filesize] = 100M php_admin_value[max_file_uploads] = 35 ;; admin_flags php_admin_flag[expose_php] = off php_admin_flag[display_errors] = off php_admin_flag[display_startup_errors] = off php_admin_flag[log_errors] = on php_admin_flag[allow_url_fopen] = on ;;php_admin_flag[allow_url_include] = off ;;php_admin_flag[file_uploads] = off php_admin_flag[session.cookie_httponly] = on ;;php_admin_flag[session.use_cookies] = on php_admin_flag[session.cookie_secure] = on ;; admin_values sessions php_admin_value[session.cookie_lifetime] = 0 php_admin_value[session.gc_maxlifetime] = 86400 php_admin_value[session.save_handler] = files php_admin_value[session.save_path] = /home/rtfm/data/sessions ;;php_admin_value[session.name] = rtfm ;; admin flag cookie php_admin_flag[session.cookie_httponly] = on php_admin_flag[session.use_cookies] = on php_admin_flag[session.cookie_secure] = on ;; admin values other php_admin_value[error_reporting] = E_ALL & ~E_NOTICE php_admin_value[upload_tmp_dir] = /home/rtfm/data/tmp php_admin_value[open_basedir] = /home/rtfm/data:/foobar ;; logs php_admin_value[mail.log] = /home/rtfm/data/logs/php_mail.log php_admin_value[error_log] = /home/rtfm/data/logs/php-fpm_error.log ;; opcache ;;php_admin_flag[opcache.enable] = 0 ;;php_admin_flag[opcache.enable_cli] = 0 ;;php_admin_flag[opcache.save_comments] = 1 ;;php_admin_flag[opcache.revalidate_freq] = 1 ;;php_admin_flag[opcache.validate_timestamps] = 0 ;;php_admin_flag[opcache.fast_shutdown] = On ;;php_admin_value[opcache.interned_strings_buffer] = 8 ;;php_admin_value[opcache.max_accelerated_files] = 16384 ;;php_admin_value[opcache.memory_consumption] = 128 ;;php_admin_value[opcache.error_log] = "/var/log/php_opcache.log" ;;php_admin_value[opcache.log_verbosity_level] = 1 env[HOSTNAME] = $HOSTNAME env[PATH] = /usr/local/bin:/usr/bin:/bin env[TMP] = /home/rtfm/data/tmp env[TMPDIR] = /home/rtfm/data/tmp env[TEMP] = /home/rtfm/data/tmp ===== phpinfo ===== Проверяем вывод phpinfo по ссылкам batman.foobar.com/info.php и joker.foobar.com/info.php {{:linux:apache:debian_fpm_phpinfo_81.png?nolink|}} {{:linux:apache:debian_fpm_phpinfo_82.png?nolink|}} ===== Проверка FPM-POOL ===== Создаём файл ''uid-check.php'' echo "" > /srv/www/batman/uid-check.php echo "" > /srv/www/joker/uid-check.php В браузере ''uid-check.php'' должен показать следующее uid=1000(batman) gid=1000(batman) groups=1000(batman) uid=1001(joker) gid=1001(joker) groups=1001(joker) ===== FPM status ===== Настраиваем [[https://www.php.net/manual/ru/fpm.status.php|страницу состояния FPM]]. В конфиг пула добавляем pm.status_path = /status ping.path = /ping В конфиг виртуального хоста добавляем SetHandler "proxy:unix:/var/run/php/php8.1-fpm.sock|fcgi://localhost" Alias /realtime-status "/usr/share/php/8.1/fpm/status.html" Формат данных - html, json, openmetrics (какая-то новинка), xml Также стоит добавить в конфиг **Require local** или **Require ip 192.168.100.0/24**, чтобы ограничить доступ к данным. ==== HTML ==== URL * http://foobar.com/status?html * http://foobar.com/status?html&full {{:linux:apache:debian_fpm_status_html.png?nolink|}} {{:linux:apache:debian_fpm_status_html_full.png?nolink|}} ==== JSON ==== URL * http://foobar.com/status?json * http://foobar.com/status?json&full {{:linux:apache:debian_fpm_status_json.png?nolink|}} {{:linux:apache:debian_fpm_status_json_full.png?nolink|}} ==== XML ==== URL * http://foobar.com/status?xml * http://foobar.com/status?xml&full {{:linux:apache:debian_fpm_status_xml.png?nolink|}} {{:linux:apache:debian_fpm_status_xml_full.png?nolink|}} ==== Realtime ==== URL - https://foobar.com/realtime-status (файл ''/usr/share/php/ВЕРСИЯ_PHP/fpm/status.html'') //GIF 400+ КБ// {{:linux:apache:debian_fpm_status_realtime.gif?nolink|}} На странице [[https://www.php.net/manual/ru/function.fpm-get-status.php|fpm_get_status]] увидел ссылку на PHP файл, который якобы будет работать без дополнительной настройки веб сервера - [[https://gist.github.com/EhsanCh/97187902e905a308ce434bda6730073c|PHP-FPM real-time status page (Single file without the need for web server configuration)]]. ===== Cloudflare realip/remoteip ===== Если для домена подключен Cloudflare, то необходима настройка для отображения реальных IP адресов с помощью **mod_remoteip** - [[https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html|Apache Module mod_remoteip]]. Пример настройки есть в статье [[linux:cloudflare_ssl_full_strict|Cloudflare: SSL сертификаты Full Strict]] ===== Ioncube ===== Основная статья [[linux:ioncube|Установка ionCube Loader]], но она немного устарела. Смотрим **extension_dir** для версии PHP 8.1 # php8.1 -i | grep extension_dir extension_dir => /usr/lib/php/20210902 => /usr/lib/php/20210902 Копируем .so файл по указанному выше пути cd ioncube && cp ./ioncube_loader_lin_8.1.so /usr/lib/php/20210902/ioncube_loader_lin_8.1.so Подключаем .so модуль к PHP # echo "; priority=10\nzend_extension=/usr/lib/php/20210902/ioncube_loader_lin_8.1.so" >> /etc/php/8.1/mods-available/ioncube.ini Теперь нужно активировать модуль через **phpenmod**. Т.к. установлено 2 версии PHP ([[https://github.com/oerdnj/deb.sury.org/wiki/Managing-Multiple-Versions#setting-global-defaults|Managing Multiple Versions]]) # update-alternatives --query php Name: php Link: /usr/bin/php Slaves: php.1.gz /usr/share/man/man1/php.1.gz Status: auto Best: /usr/bin/php8.2 Value: /usr/bin/php8.2 Alternative: /usr/bin/php8.1 Priority: 81 Slaves: php.1.gz /usr/share/man/man1/php8.1.1.gz Alternative: /usr/bin/php8.2 Priority: 82 Slaves: php.1.gz /usr/share/man/man1/php8.2.1.gz необходимо указать версию PHP, по-умолчанию phpenmod работает с версией PHP 8.2 phpenmod -v 8.1 ioncube Если не указать ''priority=10'', то будет ошибка NOTICE: PHP message: PHP Fatal error: [ionCube Loader] The Loader must appear as the first entry in the php.ini file in Unknown on line 0 И у ioncube будет приоритет 20 # ls -la /etc/php/8.1/fpm/conf.d/ | grep ioncube lrwxrwxrwx 1 root root 39 Jul 19 12:28 20-ioncube.ini -> /etc/php/8.1/mods-available/ioncube.ini Проверяем, что модуль подключился # php8.1 -v PHP 8.1.21 (cli) (built: Jul 16 2023 11:01:21) (NTS) Copyright (c) The PHP Group Zend Engine v4.1.21, Copyright (c) Zend Technologies with the ionCube PHP Loader v12.0.5, Copyright (c) 2002-2022, by ionCube Ltd. with Zend OPcache v8.1.21, Copyright (c), by Zend Technologies ===== mod_proxy ===== Вроде бы прокси запросы по-умолчанию запрещены, но лучше себя обезопасить дополнительно If you want to use apache2 as a forward proxy, uncomment the\\ # 'ProxyRequests On' line and the block below.\\ # WARNING: Be careful to restrict access inside the block.\\ # Open proxy servers are dangerous both to your network and to the\\ # Internet at large. Добавляем в ''/etc/apache2/mods-enabled/proxy.conf'' Require all denied Require local Require ip 192.168.100.0/24 ===== Дополнительные настройки ===== Включаем **mod_rewrite** - [[https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html|Apache Module mod_rewrite]] a2enmod rewrite Включаем **mod_headers** - [[https://httpd.apache.org/docs/2.4/mod/mod_headers.html|Apache Module mod_headers]] a2enmod headers ===== Ссылки ===== * https://cwiki.apache.org/confluence/display/HTTPD/PrivilegeSeparation * https://cwiki.apache.org/confluence/display/HTTPD/PHP-FPM * https://cwiki.apache.org/confluence/display/HTTPD/PHP * https://tokmakov.msk.ru/blog/item/439 * [[https://serverfault.com/questions/357108/what-permissions-should-my-website-files-folders-have-on-a-linux-webserver|What permissions should my website files/folders have on a Linux webserver?]] * [[https://unix.stackexchange.com/questions/157506/securing-www-directory-in-ubuntu-apache-without-restricting-access-to-those-who|Securing www directory in Ubuntu/Apache without restricting access to those who need it?]] * [[https://serverfault.com/questions/777180/setup-web-folder-for-multiple-developers-and-apache|Setup web folder for multiple developers and Apache]] EOM {{tag>linux debian apache php php-fpm ssl ioncube cloudflare}}