1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Как nginx разговаривает с PHP

Коротко

PHP не держит свой веб-сервер. Между nginx и кодом стоит php-fpm - служба, которая выполняет PHP-файлы; nginx разговаривает с ней не по HTTP, а набором именованных параметров.

  • Обработчиком становится fastcgi_pass. root внутри такого блока ничего не отдаёт.
  • Главный параметр - SCRIPT_FILENAME: какой файл выполнять. Забыл - клиент получает File not found. при живом файле на диске.
  • fastcgi_param наследуется всё или ничего, ровно как proxy_set_header. Симптом потери - ответ 200 с пустым телом.
  • Статику nginx отдаёт сам, интерпретатор при этом не просыпается.

Если знаешь, что делает SCRIPT_FILENAME и почему include fastcgi_params пишут в том же блоке - листай до «Теперь сам».

Сначала ответь сам

/etc/nginx/conf.d/shop.local.confhttp server
root /var/www/shop;

location ~ \.php$ {
    include      fastcgi_params;
    fastcgi_pass php:9000;
}

Файл /var/www/shop/index.php на месте, php-fpm жив, конфиг проходит nginx -t. Что получит клиент на запрос /index.php?

$  команда
curl -s http://shop.local/index.php
↳  выводcurl -s http://shop.local/index.php
File not found.

Код 404, файл существует (проверено: блок без SCRIPT_FILENAME отдаёт ровно это). В конфиге не хватает одной строки - той, что называет php-fpm имя файла. root он не читает и про URL не знает.

На что это похоже

Ты приходишь в архив с бумажкой. Сотрудник за окном не видит ни двери, в которую ты вошёл, ни вывески на здании - он видит только бумажку. Если на ней написан шифр дела, он принесёт дело. Если шифра нет, он скажет «не найдено», и спорить бесполезно: у него нет способа догадаться, что ты имел в виду.

Разговор с php-fpm устроен так же. Он не знает, по какому адресу пришёл запрос и где корень сайта. Ему передают список полей, и одно из них - путь к файлу.

Механизм: не HTTP, а набор параметров

nginx URI /index.php root /var/www/shop знает про файлы php-fpm не знает URL не знает root выполняет файл SCRIPT_FILENAME = /var/www/shop/index.php REQUEST_METHOD, QUERY_STRING, CONTENT_TYPE, ... через сокет идут пары «имя - значение», а не строка запроса с заголовками
FastCGI передаёт параметры. Всё, что php-fpm знает о запросе, приехало этим списком.

Правило

/etc/nginx/conf.d/shop.local.confhttp server
root  /var/www/shop;
index index.php index.html;

location / {
    try_files $uri $uri/ /index.php?$args;
}

location ~ \.php$ {
    try_files $uri =404;

    include      fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

fastcgi_pass здесь указывает на unix:/run/... - юникс-сокет, но бывает и php:9000, где php - имя хоста (в Docker - контейнера), 9000 - порт. $document_root - это root текущего блока, $fastcgi_script_name - имя скрипта из URI; две переменные подряд nginx читает как есть, разделитель между ними не нужен. Вместе выходит полный путь к файлу.

Адрес бывает двух видов: юникс-сокет быстрее и снаружи недоступен, но требует прав (пользователь nginx должен читать файл сокета, права задаются в конфиге пула строками listen.owner, listen.group, listen.mode); TCP нужен, когда php-fpm в другом контейнере. Симптом неверных прав узнаваем: connect() to unix:/... failed (13: Permission denied) в error_log и 502 у клиента.

Строка try_files $uri $uri/ /index.php?$args; даёт красивые ссылки: файла нет, каталога нет - зови входной скрипт и отдай ему исходные параметры. $args в конце не украшение: без него ?page=2 теряется и постраничная навигация ломается «сама по себе».

Разбор: как выглядит потеря SCRIPT_FILENAME

Отличить «параметр не передан» от «файла правда нет» помогает access-лог самого php-fpm. Три запроса подряд на стенде (формат упрощён, в docker compose logs каждую строку предваряет имя сервиса вроде php-1 |):

↳  выводстроки access-лога php-fpm, формат %R - %t "%m %r" %s script=%f
172.25.0.4 - 11/Sep/2026:11:33:54 +0000 "GET /index.php" 200 script=/var/www/html/index.php
172.25.0.4 - 11/Sep/2026:11:33:54 +0000 "GET " 404 script=-
172.25.0.4 - 11/Sep/2026:11:33:54 +0000 "- " 200 script=-

Первая строка - нормальная работа, script указывает на файл. Во второй имя скрипта пустое (script=-): параметра не было, php-fpm не понял, что выполнять, и ответил File not found.. Третья - случай хуже: пропал весь набор параметров, php-fpm ответил 200 и пустое тело (проверено: код=200 длина=0). Клиент видит белую страницу без единой ошибки.

Кстати про фразу «Primary script unknown», которая гуляет по руководствам: на забытый SCRIPT_FILENAME свежий php-fpm отвечает File not found., а эту старую строку больше не пишет (проверено на php-fpm 8.3). Ищи пустое имя скрипта, а не её.

Разбор: почему include пишут в том же блоке

Третья строка лога выше получилась из такого конфига:

/etc/nginx/conf.d/shop.local.confhttp server
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;

location ~ \.php$ {
    fastcgi_param X_MY_FLAG one;      # ← и весь набор сверху пропал
    fastcgi_pass php:9000;
}

Правило то же, что у proxy_set_header из прошлой главы: своя директива в блоке отменяет весь унаследованный набор. Отсюда порядок, который стоит держать без исключений: include fastcgi_params; пишут в том же блоке, где стоят свои параметры, а не этажом выше.

Что ломается без этого

Забыть блок ~ \.php$ целиком - и nginx отдаст файл как обычную статику:

$  команда
curl -s http://shop.local/index.php
↳  выводcurl -s http://shop.local/index.php
<?php echo "INDEX script=", $_SERVER['SCRIPT_FILENAME'] ?? '-', " uri=", $_SERVER['REQUEST_URI'] ?? '-', "\n";

Исходник ушёл клиенту. Вместе с ним уходят пароли к базе, ключи к платёжкам и всё, что лежит в конфигурационных файлах рядом. Такое случается при переносе сайта на новый сервер, где конфиг собрали заново и одну локацию не дописали.

Обратная ошибка - ждать, что try_files $uri =404; внутри php-локации отдаст файл клиенту. Он только проверяет наличие: отдавать всё равно будет контентный обработчик, то есть fastcgi_pass. Зачем тогда строка - разбор в следующем уроке, там она закрывает конкретную дыру.

Зачем это в работе

Главный выигрыш связки виден на статике. Картинки, стили и скрипты под регулярку \.php$ не подходят, поэтому их отдаёт nginx напрямую:

$  команда
curl -so /dev/null -w '%{http_code} %{content_type}\n' http://shop.local/uploads/photo.jpg
↳  выводcurl -so /dev/null -w '%{http_code} %{content_type}\n' http://shop.local/uploads/photo.jpg
200 image/jpeg

Интерпретатор при этом не просыпается вовсе. На сайте, где на страницу приходится полсотни файлов, это разница между «php-fpm занят рисованием страниц» и «php-fpm занят отдачей логотипа».

Порядок блоков в файле при этом не решает ничего, а модификаторы решают: регулярка спрашивается раньше префиксов (четвёртый раздел), поэтому location / не перехватывает .php, даже если написан выше.

Теперь сам

Сайт переехал на новый сервер. Конфиг скопировали, но root в блоке server указал на старый путь /var/www/old-shop, которого на новой машине нет. Что увидит клиент на /index.php и что в логах?

Клиент получит File not found. и код 404. В логе nginx будет 404, ошибки open() не будет вовсе: файл ищет не nginx, а php-fpm - ему передали путь /var/www/old-shop/index.php, и он его не нашёл. В логе php-fpm имя скрипта будет непустым - это и отличает «неверный root» от «забытого SCRIPT_FILENAME».

Главное

PHP работает отдельным процессом, и nginx общается с ним набором параметров, а не HTTP-запросом. Обработчиком становится fastcgi_pass, а главный параметр - SCRIPT_FILENAME $document_root$fastcgi_script_name: без него клиент получает File not found. при живом файле, и в логе php-fpm имя скрипта пустое. Остальные параметры приезжают через include fastcgi_params;, который держат в одном блоке со своими: fastcgi_param наследуется всё или ничего, а симптом потери - ответ 200 с пустым телом. Забытый блок ~ \.php$ отдаёт исходники клиенту.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.