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

Веб-стек: 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). Это документируется в UI при выборе режима; типовые 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  (или layout 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 на свой же сайт из запроса, и восемь занятых воркеров ждут друг друга. Глобальный 99-monopanel.ini ветки держит short_open_tag = Off, но копия, которую читает CLI, открывает короткие теги, пока на ветке есть сайт с пресетом bitrix: cron-скрипты и пролог Битрикса начинаются с <?, и PHP иначе печатает их текстом с кодом 0. Пулы задают short_open_tag сами, поэтому на Remi, где CLI и FPM читают один каталог, сайтам это не мешает. Задать max_children вручную можно для любого сайта; при pm=ondemand неактивные сайты не держат воркеров — основной режим для shared-сервера с сотнями сайтов. Для нагруженных сайтов — dynamic/static вручную.

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

УровеньМеханизмПо умолчанию
Файлыunix-пользователь на клиента, 0710, ACL для веб-группы, open_basedir, отдельный tmp/sessionвкл
Процессы PHPпул FPM от пользователя, disable_functions, pm.max_childrenвкл
Symlinkdisable_symlinks if_not_owner (nginx), SymLinksIfOwnerMatch (Apache)вкл
Дискfilesystem-квоты на uid (quota / xfs_quota); без квот — учёт по du в метрикахопц
CPU/RAM на сайтизолированный пул: отдельный master monopanel-php-fpm@<X.Y>-<domain>.service в Slice=monopanel-<user>.slice с MemoryMax, CPUQuota, TasksMax, PrivateTmp, ProtectSystem=strict, ReadWritePaths=/var/www/<user>опц (v1.1)
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, Route53, Hetzner, Yandex Cloud, DigitalOcean, RFC2136 и др.) → wildcard *.example.com; учётные данные хранятся зашифрованно на пользователя.
  • Ключи: ECDSA P-256 по умолчанию, RSA-2048 по флагу; опционально двойной сертификат (EC + RSA).
  • Хранение: /var/lib/monopanel/certs/<domain>/{fullchain.pem,privkey.pem,chain.pem} 0600 root (master-процессы nginx/Apache работают от root). Импорт своих сертификатов (PEM/PFX) — через UI/CLI/API; сертификат может быть общим для нескольких сайтов (SAN).
  • Продление: планировщик раз в сутки, за 30 дней до истечения; при неудаче — уведомление и повторы с backoff; после успеха — 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.
  • Анализ access-логов для метрик — инкрементальный (позиция файла запоминается), формат main с $request_time, $upstream_response_time, $host, $server_protocol.
  • В UI: просмотр хвоста и follow (SSE) для access/error/php-error/php-slow, фильтр по коду ответа и пути.

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

  • Редиректы: http→https, www↔non-www, произвольные 301/302 по пути, include для сложных правил.
  • Алиасы/поддомены сайта (общий docroot) и отдельные сайты на поддоменах.
  • Кастомный docroot внутри data/www/<domain>/ (например /public для Laravel/Symfony).
  • Basic-auth на путь; блокировка по IP/CIDR/стране (geoip2 как dynamic module, опц.); limit_req на сайт.
  • Кэш статики в браузере, gzip (+ brotli как dynamic module собственной сборки, опц.).
  • Заглушка «сайт приостановлен» при status=suspended: конфиг заменяется на минимальный с 503-страницей, пул FPM удаляется из конфига master.
  • Режим proxy (сайт → произвольный backend: Node/Python/Docker) — roadmap.

Установка 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