====== Настройка 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}}