Стек веб-сервера: nginx, PHP-FPM и Node
Как nginx, Apache, PHP-FPM и Node-приложения складываются вместе на реальном сервере.
За 30 секунд
Веб-сервер — это входная дверь вашего сервера: он принимает каждый HTTP-запрос, сам отдаёт файлы и передаёт динамические запросы вашему приложению. Сегодня эта дверь — обычно nginx, который общается с PHP через PHP-FPM, а с Node-приложениями — через локальный порт. Разобравшись с server-блоками, location и reverse proxy, вы сможете разместить Laravel API и фронтенд на Next.js рядом на одной машине и деплоить их без простоя.
Почему я это использую
- Я ставлю nginx перед почти всем, что запускаю, потому что он быстро отдаёт статику, терминирует HTTPS и проксирует запросы к приложениям на Laravel и Next.js, потребляя очень мало памяти.
- Большинство продакшен-багов, за которыми я гонялся на серверах, заканчивались в логе веб-сервера или PHP-FPM, поэтому хорошее знание этого слоя позволяет мне быстро чинить проблемы, а не гадать.
- Я работал и с хостинг-панелями, и с «голыми» серверами, и именно понимание стека под ними делает безопасной эксплуатацию любого из вариантов.
- Чистый конфиг nginx — это ещё и документация: любой может прочитать, какой домен ведёт к какому приложению, что кэшируется и что заблокировано.
Как это работает
01Что делает веб-сервер и как это делает nginx
Представьте оживлённый ресторан. Хостес у входа встречает каждого гостя, раздаёт меню (статические файлы) и передаёт настоящие заказы на кухню (вашему приложению). Веб-сервер — это и есть хостес. Он слушает порты 80 и 443, разбирает HTTP-запрос, решает, кто должен на него ответить, и отправляет ответ обратно. nginx построен на событийной модели: небольшое число воркеров (обычно по одному на ядро CPU) каждый обслуживает тысячи соединений через неблокирующий ввод-вывод, поэтому простаивающий или медленный клиент почти ничего не стоит. Apache традиционно использовал процесс или поток на соединение (prefork/worker MPM); современный event MPM сократил разрыв, а сильные стороны Apache — файлы .htaccess на уровне каталогов и огромная экосистема модулей. На практике многие стеки, включая большинство хостинг-панелей, ставят nginx впереди для статики и TLS, а Apache держат за ним ради совместимости.
- Master-процесс: читает конфиг, открывает порты, управляет воркерами.
- Воркеры: обслуживают соединения; worker_processes auto подстраивается под число ядер CPU.
- .htaccess в Apache удобен, но читается при каждом запросе; в nginx аналога нет — и это сделано намеренно.
02Server-блоки и location
Конфиг nginx — это дерево. Контекст http хранит глобальные настройки. Внутри него каждый server-блок — это один виртуальный хост, который выбирается по порту и заголовку Host (server_name). Внутри server блоки location сопоставляются с путём URL и решают, что делать: отдать файл, передать в PHP, проксировать в Node или запретить. У сопоставления location есть правила, которые стоит запомнить: первыми побеждают точные совпадения (=), затем самый длинный префикс, если только не совпала ещё и location с регулярным выражением (~) — тогда побеждает первая совпавшая регулярка (префикс ^~ отключает проверку регулярок). try_files — рабочая лошадка: он проверяет реальный файл, затем каталог, а потом откатывается к точке входа приложения. В Ubuntu сайты лежат в /etc/nginx/sites-available и включаются симлинком в sites-enabled.
server {
listen 80;
server_name static.example.com;
root /var/www/static/public;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location ~* \.(css|js|svg|png|jpg|webp|woff2)$ {
expires 30d;
access_log off;
}
location ~ /\.(?!well-known) {
deny all;
}
}
03PHP-FPM: как nginx запускает PHP
nginx сам не умеет выполнять PHP. Он пересылает запрос по протоколу FastCGI в PHP-FPM (FastCGI Process Manager) — отдельный сервис, который держит наготове пул PHP-воркеров. Соединение обычно идёт через Unix-сокет, например /run/php/php8.5-fpm.sock (PHP 8.5 — версия по умолчанию в Ubuntu 26.04; в 24.04 поставляется 8.3). Пул — это группа воркеров со своим пользователем, сокетом и лимитами. Дайте каждому приложению собственный пул, работающий от собственного системного пользователя: тогда взломанный сайт не сможет читать файлы другого сайта, а одно медленное приложение не съест всех воркеров. pm = dynamic масштабирует число воркеров в заданных пределах; pm.max_children — жёсткий потолок, поэтому рассчитывайте его примерно как (RAM под PHP) делённое на (средний объём памяти воркера, для Laravel часто 40–80 MB). Когда заняты все дочерние процессы, запросы встают в очередь, и вы видите ошибки 502/504 и сообщение «server reached pm.max_children» в логе FPM.
; /etc/php/8.5/fpm/pool.d/api.conf
[api]
user = api
group = api
listen = /run/php/php8.5-fpm-api.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
request_terminate_timeout = 60s
04Laravel API за nginx
У Laravel одна точка входа — public/index.php, и корень документов должен указывать на папку public, чтобы .env, storage и vendor никогда не были доступны из веба. try_files сначала отдаёт реальные файлы, а всё остальное отправляет в index.php, сохраняя строку запроса. Использование $realpath_root вместо $document_root важно, когда вы деплоите через симлинк «current»: PHP тогда видит разрешённый путь релиза, и OPcache не продолжает отдавать старый релиз после переключения симлинка. Пути к сертификатам ниже — те, что certbot создаёт для Let's Encrypt. http2 on — современный способ включить HTTP/2; старая форма «listen 443 ssl http2» устарела начиная с nginx 1.25.1.
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
root /var/www/api/current/public;
index index.php;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
client_max_body_size 20m;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.5-fpm-api.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
}
location ~ /\.(?!well-known) { deny all; }
}
05Фронтенд на Next.js через reverse proxy
Node-приложение (Next.js, NestJS, Express) — это собственный HTTP-сервер, слушающий локальный порт, например 127.0.0.1:3000. nginx работает как reverse proxy: принимает публичное HTTPS-соединение и пересылает запрос на этот порт, добавляя заголовки, чтобы приложение знало реальный IP клиента и протокол. Привязывайте Node-приложение к 127.0.0.1, чтобы до него никогда нельзя было достучаться напрямую. Держите процесс живым через pm2 (pm2 start npm --name web -- start, затем pm2 startup и pm2 save) или через юнит systemd с Restart=always. Блок map относится к контексту http; он пропускает апгрейд до WebSocket, оставляя обычные запросы на переиспользуемых keepalive-соединениях. Добавьте второй server-блок на порту 80, который отвечает 301 с переходом на HTTPS, и включите gzip (gzip_types для CSS, JS, JSON, SVG); для Brotli нужен дополнительный модуль — или можно поручить сжатие на краю CDN вроде Cloudflare.
map $http_upgrade $connection_upgrade { default upgrade; '' ''; }
upstream web_app { server 127.0.0.1:3000; keepalive 16; }
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://web_app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
06Перезагрузка и деплой без простоя
Никогда не перезапускайте nginx, чтобы применить изменение конфига. Проверьте конфиг, а затем сделайте reload: master-процесс читает новый конфиг, запускает новых воркеров и даёт старым завершить текущие запросы, прежде чем выйти. Если проверка не прошла, ничего не меняется, и продолжает работать старый конфиг. Та же идея применима к приложениям. PHP-FPM поддерживает плавный reload. Для Laravel деплойте каждый релиз в отдельную папку, выполняйте там composer install и миграции, затем атомарно переключайте симлинк «current» и делайте reload PHP-FPM; откат — это просто возврат симлинка назад. Перезапустите воркеры очередей (php artisan queue:restart), чтобы они подхватили новый код. Для Node pm2 reload в режиме cluster перезапускает инстансы по одному; с единственным инстансом в режиме fork это фактически restart, поэтому для настоящего нулевого простоя запускайте как минимум два инстанса.
# nginx: validate, then graceful reload
sudo nginx -t && sudo systemctl reload nginx
# Laravel release switch (atomic symlink swap)
ln -sfn /var/www/api/releases/2026-11-05_1200 /var/www/api/current
sudo systemctl reload php8.5-fpm
php /var/www/api/current/artisan queue:restart
# Node: rolling restart of a pm2 cluster
pm2 reload web
07Хостинг-панели или «голый» сервер
Панели вроде Plesk и cPanel устанавливают и обслуживают за вас весь этот стек: веб-сервер, версии PHP, базы данных, почту, DNS, сертификаты и бэкапы — всё через веб-интерфейс. Они отлично подходят, когда множеством небольших сайтов должны управлять не специалисты или когда вы продаёте хостинг. Цена — контроль и накладные расходы. Панели добавляют лицензионные платежи, лишние сервисы, которые едят память и требуют патчей, собственные шаблоны конфигов, перезаписывающие ручные правки, и большую поверхность атаки (сама панель — это страница входа в интернете). «Голый» сервер с nginx, PHP-FPM, Node и парой скриптов легче и полностью понятен, но каждая его часть на вас: обновления, сертификаты, бэкапы и мониторинг. Для нескольких приложений, которые ведёт инженер, «голый» сервер обычно лучше; для десятков сайтов-визиток и почтовых ящиков панель всё ещё может быть прагматичным выбором.
Когда применять
- nginx: как публичная точка входа почти для любого веб-приложения — для TLS, статики, сжатия и проксирования.
- PHP-FPM с отдельными пулами: всегда, когда вы хостите PHP-приложения, особенно если их несколько на одном сервере.
- pm2 или systemd: чтобы держать Node-приложения запущенными, перезапускать их при падении и стартовать при загрузке.
- Apache за nginx: когда legacy-приложения зависят от правил .htaccess или модулей, которые есть только в Apache.
Когда не стоит
- Не пишите конфиги nginx вручную, если вашему приложению и бюджету уже подходит управляемая платформа (PaaS или serverless-хостинг).
- Не открывайте порты Node или PHP-FPM напрямую в интернет; перед ними всегда должен стоять reverse proxy.
- Не ставьте хостинг-панель на сервер с одним-двумя приложениями, которые вы деплоите сами: это накладные расходы без пользы.
Альтернативы
- Caddy
Автоматический HTTPS и очень короткий конфиг; отлично подходит для небольших конфигураций, экосистема меньше, чем у nginx.
- Apache httpd
Зрелый, гибкий, с поддержкой .htaccess; в классических режимах тяжелее nginx в расчёте на соединение.
- Traefik
Reverse proxy, который сам настраивается по меткам Docker или Kubernetes; популярен в мире контейнеров.
- FrankenPHP / Laravel Octane
Держат PHP-приложение в памяти между запросами ради скорости; требуют аккуратности с состоянием.
Лучшие практики
- Всегда запускайте nginx -t перед reload и храните конфиги в системе контроля версий.
- Один server-блок на домен, один пул PHP-FPM и один системный пользователь на приложение.
- Направляйте корень документов только на папку public; запрещайте доступ к dot-файлам, кроме .well-known.
- Привязывайте серверы приложений к 127.0.0.1 или Unix-сокету, чтобы единственным публичным слушателем был nginx.
- Перенаправляйте HTTP на HTTPS, включайте HTTP/2 и gzip и задавайте долгие заголовки кэширования для ресурсов с хэшем в имени.
- Используйте папки релизов и атомарное переключение симлинка, чтобы деплой и откат занимали секунды.
- Следите за error-логом nginx и логом PHP-FPM: они объясняют большинство ошибок 502 и 504.
Частые ошибки
- Root указывает на папку проекта вместо public/, открывая доступ к .env и исходному коду.
- Днём делают restart сервисов вместо reload и обрывают живые запросы.
- Оставляют pm.max_children по умолчанию (5), и сайт встаёт в очередь при умеренном трафике, — или ставят настолько большое значение, что сервер уходит в swap.
- Забывают заголовок X-Forwarded-Proto, и приложение считает запросы HTTP и строит неправильные URL для редиректов.
- Правят вручную конфиги, которыми управляет панель, и теряют изменения при следующем обновлении панели.
Глубже
08Буферизация, медленные клиенты и потоковые ответы
Одна из самых ценных задач nginx незаметна — буферизация. Когда телефон на слабом соединении отправляет форму, nginx сначала читает всё тело запроса (в памяти до client_body_buffer_size, дальше во временный файл) и только потом передаёт его PHP-FPM или Node. На обратном пути он быстро читает весь ответ upstream в proxy- или FastCGI-буферы и сам понемногу отдаёт его медленному клиенту. Дорогой воркер приложения освобождается за миллисекунды, а не занят секундами из-за человека в поезде. Исключение — стриминг: Server-Sent Events, long polling и потоковые ответы AI или отчётов должны доходить до клиента по мере генерации. Для таких location отключайте proxy buffering и увеличивайте proxy_read_timeout, который по умолчанию равен 60 секундам между двумя чтениями. Приложение также может отключить буферизацию для конкретного ответа заголовком X-Accel-Buffering: no.
location /events/ {
proxy_pass http://web_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_buffering off; # send bytes as soon as they arrive
proxy_cache off;
proxy_read_timeout 1h; # default 60s would cut idle streams
}
09Microcaching: как пережить всплеск трафика
Для страниц, одинаковых для всех анонимных посетителей (список товаров, публичный ответ API), nginx может кэшировать ответ upstream всего на одну секунду. Звучит бесполезно, но при 500 запросах в секунду это превращает 500 выполнений PHP в одно. fastcgi_cache_lock заставляет параллельные запросы к одному незакэшированному ключу ждать единственного обращения к upstream, а не набрасываться толпой на PHP, а fastcgi_cache_use_stale с updating отдаёт слегка устаревшую копию, пока один запрос обновляет её в фоне. Опасность — закэшировать персональные данные. Кэшируйте только GET и HEAD для анонимных пользователей: обходите кэш, если есть сессионная cookie или заголовок Authorization, и никогда не кэшируйте ответы, которые ставят cookie. Заголовок X-Cache (HIT, MISS, BYPASS, STALE) позволяет проверить поведение через curl -I. proxy_cache работает так же перед Node-приложениями.
# http context
fastcgi_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:50m
max_size=1g inactive=10m use_temp_path=off;
# inside location ~ \.php$
fastcgi_cache api_cache;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 1s;
fastcgi_cache_lock on;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_bypass $cookie_app_session $http_authorization;
fastcgi_no_cache $cookie_app_session $http_authorization;
add_header X-Cache $upstream_cache_status always;
10Rate limiting через limit_req
limit_req реализует leaky bucket по ключу, обычно по адресу клиента. limit_req_zone находится в контексте http и задаёт ключ, зону общей памяти и скорость (r/s или r/m). На 64-битных системах каждое состояние занимает 128 байт, так что зона 10m отслеживает около 80 000 адресов. limit_req внутри location применяет лимит. burst — размер очереди для запросов сверх скорости. Без nodelay запросы из очереди задерживаются, чтобы уложиться в скорость: upstream работает ровно, но страницы кажутся медленными; с nodelay они обслуживаются сразу, а отклоняются только запросы сверх burst. delay=N (с версии 1.15.7) совмещает оба режима. Отказы по умолчанию отдаются как 503, что для мониторинга выглядит как авария, поэтому задайте limit_req_status 429. Используйте $binary_remote_addr вместо $remote_addr ради экономии памяти и, если впереди стоит CDN, сначала настройте реальные IP, иначе все посетители окажутся в одном ведре CDN.
# http context
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_status 429;
# server context
location = /api/login {
limit_req zone=login burst=5 nodelay;
try_files $uri /index.php?$query_string;
}
location /api/ {
limit_req zone=api burst=20 nodelay;
try_files $uri /index.php?$query_string;
}
11Пограничные случаи: default server, реальные IP и лимиты соединений
Если заголовок Host запроса не совпадает ни с одним server_name, nginx передаёт его default server для этого порта — первому объявленному, если ни один не помечен default_server. Сканеры, обращающиеся прямо к вашему IP, увидят ваш первый сайт, а по HTTPS — и его сертификат, что раскрывает, какие домены там живут. Этого позволяет избежать catch-all server, который отклоняет TLS handshake (ssl_reject_handshake, nginx 1.19.4+) и закрывает обычный HTTP нестандартным кодом 444. За Cloudflare или балансировщиком используйте модуль realip: set_real_ip_from для каждого доверенного диапазона прокси и real_ip_header CF-Connecting-IP или X-Forwarded-For, чтобы логи, rate limit и fail2ban видели посетителя. Никогда не доверяйте этому заголовку от недоверенных источников. И наконец, ёмкость: worker_processes, умноженное на worker_connections, ограничивает число одновременных соединений, а проксируемый запрос использует два (клиент плюс upstream), поэтому вместе с worker_connections увеличивайте worker_rlimit_nofile.
server {
listen 80 default_server;
listen 443 ssl default_server;
server_name _;
ssl_reject_handshake on; # no certificate leaked to IP scanners
return 444; # close the connection without a response
}
# http context, behind Cloudflare (one line per range from cloudflare.com/ips)
set_real_ip_from 173.245.48.0/20;
real_ip_header CF-Connecting-IP;
Реальные ситуации
Случайные 502 в обеденное время
Проблема
Laravel API утром работает, а потом каждый день около полудня периодически возвращает 502 и 504. Перезапуск PHP-FPM помогает на час.
Как решить
Сначала прочитайте лог FPM: «server reached pm.max_children» подтверждает, что пул исчерпан. Включите pm.status_path для этого пула и во время пика следите за активными процессами и listen queue. Затем выясните, почему воркеры медленные: включите slow log (request_slowlog_timeout = 5s, путь slowlog), чтобы получить stack trace зависших запросов, — обычно они указывают на внешний API-вызов без таймаута или запрос без индекса. Сначала исправьте это, добавьте таймауты HTTP-клиенту и только потом поднимайте pm.max_children в рамках бюджета памяти (свободная RAM, делённая на средний RSS воркера из ps). Перезапуск просто скрывал очередь.
Загрузки падают с 413 или молча дают пустые файлы
Проблема
Пользователи сообщают, что загрузка видео на 30 MB не проходит. Иногда браузер показывает 413 Request Entity Too Large, иногда запрос проходит, но файла в приложении нет.
Как решить
Здесь три последовательных лимита, и побеждает наименьший. nginx отклоняет тела больше client_max_body_size с кодом 413, поэтому увеличьте его в server или в конкретном location для загрузок. Затем PHP применяет post_max_size и upload_max_filesize; при их превышении PHP отбрасывает тело, и приложение видит пустой запрос вместо ошибки — отсюда пропавшие файлы. Задайте их в пуле через php_admin_value[post_max_size] = 64M и php_admin_value[upload_max_filesize] = 64M, перезагрузите FPM и проверьте request_terminate_timeout для медленных каналов. Если впереди Cloudflare, его тариф тоже ограничивает тело запроса (100 MB на бесплатном тарифе). Крупные медиафайлы загружайте прямо в объектное хранилище по presigned URL.
У всех посетителей в логах один и тот же IP
Проблема
После переноса DNS в Cloudflare с включённым проксированием в access-логе видно лишь несколько адресов Cloudflare. Rate limiting в Laravel теперь блокирует всех разом, а аудит-логи бесполезны.
Как решить
Научите nginx понимать, кто реальный клиент. Добавьте в контекст http строки set_real_ip_from для каждого диапазона, опубликованного на cloudflare.com/ips (IPv4 и IPv6, в идеале генерируемые небольшим скриптом по расписанию), и real_ip_header CF-Connecting-IP, затем nginx -t и reload. Теперь $remote_addr, логи, limit_req и fail2ban видят посетителя. В Laravel настройте trusted proxies, чтобы request()->ip() и определение HTTPS использовали forwarded-заголовки, но только от вашего собственного nginx. Наконец, ограничьте порты 80 и 443 диапазонами Cloudflare, чтобы никто не мог обойти edge и подделать заголовок.
Шпаргалка
- Проверить конфиг и мягко перезагрузить
sudo nginx -t && sudo systemctl reload nginx- Вывести полный собранный конфиг
sudo nginx -T | less- Включить сайт в Ubuntu
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/- Проверить конфиг PHP-FPM
sudo php-fpm8.5 -t- Открыть статус пула FPM (защитите его)
pm.status_path = /fpm-status- Логировать медленные PHP-запросы со stack trace
request_slowlog_timeout = 5s slowlog = /var/log/php8.5-fpm-api.slow.log- Проверить, на каких сокетах слушает PHP-FPM
sudo ss -xlp | grep php- Проверить vhost до переключения DNS
curl -sI --resolve example.com:443:127.0.0.1 https://example.com- Смотреть ошибки в реальном времени
sudo tail -f /var/log/nginx/error.log /var/log/php8.5-fpm.log- Отдавать 429 вместо 503 при rate limit
limit_req_status 429;
В моих реальных проектах
- Я обслуживаю серверы с Plesk, где nginx стоит перед Apache и PHP-FPM и обслуживает множество сайтов на Laravel и Next.js.
- На моих серверах воркеры очередей Laravel работают под supervisor, Node-приложения — под pm2, а планировщики — через cron.
- На выделенном SaaS-сервере nginx работает как reverse proxy перед сервисами Docker Compose.
- Мой план консолидации — работать без панели: «голый» nginx с PHP-FPM и Node-приложениями за ним.
Вопросы и ответы
Q1Почему nginx обычно быстрее Apache для статики и большого числа соединений?
nginx использует событийную неблокирующую модель: несколько воркеров мультиплексируют каждый тысячи соединений, поэтому простаивающие keepalive-соединения или медленные клиенты стоят совсем немного памяти. Классические MPM в Apache выделяют на каждое соединение процесс или поток, что дорого обходится при высокой конкурентности. Event MPM в Apache сократил разрыв, а для динамических запросов узким местом обычно становится приложение, а не веб-сервер. Другое практическое различие — .htaccess: Apache проверяет его при каждом запросе, что гибко, но медленнее, а nginx загружает весь конфиг один раз при старте. Поэтому многие стеки ставят nginx впереди для TLS, статики и буферизации, а Apache оставляют только там, где нужны его модули.
Q2Как nginx общается с PHP и из-за чего возникает 502 Bad Gateway?
nginx пересылает PHP-запросы по FastCGI в PHP-FPM, обычно через Unix-сокет. PHP-FPM держит пул воркеров, которые выполняют скрипт и возвращают ответ. 502 означает, что nginx не смог получить корректный ответ от этого апстрима: PHP-FPM лежит, неверен путь к сокету или права на него, или воркер упал. 504 означает, что апстрим отвечал слишком долго. Мои первые шаги — error-лог nginx, лог PHP-FPM и systemctl status для сервиса FPM. Очень частая причина — «server reached pm.max_children», то есть все воркеры заняты, часто из-за медленных запросов к БД или вызовов внешних API, которые нужно исправлять, а не просто добавлять воркеров.
Q3Как разместить Laravel API и фронтенд на Next.js на одном сервере?
Два server-блока в nginx, по одному на домен. Блок API использует папку public Laravel как root, try_files с откатом к index.php и fastcgi_pass к отдельному пулу PHP-FPM, работающему от собственного пользователя. Блок фронтенда проксирует на сервер Next.js на 127.0.0.1:3000, который держит живым pm2 или systemd, с заголовками пересылки и поддержкой апгрейда до WebSocket. Оба получают сертификаты Let's Encrypt, редиректы с HTTP на HTTPS, HTTP/2 и сжатие. Воркеры очередей работают под supervisor или systemd, планировщик — через cron. Деплой использует папки релизов и переключение симлинка для Laravel и pm2 reload для Next.js, так что ни у одной из сторон нет простоя.
Q4Чем nginx reload отличается от restart?
Restart останавливает master-процесс и всех воркеров, а затем запускает их снова: открытые соединения обрываются, и если новый конфиг сломан, сайт так и остаётся лежать. Reload отправляет сигнал master-процессу, который разбирает новый конфиг; если он валиден, master запускает с ним новых воркеров и говорит старым перестать принимать соединения и выйти после завершения текущих запросов. Если конфиг невалиден, продолжает работать старый. Поэтому безопасная привычка — nginx -t, а затем systemctl reload nginx. Настоящий restart нужен только для вещей вроде изменения некоторых низкоуровневых настроек или обновления бинарника, и даже для этого есть механизм плавной замены на месте.
Q5Стоит ли нам использовать хостинг-панель вроде Plesk или cPanel?
Зависит от того, кто обслуживает сервер и сколько на нём сайтов. Панель оправдана, когда вы хостите много небольших сайтов, почтовых ящиков и DNS-зон и хотите, чтобы ими управляли не инженеры через интерфейс со встроенными бэкапами и сертификатами. Для нескольких собственных приложений, которые деплоят разработчики, «голый» сервер легче: нет лицензионных платежей, меньше расход памяти, нет страницы входа в панель, открытой в интернет, а конфиги полностью под вашим контролем и могут храниться в Git. Компромисс в том, что обновления, сертификаты, бэкапы и мониторинг придётся вести самим, в идеале скриптами. Многие команды переходят с панели на «голые» серверы, когда их приложения перерастают хостинг сайтов-визиток.
Q6Как держать Node.js-приложение запущенным в продакшене?
Запускать его под менеджером процессов, никогда не в терминальной сессии. pm2 популярен: он перезапускает приложение при падении, может запускать несколько инстансов в режиме cluster, ротирует логи с помощью модуля, а pm2 startup вместе с pm2 save восстанавливают процессы после перезагрузки. Юнит systemd с Restart=always, отдельным пользователем и env-файлом — столь же хороший вариант без зависимостей. В любом случае привязывайте приложение к 127.0.0.1, ставьте перед ним nginx для TLS и буферизации, задавайте лимиты памяти, чтобы утечка приводила к перезапуску, а не к свопингу, и откройте health-эндпоинт, который сможет проверять мониторинг. Для нулевого простоя запускайте как минимум два инстанса и перезагружайте их по одному.
Вопросы уровня senior
S1Один сервер с nginx и PHP-FPM должен пережить на следующей неделе десятикратный всплеск трафика. Что вы сделаете?
Сначала измерю текущий потолок нагрузочным тестом на staging, наблюдая за активными процессами FPM, CPU и нагрузкой на базу, чтобы понять, какой ресурс ломается первым. Затем убираю работу, а не добавляю воркеры. Статика идёт через CDN с длинными cache-заголовками. Анонимные страницы получают microcache на одну секунду с cache lock и stale-while-updating, что схлопывает одинаковые запросы. Дорогие эндпоинты получают rate limit с ответом 429. Медленные побочные действия вроде писем и вебхуков уходят в очереди. Только после этого я подбираю pm.max_children по реальной памяти воркера и, если упираемся в CPU, масштабирую сервер вертикально на время события. Добавление воркеров без устранения узкого места обычно просто переносит очередь в базу данных.
S2PHP-FPM или долгоживущий рантайм вроде Laravel Octane или FrankenPHP: как вы решаете?
PHP-FPM загружает фреймворк на каждый запрос, что стоит от нескольких до десятков миллисекунд, зато даёт идеальную изоляцию: состояние не протекает между запросами, а утечка памяти умирает вместе с запросом. Octane на FrankenPHP, Swoole или RoadRunner держит загруженное приложение в памяти, поэтому задержка и пропускная способность у тяжёлых для фреймворка эндпоинтов заметно улучшаются. Цена — дисциплина: синглтоны, статические свойства и всё, что разрешено при загрузке, могут перенести данные одного пользователя в следующий запрос, а утечки копятся до перезапуска воркера. По умолчанию я выбираю FPM, а Octane — когда профилирование показывает, что доминирует время bootstrap, кодовая база проверена на состояние запросов, а у команды есть тесты и перезапуск воркеров по max-requests, чтобы ловить утечки.
S3Как безопасно сохранить IP и протокол клиента на пути через CDN, nginx и приложение?
Каждый узел должен доверять только узлу перед собой. На edge CDN IP клиента известен; CDN добавляет заголовок, например CF-Connecting-IP. nginx использует модуль realip с set_real_ip_from, ограниченным опубликованными диапазонами CDN, так что $remote_addr становится адресом посетителя, только если соединение действительно пришло от CDN. Затем nginx выставляет X-Forwarded-For и X-Forwarded-Proto в сторону приложения, а фреймворк доверяет прокси только с 127.0.0.1 или адреса приватного балансировщика. Файрвол открывает веб-порты только для CDN, иначе кто угодно мог бы подключиться напрямую и подделать заголовок. Ошибка здесь ломает rate limit, антифрод-проверки, аудит-логи и определение HTTPS для редиректов.
S4Когда вы выберете Caddy или Traefik вместо nginx?
Caddy — сильный выбор для небольших установок, где автоматическое управление сертификатами и конфиг на десять строк важнее тонкой настройки; он сам получает и продлевает сертификаты и имеет разумные настройки TLS по умолчанию. Traefik подходит для контейнерных платформ: он находит сервисы по Docker-лейблам или ресурсам Kubernetes и перенастраивает маршруты без reload, что идеально, когда сервисы появляются и исчезают. nginx выигрывает, когда нужны зрелое кэширование, детальный rate limiting, FastCGI к PHP-FPM, огромная база задокументированных конфигураций и предсказуемая производительность под нагрузкой. С эксплуатационной точки зрения лучший прокси — тот, который ваша команда сможет отладить ночью. Для PHP и смешанных стеков я стандартизируюсь на nginx, а в контейнерной среде, управляемой лейблами, принял бы Traefik.
Глоссарий
- Reverse proxy
- Сервер, который принимает запросы клиентов и пересылает их бэкенд-приложениям, возвращая их ответы как свои.
- Upstream
- В nginx — бэкенд-сервер или группа серверов, куда проксируются запросы, например Node-приложение или PHP-FPM.
- FastCGI
- Бинарный протокол, по которому веб-сервер отправляет запросы долгоживущему менеджеру процессов приложения, например PHP-FPM.
- Pool (PHP-FPM)
- Группа воркер-процессов PHP со своим пользователем, сокетом и лимитами, обычно по одной на приложение.
- worker_connections
- Максимальное число одновременных соединений, которое может открыть один воркер nginx, включая соединения с upstream.
- Keepalive
- Переиспользование одного TCP-соединения для нескольких HTTP-запросов, экономящее handshake между клиентом, nginx и upstream.
- Буферизация
- nginx целиком сохраняет запрос или ответ, прежде чем передать дальше, чтобы медленные клиенты не держали воркеры приложения.
- Microcache
- Кэширование динамических ответов на очень короткое время, часто на секунду, чтобы схлопывать всплески одинаковых запросов.
- Default server
- Блок server, который nginx использует для порта, если заголовок Host не совпал ни с одним server_name.
- OPcache
- Встроенный в PHP кэш скомпилированных скриптов в общей памяти, избавляющий от повторного разбора PHP-файлов на каждом запросе.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.