РазделВеб-стек
ДокументацияРуководство

Веб-стек: nginx + php-fpm и nginx + Apache

1. Режимы сайта

Режим A — nginx → php-fpm (по умолчанию)

клиент ──HTTPS (h2/h3)──▶ nginx ──статика напрямую
                              └─ *.php ──FastCGI unix:/run/monopanel/php/<domain>.sock──▶ php-fpm pool (user: <user>)

Режим B — nginx → Apache → php-fpm

клиент ──HTTPS──▶ nginx ──статика напрямую (по расширениям, отключаемо)
                      └─ остальное ──proxy_pass http://127.0.0.1:8080──▶ apache (mpm_event, .htaccess)
                                                                        └─ *.php ──mod_proxy_fcgi──▶ тот же php-fpm pool

Переключение режима — одно поле sites.mode; пул FPM, сокет, пользователь и php.ini не меняются. Apache слушает только loopback (127.0.0.1:8080; для multi-IP серверов тоже loopback — nginx передаёт Host). Реальный IP клиента в Apache — mod_remoteip (RemoteIPHeader X-Real-IP, RemoteIPInternalProxy 127.0.0.1).

Ограничение режима B: директивы php_value / php_flag в .htaccess работают только с mod_php, который не поставляется. Замена — настройки PHP в панели (php_admin_value в пуле) и .user.ini в docroot (FPM читает его благодаря user_ini.filename). Подсказка об этом при выборе режима и поиск php_*-директив в .htaccess с предложением перенести их в панель — планируются.

2. Файловая структура на сервере

/var/www/                                0711 root:root
/var/www/<user>/                         0710 <user>:<user>  + ACL g:monopanel-web:x   (только проход)
└── data/                                0750
    ├── www/<domain>/                    docroot; default ACL g:monopanel-web:rX
    ├── logs/                            access/error/php-error/php-slow по сайтам; ACL g:monopanel-web:x
    ├── tmp/                             0700; upload_tmp_dir / session.save_path / sys_temp_dir
    ├── bin/php -> /opt/monopanel/php/8.4/bin/php
    └── .ssh/ .composer/ .wp-cli/ …

Группа monopanel-web = { nginx, www-data | apache }. Веб-серверы читают статику через ACL, сокеты FPM создаются с listen.group = monopanel-web, listen.mode = 0660. Клиенты не видят друг друга. Файлы, которые создаёт PHP (uploads, cache), принадлежат клиенту и наследуют default ACL — ручной chown/chmod 777 не нужен.

Логи сайтов ротирует /etc/logrotate.d/monopanel-sites: по блоку на аккаунт с su <логин> <логин>. Каталог логов принадлежит клиенту, и что бы он туда ни положил, logrotate действует с его правами, а не с правами root. Ротация раз в неделю или на ежедневном прогоне, если лог перевалил за 100 МБ; хранится восемь копий (example.com.access.log.1, .2.gz …), сжатие начиная со второй.

Все логи сайта принадлежат аккаунту и имеют права 0660. Панель создаёт их при применении сайта — до того, как их откроют nginx, Apache и php-fpm; уже созданные передаёт аккаунту через агент (по открытому файлу, а не по имени), а после ротации create 0660 делает новые такими же. Это нужно logrotate до 3.19 (EL9): ротируя, он открывает лог, а сжимая — ещё и на запись, и файл, который nginx создал от root, или slow-лог, который php-fpm создаёт с правами 0600, аккаунт открыть не смог бы. Сам nginx, переоткрывая лог, меняет только владельца, а php-fpm оставляет найденный slow-лог как есть.

После ротации nginx получает USR1, Apache — graceful restart, оба через pid-файлы: спрашивать systemd из logrotate SELinux не разрешает. Новые файлы после USR1 открывают рабочие процессы nginx, а не мастер, поэтому у группы monopanel-web есть проход через data/logs (ACL x); без него рабочие продолжали бы писать в переименованный файл — и после ночной ротации самого nginx тоже. Журнал ошибок PHP и slow-лог php-fpm открывают файл заново на каждую запись, им сигнал не нужен.

Трассировку в slow-лог php-fpm снимает через ptrace рабочего процесса; на EL это разрешает модуль политики панели (02), без него slow-логи оставались пустыми.

Конфиги:

/etc/nginx/nginx.conf                          шаблон панели (worker_processes auto, http{} с include)
/etc/nginx/monopanel/http.d/*.conf             глобальные: ssl defaults, gzip/brotli, maps, limit zones, log format,
                                               ip-<ip>.conf — default-server на IP (acme, quic reuseport, 444 чужим именам)
/etc/nginx/monopanel/snippets/*.conf           фрагменты: fastcgi, static-cache, deny-dotfiles, acme, ssl, proxy-apache
/etc/nginx/monopanel/sites/<domain>.conf       сгенерированный server{} сайта
/etc/nginx/monopanel/sites/<domain>.d/*.conf   пользовательские include внутри server{} — не перезаписываются
/etc/nginx/conf.d/                             свободная зона администратора (панель не трогает)
/etc/apache2|httpd/monopanel/httpd.conf        Listen 127.0.0.1:8080, mpm_event, remoteip, глобальные настройки
/etc/apache2|httpd/monopanel/sites/<domain>.conf
/etc/apache2|httpd/monopanel/sites/<domain>.d/*.conf
(подключается одной строкой Include из conf-enabled/ или conf.d/)

пул php-fpm: /etc/monopanel/php/X.Y/pool.d/<domain>.conf  (или раскладка Sury/Remi — см. 02 §5)

3. Шаблон nginx (режим A, сокращённо)

# /etc/nginx/monopanel/sites/example.com.conf — generated by MonoPanel; правки в example.com.d/
server {
    listen 203.0.113.10:80;
    server_name example.com www.example.com;
    include monopanel/snippets/acme.conf;              # /.well-known/acme-challenge/ → общий webroot
    {{if redirect_https}}return 301 https://$host$request_uri;{{else}}# …тело, как в 443{{end}}
}
server {
    listen 203.0.113.10:443 ssl;
    http2 on;
    {{if http3}}listen 203.0.113.10:443 quic;{{end}}  # reuseport — в default-server на этот IP
    server_name example.com www.example.com;
    ssl_certificate     /var/lib/monopanel/certs/example.com/fullchain.pem;
    ssl_certificate_key /var/lib/monopanel/certs/example.com/privkey.pem;
    include monopanel/snippets/ssl.conf;               # TLS 1.2/1.3, ciphers, stapling, session cache, HSTS (флаг)
    {{if http3}}add_header Alt-Svc 'h3=":443"; ma=86400';{{end}}
    {{if redirect_www}}# if ($host = www.example.com) { return 301 … }{{end}}

    root  /var/www/alex/data/www/example.com;
    index index.php index.html;
    access_log /var/www/alex/data/logs/example.com.access.log main;
    error_log  /var/www/alex/data/logs/example.com.error.log;
    client_max_body_size 64m;                          # = post_max_size пула
    disable_symlinks if_not_owner from=$document_root;

    include monopanel/snippets/deny-dotfiles.conf;     # .git, .env, .user.ini, .htaccess, composer.*
    include monopanel/snippets/static-cache.conf;      # expires для картинок/шрифтов/js/css
    include monopanel/sites/example.com.d/*.conf;      # пользовательские location — раньше общих

    location / { try_files $uri $uri/ /index.php?$args; }
    location ~ \.php$ {
        try_files $uri =404;
        include monopanel/snippets/fastcgi.conf;       # fastcgi_params, буферы, таймауты = request_terminate_timeout
        fastcgi_pass unix:/run/monopanel/php/example.com.sock;
    }
}

Режим B отличается блоками location: статика по regex расширений отдаётся nginx (переключатель static_by_nginx), всё остальное — proxy_pass http://127.0.0.1:8080 со сниппетом proxy-apache.conf (proxy_http_version 1.1, Host, X-Real-IP, X-Forwarded-Proto, X-Forwarded-For, буферы, таймауты).

HTTP/3: listen … quic reuseport допускается один раз на IP:порт, поэтому панель держит на каждый IP отдельный default-server (http.d/ip-<ip>.conf) с reuseport, а сайты объявляют quic без него. Он же принимает ACME-проверки и закрывает соединения к неизвестным доменам — об этом абзац ниже.

Сервер по умолчанию на каждом адресе (http.d/ip-<адрес>.conf) принимает ACME-проверки, имя самой панели без порта перенаправляет на её HTTPS-порт (http://panel.example.com/ → https://panel.example.com:8443/), а всё остальное закрывает без ответа (444). Если у имени панели есть свой сайт, побеждает сайт: точное server_name в nginx старше блока по умолчанию. Файлы перегенерируются при старте API, так что смена web.hostname доезжает до nginx после --restart.

4. Шаблон Apache (режим B)

<VirtualHost 127.0.0.1:8080>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/alex/data/www/example.com
    <Directory /var/www/alex/data/www/example.com>
        AllowOverride All
        Options -Indexes +SymLinksIfOwnerMatch
        Require all granted
    </Directory>
    <FilesMatch "\.php$">
        SetHandler "proxy:unix:/run/monopanel/php/example.com.sock|fcgi://localhost"
    </FilesMatch>
    ProxyTimeout 150
    RemoteIPHeader X-Real-IP
    RemoteIPInternalProxy 127.0.0.1
    ErrorLog  /var/www/alex/data/logs/example.com.apache.error.log
    CustomLog /var/www/alex/data/logs/example.com.apache.access.log combined
    IncludeOptional /etc/apache2/monopanel/sites/example.com.d/*.conf
</VirtualHost>

Модули: mpm_event, proxy, proxy_fcgi, rewrite, remoteip, headers, expires, setenvif, dir, alias, deflate, env, mime, authz_core, autoindex(off). mpm_prefork и любые php* — выключены. ProxyTimeout = request_terminate_timeout пула.

5. Шаблон пула php-fpm

[example.com]
user  = alex
group = alex
listen = /run/monopanel/php/example.com.sock
listen.owner = alex
listen.group = monopanel-web
listen.mode  = 0660
pm = ondemand                    ; ondemand (по умолчанию) | dynamic | static
pm.max_children = 8
pm.process_idle_timeout = 10s
pm.max_requests = 500
request_terminate_timeout = 150s ; max_execution_time + 30
slowlog = /var/www/alex/data/logs/example.com.php.slow.log
request_slowlog_timeout = 10s
catch_workers_output = yes
clear_env = no                   ; PATH/HOME нужны composer/wp-cli
env[PATH]   = /var/www/alex/data/bin:/usr/local/bin:/usr/bin:/bin
env[TMPDIR] = /var/www/alex/data/tmp
php_admin_value[open_basedir]      = /var/www/alex/data
php_admin_value[upload_tmp_dir]    = /var/www/alex/data/tmp
php_admin_value[session.save_path] = /var/www/alex/data/tmp/sess
php_admin_value[sys_temp_dir]      = /var/www/alex/data/tmp
php_admin_value[error_log]         = /var/www/alex/data/logs/example.com.php.error.log
php_admin_value[sendmail_path]     = /usr/sbin/sendmail -t -i -f noreply@example.com
php_admin_value[disable_functions] = passthru,shell_exec,system,proc_open,popen,pcntl_exec,pcntl_fork
; редактируются из UI/CLI; php_value допускает переопределение в .user.ini
php_value[memory_limit]         = 256M
php_value[upload_max_filesize]  = 64M
php_value[post_max_size]        = 64M
php_value[max_execution_time]   = 120
php_value[date.timezone]        = Europe/Moscow
php_value[display_errors]       = Off
php_value[short_open_tag]       = Off   ; пресет bitrix — On
php_value[opcache.enable]       = 1

disable_functions по умолчанию перечислены выше; переключатель allow_exec на сайт снимает их (WP-CLI-хуки, очереди Laravel, ImageMagick через CLI).

PHP-сессии по умолчанию — файлы в tmp/sess аккаунта. Сайт можно перевести в экземпляр Valkey sessions своего аккаунта (mp site set <домен> --sessions valkey, поле «PHP-сессии» в настройках сайта), и тогда вместо session.save_path на файлы пул получает:

php_admin_value[session.save_handler] = redis
php_admin_value[session.save_path]    = "unix:///run/monopanel-valkey/alex-sessions/valkey.sock"

Нужно расширение redis у ветки PHP сайта. Сокет с правами 600 принадлежит аккаунту: пул сайта работает от его имени и подключается, пулы других аккаунтов — нет (02).

pm.max_children по умолчанию 8; пресету bitrix — по воркеру на 256 МБ RAM, от 8 до 48 (число выбирается, когда сайт получает пресет: при создании, смене пресета или переносе). Причина в том, что шаблоны Битрикса ходят по HTTP на свой же сайт прямо из запроса, и восемь занятых воркеров ждут друг друга. Задать max_children вручную можно для любого сайта. При pm=ondemand неактивные сайты не держат воркеров — это основной режим для общего сервера с сотнями сайтов; нагруженным сайтам вручную ставят dynamic или static.

Глобальный 99-monopanel.ini ветки держит short_open_tag = Off, но копия, которую читает CLI, разрешает короткие теги, пока на ветке есть сайт с пресетом bitrix: cron-скрипты и пролог Битрикса начинаются с <?, и иначе PHP печатал бы их как текст с кодом 0. Пулы задают short_open_tag сами, поэтому на Remi, где CLI и FPM читают один каталог, сайтам это не мешает.

6. Изоляция и лимиты

УровеньМеханизмСостояние
Файлыunix-пользователь на клиента, 0710, ACL для веб-группы, open_basedir, отдельный tmp/sessionесть, включено
Процессы PHPпул FPM от пользователя, disable_functions, pm.max_childrenесть, включено
Symlinkdisable_symlinks if_not_owner (nginx), SymLinksIfOwnerMatch (Apache)есть, включено
Дискквоты файловой системы на uid (quota / xfs_quota); без квот — учёт по du в метрикахпланируется
CPU/RAM на сайтизолированный пул: отдельный мастер monopanel-php-fpm@<X.Y>-<domain>.service в Slice=monopanel-<user>.slice с MemoryMax, CPUQuota, TasksMax, PrivateTmp, ProtectSystem=strict, ReadWritePaths=/var/www/<user>планируется
SSHshell только по флагу; SFTP-only chroot (Match Group monopanel-sftp)есть, по умолчанию только SFTP
Сетьисходящие соединения PHP не ограничиваются (API, платежи); запрет исходящего SMTP через nft-правило на uid (meta skuid)планируется

7. TLS / ACME

  • Клиент — lego как библиотека внутри monopaneld api: аккаунт, ключи, challenge-и, продление.
  • HTTP-01: общий webroot /var/lib/monopanel/acme/webroot, подключён сниппетом acme.conf во всех :80 server{} и в default-server каждого IP → выпуск работает и до создания сайта, и при редиректе на HTTPS.
  • DNS-01: провайдеры lego — Cloudflare, Hetzner, DigitalOcean, Gandi, deSEC, Namecheap и RFC2136 (mp dns-provider types) — для wildcard *.example.com; учётные данные хранятся зашифрованными, у каждого пользователя свои.
  • Ключи: ECDSA P-256 по умолчанию, RSA-2048 по флагу; двойной сертификат (EC + RSA) — планируется.
  • Хранение: /var/lib/monopanel/certs/<domain>/{fullchain.pem,privkey.pem,chain.pem} 0600 root (мастер-процессы nginx/Apache работают от root). Импорт своих сертификатов (PEM) — через UI/CLI/API; один сертификат может обслуживать несколько сайтов (SAN).
  • Продление: планировщик раз в сутки, за 30 дней до истечения; при неудаче — повторы с растущим интервалом, ошибка видна в mp doctor и уходит webhook-событием cert.issue.failed; после успеха — ApplyConfigSet только с reload nginx (и Apache при необходимости).
  • Нативный ngx_http_acme_module (nginx 1.29+) вместо lego для HTTP-01 — возможный вариант на будущее.
  • Хост панели: сертификат для web.hostname тем же механизмом (mp ssl issue <hostname>); панель подхватывает его на лету через tls.Config.GetCertificate, до его получения — self-signed с отпечатком в выводе mp setup. Текущее состояние — mp web tls / GET /web/tls.
  • Реализовано (этап 0): internal/acme (аккаунты в <data>/acme/accounts/<directory>/<email>/, сертификаты в <data>/certs/<name>/), задача cert.issue с проверкой публичного DNS и probe webroot через nginx, планировщик продления в API-процессе.
  • Директория ACME настраивается (Let's Encrypt, ZeroSSL, Buypass, внутренний Step-CA).
  • Сертификат панели и сертификаты сайтов разведены (2026-09-10). Сертификат панели — запись с именем web.hostname: GET /ssl/panel показывает, что панель отдаёт сейчас и что за этим стоит (заказ в работе, ошибка), POST /ssl/panel/issue заказывает ровно для этого имени (HTTP-01 или dns для DNS-01), POST /ssl/panel/import ставит готовый, DELETE /ssl/panel возвращает самоподписанный.
  • Сертификат сайта заказывается из сайта: POST /sites/{domain}/tls/issue берёт домен и алиасы, переключает сайт на ssl: auto, и задача выпуска сама переприменяет сайт с HTTPS. В списке /certificates у каждого сертификата есть used_by_panel и used_by_sites; занятый сертификат удалить нельзя (409). Сертификат, который панель делит с одноимённым сайтом (панель на example.com и сайт example.com), — законный случай: он числится за обоими. Страница SSL в Web UI показывает три блока: панель, сайты, неиспользуемые.

8. Логи и ротация

  • Ротация — /etc/logrotate.d/monopanel-sites, блок на аккаунт с su <логин> <логин>: раз в неделю или раньше, если лог перевалил за 100 МБ, восемь копий, сжатие со второй; после ротации nginx получает USR1, Apache — graceful restart, оба через pid-файлы. Подробно — в разделе 2.
  • Формат main пишет $request_time, $upstream_response_time, $host и $server_protocol. Разбор access-логов для метрик по сайтам (инкрементальный, с запоминанием позиции в файле) — планируется.
  • В UI — хвост логов access/error/php-error/php-slow (вкладка «Логи» сайта, mp site logs); слежение за логом вживую и фильтры по коду ответа и пути — планируются.

9. Дополнительные веб-функции (флаги сайта, шаблоны)

  • Редиректы: http→https, www↔non-www; произвольные 301/302 по пути — через свои директивы в <domain>.d/ или mp site nginx.
  • Алиасы сайта (общий docroot) и отдельные сайты на поддоменах.
  • Свой docroot внутри data/www/<domain>/ (например /public для Laravel и Symfony).
  • Доступ только с перечисленных адресов (--allow); ограничение частоты запросов с одного адреса (--rate-limit N — limit_req на динамические страницы, статика не считается, лишним — 429); заголовки безопасности (--security-headers). Basic-auth на путь и блокировка по странам (geoip2 как динамический модуль) — планируются.
  • IPv6: сайт слушает и на IPv6-адресе сервера (--ipv6; по умолчанию первый глобальный, none — только IPv4), у каждого адреса свой default-server. Адрес, который приходит от маршрутизатора (SLAAC) или по DHCPv6, появляется на интерфейсе позже, чем стартует nginx, и без подготовки nginx при загрузке падал с «Cannot assign requested address». Поэтому панель ставит net.ipv6.ip_nonlocal_bind = 1 (/etc/sysctl.d/90-monopanel.conf) и добавляет юниту nginx drop-in с Restart=on-failure; default-server IPv6-адреса в первые минуты после загрузки не считается устаревшим.
  • Кеш статики в браузере, gzip; brotli как динамический модуль собственной сборки — планируется.
  • Заглушка «сайт приостановлен» при status=suspended: конфиг заменяется на минимальный с 503-страницей, пул FPM удаляется из конфига мастера.
  • Режим proxy — сайт отдаётся своему приложению (Node, Python, Docker): --mode proxy --backend http://127.0.0.1:3000; фоновое приложение можно держать под systemd как app-сервис (mp app).

10. Установка CMS

mp cms install <домен> <cms> (и вкладка «CMS» на странице сайта) ставит WordPress, Joomla, OpenCart или 1С-Битрикс в существующий сайт так, как это документирует производитель, только без кликов:

  1. Сайт получает пресет CMS, если он другой, и конфигурация применяется.
  2. Docroot должен быть пуст; с --force его содержимое удаляется (от имени клиента).
  3. Создаётся база <login>_<cms> (при повторной установке — <login>_<cms>2) с отдельным пользователем и сгенерированным паролем.
  4. Дистрибутив скачивается у производителя (WordPress — wordpress.org, Joomla и OpenCart — последний релиз на GitHub без API, Битрикс — пробная редакция с 1c-bitrix.ru) и потоком, без буфера в памяти, распаковывается агентом через tar прямо в docroot (zip OpenCart перекодируется в tar на лету); файлы отдаются клиенту, метки SELinux восстанавливаются.
  5. Установщик CMS запускается от имени клиента через fsop run — он исполняет только интерпретатор PHP (ветка сайта) внутри домашнего каталога: wp-cli (/usr/local/lib/monopanel/wp-cli.phar, проверяется по опубликованной sha512), installation/joomla.php install, install/cli_install.php. У Битрикса CLI-установщика нет: панель проходит его веб-мастер по HTTP через сам сайт (имя сайта резолвится в его адрес), включая AJAX-шаги установки модулей, обновлений и решения, и пробную регистрацию на 1c-bitrix.ru от имени администратора сайта. Редакция (--edition): пробные start, standard, small_business, business с 1c-bitrix.ru; лицензионный ключ вводится потом в настройках Битрикса, и редакция должна совпадать с ключом. Решение (--solution): по умолчанию clean — «Чистая установка «1С-Битрикс»» из Маркетплейса (пустой сайт со стандартными модулями, без демо-контента), demo — демо-сайт, встроенный в дистрибутив, или id любого решения из Маркетплейса (vendor.solution), которое мастер скачает и поставит; мастер решения панель проходит так же, как основной.
  6. В записи сайта остаются CMS, версия и время установки; пароль администратора возвращается один раз в ответе API и не попадает в журнал задачи (в payload он зашифрован ключом панели).

Пароль администратора — 12–20 символов, чтобы устроить все четыре CMS сразу; без него генерируется 16-символьный. E-mail — владельца сайта, иначе admin@<домен>.

Исходник страницы на GitHub