Коротко: боевая связка PHP — Nginx как фронт и PHP-FPM по FastCGI через unix-сокет; Nginx отдаёт статику, FPM держит пул воркеров.

Боевая связка для PHP-сайта — это Nginx как фронт и PHP-FPM как процесс-менеджер интерпретатора, общающиеся по FastCGI через unix-сокет. Nginx отдаёт статику и проксирует только PHP-запросы в FPM; FPM держит пул воркеров. Разберу минимально-достаточный production-конфиг и типовые грабли.

Почему именно Nginx + PHP-FPM

Nginx прекрасно и дёшево отдаёт статику и держит тысячи соединений, но сам PHP не исполняет. PHP-FPM (FastCGI Process Manager) управляет пулом PHP-процессов. Связка получается такой: статика (css/js/images) — отдаёт Nginx напрямую с диска, а запросы к index.php уходят в FPM. Это быстрее и стабильнее, чем mod_php в Apache.

Конфиг виртуального хоста Nginx

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example/public;   # public/, а не корень проекта!
    index index.php;

    # статику отдаём напрямую, с кэшем
    location ~* \.(?:css|js|jpg|jpeg|png|gif|ico|svg|woff2?)$ {
        expires 30d;
        access_log off;
        try_files $uri =404;
    }

    # фронт-контроллер Symfony: всё на index.php
    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        internal;   # прямой доступ к /index.php извне запрещён
    }

    # запрещаем исполнять любые другие .php (защита от залитых файлов)
    location ~ \.php$ { return 404; }
}

Пул PHP-FPM

FPM настраивается отдельно (например, /etc/php/8.4/fpm/pool.d/www.conf). Ключевое — модель процессов и их число:

; общение через unix-сокет (быстрее TCP на одной машине)
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data

; динамический пул
pm = dynamic
pm.max_children = 20        ; потолок процессов (упирается в RAM)
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500       ; перезапуск воркера, лечит утечки памяти

Главная ошибка — задрать pm.max_children «на всякий случай». Считайте от памяти: (свободная RAM) / (средняя память одного процесса). Если процессов больше, чем сервер тянет, при нагрузке начнётся своппинг и всё ляжет.

Production-минимум, который часто забывают

  • root указывает на public/, а не на корень проекта — иначе наружу торчат .env, vendor/, конфиги.
  • Запрет исполнения произвольных .php (см. конфиг) — чтобы залитая через дыру картинка-«шелл» не выполнилась.
  • В php.ini для прода: display_errors = Off, expose_php = Off, включён OPcache.
  • OPcache — кэш скомпилированного байткода, на проде даёт кратный прирост; в продакшене opcache.validate_timestamps=0 (сброс кэша только при деплое).
  • Таймауты: согласуйте fastcgi_read_timeout в Nginx и max_execution_time в PHP.

Как проверить и перезапустить

# проверить синтаксис конфигов перед рестартом
nginx -t
php-fpm8.4 -t

# применить
systemctl reload nginx
systemctl restart php8.4-fpm

# посмотреть, что сокет на месте и слушается
ss -lx | grep php8.4-fpm.sock

Итог

Базовая боевая связка несложная: Nginx отдаёт статику и проксирует PHP в пул FPM, root смотрит в public/, произвольные .php запрещены, число воркеров посчитано от памяти, OPcache включён. Этого достаточно для уверенного старта большинства PHP-проектов.

Если нужно настроить или вычистить боевое окружение под нагрузку — помогу на консультации.

Когда сервер настроен, важно видеть, что на нём реально происходит: ошибки, аптайм и метрики удобно собирать self-hosted — например, через Gotcha, не платя за облачный мониторинг.