О разделе

Backend-разработчик, который умеет только писать код, а деплоем занимается «кто-то другой» — ограничен. Как только приложение выходит из локалки, начинаются вопросы: почему 502, где логи, как перезапустить, почему письма не уходят, куда пропал SSL, что за 413 Payload Too Large, почему `.env` не подтянулся, где хранятся сессии, как задеплоить без даунтайма. Всё это — не «магия DevOps», а базовая эксплуатация.

Цель раздела: дать рабочее понимание Nginx (главный веб-сервер под Linux) и базы Docker (стандартная контейнеризация) с уклоном в реальные операционные задачи: развернуть, настроить, перезапустить, диагностировать, обновить, восстановить.
Карта раздела
БлокЧто внутри
Nginx — основыЧто это, где лежит, как перезапустить, базовые директивы, server blocks, location.
Nginx — интеграцияPHP-FPM (для Laravel/PHP), статика с gzip, reverse proxy для Node/Python.
Nginx — продакшнSSL/Let's Encrypt, безопасность + rate limiting, кеширование, load balancing, логи, полный Laravel-конфиг, разбор реальных ошибок.
Docker — базаЧто такое контейнеры, команды, Dockerfile, docker compose, volumes, networks, Laravel в Docker, боли.
Что ещё подтянутьsystemd, SSH-deploy, cron, мониторинг, CI/CD, чек-лист боевого сервера.
Аналогия: сайт в интернете — это ресторан. Nginx — метрдотель у входа: принимает всех гостей, отдаёт быстрые заказы (статика — картинки, CSS, JS) сам, а сложные заказы (динамика — PHP-код) передаёт на кухню (PHP-FPM). Docker — стандартные ящики, в которых кухня, склад, посудомойка приезжают на любую точку и работают одинаково.
Почему акцент на Nginx: в 2026 году Nginx обслуживает больше 30% всех сайтов интернета и почти 100% Laravel-деплоев на VPS. Apache уходит, Caddy/OpenResty — нишевые. Знание Nginx — базовая грамотность для PHP/Laravel-разработчика уровня middle+.
Что такое Nginx
Определение

Nginx (произносится «энджин-икс») — HTTP-сервер и обратный прокси (reverse proxy). Создан в 2004 году Игорем Сысоевым для Rambler.ru как ответ на «C10k problem» — как обслужить 10 000 одновременных соединений на одном сервере, не съев всю память. С тех пор стал одним из двух главных веб-серверов интернета (второй — Apache).

В типовой архитектуре Nginx стоит первым, между интернетом и приложением:

Пользователь (браузер) │ │ HTTP/HTTPS запрос ▼ ┌─────────────┐ │ NGINX │ ← слушает 80/443, терминирует SSL, │ (порт 80, │ отдаёт статику, gzip'ит, кеширует │ 443) │ └──────┬──────┘ │ ┌────┴────────────┬────────────────┬────────────────┐ ▼ ▼ ▼ ▼ PHP-FPM Node.js app Python (Gunicorn) Static files (Laravel) (Express) (Django) (public/) :9000 :3000 :8000 прямо с диска
Почему Nginx быстрый: event-driven архитектура

Ключевое архитектурное отличие Nginx от старого Apache prefork — модель обработки соединений.

Apache preforkNginx
МодельПроцесс на соединениеEvent-driven, non-blocking I/O
10 000 соединений10 000 процессов × 10 МБ = 100 ГБ RAM4 worker-процесса × ~50 МБ = 200 МБ RAM
Как ждёт I/OБлокирует процесс — ждёт БД/файлПишет в epoll, идёт обслуживать других
Идеально дляТяжёлых mod_php приложений старой школыReverse proxy, статика, много соединений
Аналогия: Apache prefork — официант на каждого гостя, даже если гость молча жуёт 20 минут. Nginx — один расторопный официант, который обходит все столики: пока один клиент читает меню, он несёт другому счёт, третьему — заказ.
Что Nginx умеет

HTTP-сервер

Отдавать статику (HTML, CSS, JS, картинки, шрифты, видео) с диска максимально быстро. Умеет keep-alive, gzip, HTTP/2, HTTP/3 (QUIC).

Reverse proxy

Принимает запрос от клиента и передаёт бэкенду (PHP-FPM, Node.js, Python, Java). Скрывает бэкенд от интернета, добавляет заголовки, обрабатывает таймауты.

Load balancer

Распределяет запросы между несколькими экземплярами бэкенда (upstream). Round-robin, least_conn, ip_hash. Проверяет здоровье бэкендов.

SSL termination

Держит HTTPS-сертификаты, разбирает TLS, передаёт бэкенду уже расшифрованный HTTP. Бэкенд не думает про сертификаты.

Кеш

proxy_cache / fastcgi_cache — Nginx может кешировать ответы бэкенда на диск и отдавать их без обращения к PHP. На небольших проектах заменяет Varnish.

Security layer

Rate limiting (limit_req), IP-фильтрация, скрытие версий, HTTP security headers, защита от медленных клиентов (slowloris).

Nginx vs Apache — коротко

В 2026 году в мире Laravel/PHP-хостинга Nginx выиграл. Apache остался в 3 нишах: cPanel-хостинг (legacy), Windows-стек, и когда нужен .htaccess в shared-хостинге с mod_rewrite «на лету». Всё что новое строится — на Nginx.

Итог: Nginx — быстрый event-driven HTTP-сервер и reverse proxy. Именно он принимает первый запрос от браузера, отдаёт статику сам, а «динамику» проксирует в PHP-FPM (для Laravel) или на любой другой бэкенд.
Установка Nginx + структура файлов
Установка (Debian/Ubuntu)
# 1. Обновить индексы пакетов
sudo apt update

# 2. Поставить nginx
sudo apt install nginx -y

# 3. Автозапуск при загрузке системы
sudo systemctl enable nginx

# 4. Стартанули
sudo systemctl start nginx

# 5. Проверка версии
nginx -v          # короткая версия
nginx -V          # с флагами сборки: --with-http_ssl_module и т.д.

На CentOS / RHEL / AlmaLinux / Rocky: sudo dnf install nginx -y (или yum на старых). На macOS (dev): brew install nginx.

После установки: сразу открой в браузере http://IP-сервера/. Должна появиться дефолтная страница «Welcome to nginx!». Не появилась — firewall не пропускает 80/443: sudo ufw allow 'Nginx Full'.
Структура файлов Nginx (Ubuntu/Debian)

Nginx на Debian/Ubuntu раскладывает файлы по нескольким директориям — это важно знать, чтобы понимать что где искать:

/etc/nginx/
├── nginx.conf              # ГЛАВНЫЙ конфиг — общие настройки, worker'ы, включает всё остальное
├── mime.types              # таблица MIME-типов (.css → text/css, .png → image/png)
├── fastcgi_params          # стандартные переменные для FastCGI (PHP-FPM)
├── proxy_params            # стандартные заголовки для reverse proxy
├── snippets/               # переиспользуемые куски (fastcgi-php.conf, ssl-params.conf)
│   ├── fastcgi-php.conf
│   └── ssl-params.conf
├── conf.d/                 # *.conf файлы, включаемые из nginx.conf через include
│   └── (по умолчанию пусто)
├── sites-available/        # ВСЕ конфиги сайтов (по одному файлу на сайт)
│   ├── default
│   ├── example.com
│   └── api.example.com
└── sites-enabled/          # СИМЛИНКИ на активные сайты из sites-available/
    ├── default -> ../sites-available/default
    └── example.com -> ../sites-available/example.com

/var/log/nginx/             # ЛОГИ
├── access.log              # все запросы
├── error.log               # ошибки, критично при отладке
├── access.log.1.gz         # ротированные (logrotate)
└── error.log.1.gz

/var/www/                   # типичное место для сайтов (традиция, не обязательно)
├── html/                   # дефолтная welcome-страница
└── example.com/            # твой Laravel-проект
    └── public/             # root у Laravel всегда = public/

/usr/sbin/nginx             # сам бинарник
/var/run/nginx.pid          # PID главного процесса (нужен для reload)
sites-available vs sites-enabled — зачем два

Это Debian-специфичный паттерн (в CentOS его нет — там просто conf.d/*.conf). Идея простая:

  • sites-available/ — хранилище всех конфигов сайтов. Даже отключённые сайты остаются здесь.
  • sites-enabled/ — активные сайты. Это только симлинки на файлы из sites-available/.
  • nginx.conf подключает через include /etc/nginx/sites-enabled/*; — только то, что в enabled.

Включить сайт:

# Создать симлинк
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

# Проверить конфиг
sudo nginx -t

# Применить
sudo systemctl reload nginx

Отключить сайт (без удаления файла):

sudo rm /etc/nginx/sites-enabled/example.com
sudo systemctl reload nginx
Дефолтный сайт. sites-enabled/default ловит всё, что не подошло другим server_name. Оставь его, если хочешь отдавать 404 «в никуда»; удали симлинк, если первым сайтом должен быть твой.
Установка на macOS (Homebrew)

На маке Nginx ставят через Homebrew — для локальной разработки, не для прода. Устройство там заметно другое, чем на Debian, и первые грабли начинаются сразу.

# 1. Установка
brew install nginx

# 2. Запуск как сервис (поднимется и после перезагрузки)
brew services start nginx

# 3. Статус всех brew-сервисов
brew services list

# 4. Перезапуск после правки конфига
brew services restart nginx

# 5. Проверка
nginx -v
curl http://localhost:8080
Порт 8080, а не 80 — и это главное отличие. Homebrew запускает Nginx от твоего пользователя, а не от root. Порты ниже 1024 без root занять нельзя, поэтому в дефолтном конфиге стоит listen 8080. Если нужен именно 80 — запускай сервис через sudo brew services start nginx и правь listen в конфиге.
На macOS нет systemctl. Вместо него launchd: brew services кладёт plist в ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist. Команды systemctl status nginx, journalctl -u nginx тут просто не существуют — смотри brew services list и /opt/homebrew/var/log/nginx/error.log.
Префикс зависит от процессора. На Apple Silicon (M1–M4) Homebrew живёт в /opt/homebrew, на старых Intel-маках — в /usr/local. Все пути ниже даны для Apple Silicon. Свой путь всегда можно узнать командой brew --prefix.
Структура файлов на macOS (Homebrew)
/opt/homebrew/etc/nginx/ ├── nginx.conf # ГЛАВНЫЙ конфиг ├── nginx.conf.default # нетронутая копия от установщика ├── mime.types # таблица MIME-типов ├── fastcgi_params # переменные для FastCGI (PHP-FPM) ├── fastcgi.conf # то же + SCRIPT_FILENAME ├── scgi_params, uwsgi_params ├── koi-utf, koi-win, win-utf # кодировочные карты, трогать не нужно ├── *.default # эталонные копии ВСЕХ конфигов └── servers/ # ← сюда кладут конфиги своих сайтов └── .keepme # пустышка, чтобы папка не пропала /opt/homebrew/var/ ├── www/ # дефолтный docroot (welcome-страница) ├── log/nginx/ │ ├── access.log │ └── error.log └── run/ └── nginx.pid # PID мастер-процесса /opt/homebrew/bin/nginx # симлинк на бинарник /opt/homebrew/Cellar/nginx/1.31.7/ # сама установка нужной версии └── bin/nginx ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist # автозапуск
Главное структурное отличие: папки servers/ вместо пары sites-available + sites-enabled. Символьных ссылок и команды nginx_ensite тут нет — просто положи файл в servers/, и он подхватится. В конце http-блока главного конфига стоит include servers/*;.
Зачем файлы .default. Homebrew хранит рядом с каждым конфигом его эталонную копию. Если доломал nginx.conf — всегда можно вернуться: cp nginx.conf.default nginx.conf. При обновлении формулы brew не перетирает твой конфиг, а обновляет только .default.
ЧтоDebian / UbuntumacOS (Homebrew, Apple Silicon)
Установкаapt install nginxbrew install nginx
Главный конфиг/etc/nginx/nginx.conf/opt/homebrew/etc/nginx/nginx.conf
Конфиги сайтовsites-available/ + симлинк в sites-enabled/servers/ — без симлинков
Порт по умолчанию808080
От кого работаетroot → воркеры от www-dataОт твоего пользователя
Управлениеsystemctlbrew services (launchd)
Логи/var/log/nginx//opt/homebrew/var/log/nginx/
Docroot/var/www//opt/homebrew/var/www/
PID/var/run/nginx.pid/opt/homebrew/var/run/nginx.pid
Бинарник/usr/sbin/nginx/opt/homebrew/bin/nginx
Что означает каждый путь и файл

Откуда взялось /opt

В Unix есть устоявшаяся раскладка каталогов, и Homebrew её повторяет — просто внутри своего корня.

КаталогНазначениеРасшифровка
/etcКонфигурационные файлыОт «et cetera», но прижилась трактовка editable text configuration
/bin, /usr/binИсполняемые файлыbinaries
/varИзменяемые данные: логи, PID, кешvariable
/optДополнительное, не системное ПОoptional
/home, /UsersДомашние папки—
# Homebrew зеркалит эту структуру у себя
/opt/homebrew/etc/nginx/   # конфиги
/opt/homebrew/var/         # логи, pid, docroot
/opt/homebrew/bin/nginx    # бинарник
/opt/homebrew/Cellar/      # сами версии пакетов
Зачем отдельный корень. Всё добро Homebrew лежит в одном месте и не размазывается по системе. Удалить начисто — rm -rf /opt/homebrew, и система не пострадает.
Уточнение про /usr/local и SIP. Часто пишут, что Homebrew ушёл из /usr/local, потому что тот «защищён SIP». Это неверно: System Integrity Protection закрывает /usr, /bin, /sbin, /System, но /usr/local из-под неё явно выведен — именно чтобы туда ставили стороннее ПО. Настоящая причина другая: /usr/local остался префиксом Intel-версии Homebrew, и отдельный /opt/homebrew позволяет держать обе сборки рядом — нативную ARM и Intel под Rosetta. Плюс это свежий каталог, которым Homebrew владеет целиком, без перенастройки прав на общесистемную папку.

Что такое binary и почему папка называется bin

Binary (мн. binaries, в обиходе «бинарник») — файл с данными в двоичном виде, а не в текстовом. В контексте bin/ это исполняемые файлы: программы, уже скомпилированные в машинный код конкретной платформы.

Тип файлаfile показываетЧем является
БинарникMach-O 64-bit executable arm64Машинный код под конкретный процессор. Открыть в редакторе — увидишь мусор
СкриптBourne-Again shell script text executableОбычный текст, который исполняет интерпретатор
КонфигASCII textПросто текст, никто его не запускает
# Чем на самом деле является файл
file /opt/homebrew/Cellar/nginx/1.31.7/bin/nginx
# Mach-O 64-bit executable arm64   ← настоящий бинарник

file /opt/homebrew/bin/brew
# Bourne-Again shell script        ← а brew это скрипт, не бинарник

# Первые байты выдают формат
xxd nginx | head -1
# cffa edfe ...   ← магия Mach-O (macOS)
#                   7f45 4c46 = ELF (Linux), 4d5a = MZ (Windows .exe)
Почему «исполняемый» и «бинарный» — не синонимы. В bin/ лежит и то, и другое: nginx — скомпилированный бинарник, а brew — обычный shell-скрипт с правом запуска. Исполняемость даёт бит x в правах, а бинарность — формат содержимого. Папку назвали bin исторически, когда там действительно лежали только скомпилированные программы.
Разные bin — разные хозяева. /bin и /usr/bin принадлежат системе (на маке их защищает SIP), /usr/local/bin и /opt/homebrew/bin — стороннему ПО. Какой из них сработает, решает порядок в PATH: проверить, что реально запустится, — which nginx, а все варианты сразу — which -a nginx.

Файлы *.default

Эталонные копии от установщика. Их не трогают — это «заводская комплектация».

  • Сломал конфиг — сравни с .default и увидишь, что именно менял: diff nginx.conf nginx.conf.default
  • Восстановить дефолт: cp nginx.conf.default nginx.conf
  • После обновления формулы видно, что поменялось в эталоне — твой файл brew при этом не трогает

mime.types — таблица «расширение → Content-Type»

MIME (Multipurpose Internet Mail Extensions) — стандарт, объясняющий, что за файл отдаётся. Придуман для почты, прижился в вебе. Сервер сообщает тип заголовком, а браузер по нему решает, что делать.

Content-TypeЧто делает браузер
text/htmlОтрисовывает как HTML
text/cssПрименяет как стили
application/javascriptИсполняет как скрипт
image/pngПоказывает картинку
application/jsonНе рисует, отдаёт JS-коду
application/pdfОткрывает во вьювере
# Содержимое mime.types — около 80 строк вида «тип расширения»
types {
    text/html                  html htm shtml;
    text/css                   css;
    application/javascript     js;
    image/png                  png;
    image/jpeg                 jpeg jpg;
    application/json           json;
    ...
}
# Подключение в nginx.conf — строки 18–19 дефолтного конфига
include      mime.types;
default_type application/octet-stream;

default_type — что отдавать, если расширение неизвестно. application/octet-stream означает «бинарный поток»: браузер скачает файл, а не попытается отрисовать.

Без mime.types сайт ломается молча. CSS уедет как text/plain, и браузер не применит стили. JS может не выполниться — особенно при включённом X-Content-Type-Options: nosniff, который запрещает браузеру угадывать тип. Картинки начнут скачиваться вместо показа, а JSON API сломается, если отдать его как text/html.

fastcgi_params, fastcgi.conf и соседи

Наборы переменных для протоколов общения с приложением:

ФайлПротоколДля чего
fastcgi_params / fastcgi.confFastCGIСвязь с PHP-FPM
scgi_paramsSCGIАналог, встречается редко
uwsgi_paramsuWSGIPython-приложения на uWSGI
# Так Nginx передаёт контекст запроса в PHP — это и есть $_SERVER
fastcgi_param  QUERY_STRING     $query_string;
fastcgi_param  REQUEST_METHOD   $request_method;
fastcgi_param  CONTENT_TYPE     $content_type;
fastcgi_param  REQUEST_URI      $request_uri;
fastcgi_param  DOCUMENT_ROOT    $document_root;
fastcgi_param  REMOTE_ADDR      $remote_addr;
fastcgi_param  HTTPS            $https if_not_empty;
Разница между двумя файлами — ровно одна строка. diff на реальной установке показывает, что fastcgi.conf — это fastcgi_params плюс:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

То есть в fastcgi.conf путь к PHP-скрипту уже прописан, а при include fastcgi_params его надо дописывать самому. Поэтому обычно подключают fastcgi.conf.
location ~ \.php$ {
    include fastcgi.conf;          # SCRIPT_FILENAME уже внутри
    fastcgi_pass 127.0.0.1:9000;
}
Пустая белая страница вместо PHP — классический симптом подключённого fastcgi_params без ручного SCRIPT_FILENAME. PHP-FPM не понимает, какой файл исполнять, и отдаёт пустоту.

koi-utf, koi-win, win-utf

Таблицы перекодировки для директивы charset_map: KOI8-R ↔ UTF-8 ↔ Windows-1251. Позволяли отдавать сайт в UTF-8, когда файлы лежали в KOI8-R.

В наши дни практически не нужны — всё в UTF-8. Файлы лежат для совместимости, трогать их незачем.

servers/, var/, Cellar/ и автозапуск

ПутьЧто это
etc/nginx/servers/Твои конфиги сайтов. Подключаются строкой include servers/*; в конце http-блока — главный nginx.conf править не нужно, каждый сайт в своём файле
servers/.keepmeПустышка, чтобы папка существовала сразу после установки и не терялась как пустая
var/www/Дефолтный docroot с welcome-страницей
var/log/nginx/access.logВсе запросы
var/log/nginx/error.logОшибки — первое место, куда смотреть при отладке
var/run/nginx.pidPID мастер-процесса. По нему nginx -s reload и nginx -s stop понимают, кому слать сигнал
Cellar/nginx/1.31.7/«Погреб» — сама установленная версия. Homebrew держит их несколько и переключается между ними
bin/nginxСимлинк на актуальную версию из Cellar. Поэтому nginx работает из любой папки
Зачем Cellar плюс симлинки. Пакет ставится в свой каталог с номером версии, а в bin/, etc/ и остальное кладутся ссылки на него. Переключение версии — это пересоздание ссылок (brew link), а не переустановка. Отсюда же brew unlink / brew link при конфликте двух версий PHP.

~/Library/LaunchAgents/homebrew.mxcl.nginx.plist

launchd — механизм автозапуска macOS, аналог systemd в Linux. .plist — XML-описание: что запускать, когда и как перезапускать при падении.

Часть имениЧто означает
~/Library/LaunchAgents/Автозапуск для текущего пользователя. Общесистемный — в /Library/LaunchDaemons/, туда кладёт sudo brew services
homebrewКто поставил
mxclНик Max Howell, создателя Homebrew
nginxЧто запускается
Руками этот файл не правят — им управляет brew services start|stop|restart|list. Отсюда и разница: brew services start ставит plist в пользовательский каталог, а sudo brew services start — в системный, и только так сервис может занять порт ниже 1024.
Симлинки — как это работает

Симлинк (symbolic link, символьная ссылка) — файл-указатель на другой файл или папку. По смыслу то же, что ярлык в Windows: сам он ничего не содержит, только говорит «иди туда, там настоящий файл».

/opt/homebrew/bin/nginx → /opt/homebrew/Cellar/nginx/1.31.7/bin/nginx ↑ ↑ симлинк настоящий бинарник

Когда ты набираешь nginx, система:

  1. находит nginx в PATH — это /opt/homebrew/bin/nginx;
  2. видит, что это симлинк;
  3. переходит по нему в Cellar/nginx/1.31.7/bin/nginx;
  4. запускает настоящий бинарник.

Создать и посмотреть

# ln -s ЦЕЛЬ ИМЯ_ССЫЛКИ    (-s = symbolic)
ln -s /opt/homebrew/Cellar/nginx/1.31.7/bin/nginx /opt/homebrew/bin/nginx

# Посмотреть
ls -l /opt/homebrew/bin/nginx
# lrwxr-xr-x@ 1 observer admin 32 ... nginx -> ../Cellar/nginx/1.31.7/bin/nginx
#  ↑ первая буква l = link, стрелка показывает цель

# Куда указывает, одной командой
readlink /opt/homebrew/bin/nginx

# Перезаписать существующий симлинк (-f = force)
ln -sf /новый/путь/nginx /opt/homebrew/bin/nginx

Зачем они нужны

ЗадачаКак помогает симлинк
Переключение версийВ Cellar лежат 1.31.7 и 1.32.0, а bin/nginx — один симлинк. Обновился — Homebrew просто переставил его. Скрипты, вызывающие nginx, ничего не заметили
Короткие путиnginx -s reload вместо /opt/homebrew/Cellar/nginx/1.31.7/bin/nginx -s reload — потому что bin/ в PATH
Атомарный деплойcurrent → releases/2026-10-16. Выкатил новый релиз рядом, переставил симлинк — переключение мгновенное, откат такой же
Без дублированияОдин файл, много имён — место не тратится

Симлинк против hard link

Симлинк ln -sHard link ln
Что этоОтдельный файл-указательВторое имя того же файла
Указывает наПуть — то есть на строкуСам inode — то есть на данные
Удалили оригиналСсылка ломается (dangling)Данные живут, пока есть хоть одно имя
На папкуДаНет
Между файловыми системамиДаНет
Откуда разница. Hard link — это ещё одна запись в каталоге, указывающая на тот же inode. Файл физически удаляется, только когда счётчик имён доходит до нуля. Симлинк хранит текст пути и ничего не знает о самих данных — поэтому и работает между дисками, и ломается при переезде цели.

Сломанный симлинк

Цель удалили или переместили — ссылка остаётся, но ведёт в никуда. Такую называют dangling, «висячей».

$ ls -l /opt/homebrew/bin/nginx
lrwxr-xr-x ... nginx -> ../Cellar/nginx/1.31.7/bin/nginx   # ссылка есть

$ nginx
zsh: no such file or directory: nginx                        # цели нет

# Починить
brew link nginx          # пересоздать ссылки формулы
brew reinstall nginx     # если link не помог

# Найти все битые ссылки в каталоге
find /opt/homebrew/bin -type l ! -exec test -e {} \; -print
Симптом узнаваемый: ls файл показывает, а запуск или открытие говорит «нет такого файла». Значит указатель жив, а цель пропала.

Что в Homebrew симлинк, а что нет

Распространённое заблуждение — будто симлинками сделано всё. Это не так, и разница принципиальная.

ПутьЧто это на самом деле
/opt/homebrew/bin/nginxСимлинк → ../Cellar/nginx/1.31.7/bin/nginx
/opt/homebrew/etc/nginx/Обычный каталог с настоящими файлами
/opt/homebrew/etc/nginx/nginx.confОбычный файл
/opt/homebrew/var/log/nginx/Обычный каталог
/opt/homebrew/var/www/Обычный каталог
Почему конфиги и данные НЕ симлинкуют. Симлинки ведут в Cellar, а Cellar при обновлении формулы пересоздаётся под новую версию. Будь etc/nginx ссылкой туда — твой правленый nginx.conf улетал бы при каждом brew upgrade. Поэтому конфиги и var/ копируются один раз и живут отдельно от версий. Ровно отсюда и берутся файлы *.default: обновляется эталон, а твой рабочий файл не трогают.
Правило: симлинками делают то, что принадлежит версии пакета — бинарники, библиотеки. Обычными файлами лежит то, что принадлежит тебе — конфиги, логи, данные.
brew switch больше нет. Команда удалена из современного Homebrew — в старых статьях она ещё встречается. Переключение версий сейчас: brew unlink nginx && brew link nginx@1.31, либо установка версионной формулы, если она есть. Посмотреть, что лежит в Cellar: ls /opt/homebrew/Cellar/nginx/.
Laravel на macOS: конфиг сайта + PHP-FPM

PHP-FPM из Homebrew по умолчанию слушает TCP-порт 9000, а не unix-сокет — в отличие от Debian, где обычно /run/php/php8.3-fpm.sock. Это первое, обо что спотыкаются при переносе конфига с сервера на мак.

# Ставим PHP (придёт вместе с php-fpm) и запускаем
brew install php@8.3
brew services start php@8.3

# Проверить, на чём слушает
grep "^listen =" /opt/homebrew/etc/php/8.3/php-fpm.d/www.conf
# listen = 127.0.0.1:9000

Конфиг сайта кладём в servers/ — например /opt/homebrew/etc/nginx/servers/myapp.conf:

server {
    listen 8000;
    server_name localhost;

    # У Laravel root ВСЕГДА указывает на public/
    root /Users/username/projects/myapp/public;
    index index.php;

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

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass 127.0.0.1:9000;   # на Debian тут был бы unix-сокет
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
# Проверить синтаксис и применить
nginx -t
brew services restart nginx
Права доступа к домашней папке. Nginx работает от твоего пользователя, так что до ~/projects он дотянется. Но если проект лежит в ~/Documents, ~/Desktop или ~/Downloads, macOS может заблокировать доступ своей защитой приватности — в логе будет Permission denied при живых правах файла. Держи локальные проекты вне этих трёх папок.
Отладка — всегда в таком порядке: nginx -t проверит синтаксис → brew services list покажет, поднят ли сервис → tail -f /opt/homebrew/var/log/nginx/error.log скажет, что именно не так. На macOS journalctl нет, лог — единственный источник правды.
Куда класть свой Laravel-проект

Соглашение: /var/www/example.com/ — корень проекта, /var/www/example.com/public/ — то, что Nginx смотрит как root (там лежит index.php). Владелец — www-data:www-data (пользователь, под которым бегает Nginx и PHP-FPM на Ubuntu/Debian).

sudo mkdir -p /var/www/example.com
sudo chown -R $USER:www-data /var/www/example.com
cd /var/www/example.com

# Клонируем проект
git clone git@github.com:youruser/yourrepo.git .

# Права: storage и bootstrap/cache должны быть writable для www-data
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
«500 Internal Server Error» после deploy — в 90% случаев права. Laravel хочет писать в storage/logs/, storage/framework/, bootstrap/cache/. Проверяй владельца и chmod.
Управление сервисом — start/stop/restart/reload
Основные команды systemctl
КомандаЧто делаетКогда применять
sudo systemctl status nginxСтатус: active/inactive/failed, PID, недавние логиПервое что делать, если «сайт лёг»
sudo systemctl start nginxЗапуститьПосле установки или ручной остановки
sudo systemctl stop nginxПолностью остановитьОбслуживание, обновление системы
sudo systemctl restart nginxПерезапустить (stop + start). Есть даунтайм ~1 секПри проблемах, после серьёзных изменений
sudo systemctl reload nginxПеречитать конфиг без даунтайма: старые worker'ы дорабатывают, новые с новым конфигомПосле правки конфига — всегда reload, не restart
sudo systemctl enable nginxАвтозапуск при загрузкеОдин раз после установки
sudo systemctl disable nginxУбрать из автозапускаРедко
Reload vs restart: reload отправляет SIGHUP — Nginx запускает новых worker'ов с новым конфигом, а старые дорабатывают текущие запросы и умирают. Ноль даунтайма. restart — грубее, есть короткий момент недоступности.
ГЛАВНАЯ команда — nginx -t

Всегда проверяй конфиг ДО перезапуска:

sudo nginx -t

Что покажет:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Или с ошибкой:

nginx: [emerg] "server" directive is not allowed here in /etc/nginx/sites-enabled/example.com:15
nginx: configuration file /etc/nginx/nginx.conf test failed
Никогда не делай systemctl reload nginx без nginx -t. Если в конфиге ошибка — reload провалится, Nginx останется на старом конфиге (это ок), но если ты сделаешь restart с битым конфигом — Nginx не запустится, сайт лёг.

Идиоматический workflow:

# 1. Правим конфиг
sudo nano /etc/nginx/sites-available/example.com

# 2. Проверяем синтаксис
sudo nginx -t

# 3. Если ок — reload
sudo systemctl reload nginx

# Одной командой (сначала проверка, потом reload):
sudo nginx -t && sudo systemctl reload nginx
Nginx-сигналы напрямую (без systemd)

До systemd управляли через kill -СИГНАЛ <master-pid>. Иногда полезно понимать, что делает reload:

СигналДействиеАналог systemd
SIGTERM, SIGINTБыстрый выход (обрывает текущие запросы)systemctl stop
SIGQUITGraceful shutdown — worker'ы дорабатывают запросы—
SIGHUPПеречитать конфиг, graceful reloadsystemctl reload
SIGUSR1Переоткрыть лог-файлы (нужно после logrotate)—
SIGUSR2Обновить исполняемый файл (upgrade nginx на лету)—
# PID главного процесса
cat /var/run/nginx.pid

# Ручной reload через сигнал (эквивалент systemctl reload)
sudo kill -HUP $(cat /var/run/nginx.pid)

# После logrotate — переоткрыть логи
sudo kill -USR1 $(cat /var/run/nginx.pid)
Диагностика: сервис лёг

Приложение не отвечает — по шагам:

  1. sudo systemctl status nginx — что говорит systemd. active (running) — Nginx жив, дело в бэкенде. failed — Nginx не запускается.
  2. sudo journalctl -u nginx -n 50 --no-pager — последние 50 строк лога сервиса. Причина падения обычно в первой красной строке.
  3. sudo tail -f /var/log/nginx/error.log — живой поток ошибок Nginx. Показывает 502/504/permission denied/upstream timed out.
  4. sudo nginx -t — если правил конфиг перед падением, проверь синтаксис.
  5. ss -tlnp | grep -E ':80|:443' — что вообще слушает эти порты (может, Apache занял).
  6. sudo systemctl status php8.3-fpm — если Nginx работает, но 502 — умер PHP-FPM.
Мнемоника: «упало → status → journalctl → error.log → nginx -t». Четыре команды закрывают 90% диагностики.
nginx.conf — базовые директивы
Три уровня контекста в конфиге

Nginx-конфиг иерархический — директивы живут в разных контекстах:

main context (глобальный) ├── user, worker_processes, error_log — уровень процесса │ ├── events { ← настройки I/O │ worker_connections 1024; │ } │ └── http { ← ВСЁ про HTTP include mime.types; gzip on; keepalive_timeout 65; server { ← виртуальный хост (сайт) listen 80; server_name example.com; location / { ← правила для конкретного URL root /var/www/example.com/public; } } server { ... } ← ещё один сайт }

Директивы наследуются от родителя к потомку: gzip on в http — включает gzip во всех server внутри.

Полный nginx.conf с пояснениями
# /etc/nginx/nginx.conf — главный конфиг

# Под каким пользователем работают worker-процессы
user www-data;

# Кол-во worker'ов. auto = равно кол-ву CPU-ядер (оптимально)
worker_processes auto;

# PID главного процесса — где записан
pid /run/nginx.pid;

# Динамические модули (brotli, geoip и т.д.)
include /etc/nginx/modules-enabled/*.conf;

events {
    # Сколько соединений на один worker (не то же, что клиентов!)
    worker_connections 1024;
    # worker_connections × worker_processes = максимум соединений сервера
    # 1024 × 4 CPU = 4096 одновременных клиентов
}

http {
    # ───── I/O оптимизации ─────
    sendfile on;              # Ядро копирует файл сразу в сокет — быстро
    tcp_nopush on;            # Отправить заголовок и данные одним пакетом
    tcp_nodelay on;           # Не буферизовать (нужно для keep-alive)

    # ───── Таймауты ─────
    keepalive_timeout 65;     # Держать соединение открытым 65 сек
    client_body_timeout 12;   # Ждать тело запроса 12 сек
    client_header_timeout 12; # Ждать заголовки 12 сек
    send_timeout 10;          # Между двумя send'ами клиенту

    # ───── Размеры ─────
    client_max_body_size 20M; # !! Максимальный upload (по умолчанию 1M — часто мало)
    client_body_buffer_size 16K;
    large_client_header_buffers 4 8k;

    # ───── MIME-типы ─────
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # ───── Скрыть версию Nginx (безопасность) ─────
    server_tokens off;

    # ───── Gzip компрессия ─────
    gzip on;
    gzip_vary on;               # Vary: Accept-Encoding — важно для CDN/кешей
    gzip_min_length 1024;      # Не жать файлы меньше 1KB (накладные больше выигрыша)
    gzip_comp_level 5;         # 1..9. 5 — золотая середина CPU/размер
    gzip_types
        text/plain text/css application/json application/javascript
        text/xml application/xml application/xml+rss text/javascript
        application/vnd.ms-fontobject application/x-font-ttf font/opentype
        image/svg+xml image/x-icon;

    # ───── Логи ─────
    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';
    access_log /var/log/nginx/access.log main;
    error_log  /var/log/nginx/error.log warn;

    # ───── Подключаем конфиги сайтов ─────
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}
Главные директивы, которые крутят чаще всего
ДирективаЧто делаетРеальный кейс
worker_processes auto;Worker'ов = кол-во CPUОставь auto — Nginx сам определит
worker_connectionsСоединений на worker1024–4096. Больше = больше RAM
keepalive_timeoutДержать соединение живым65с — стандарт. 5с — если под DDoS
client_max_body_sizeМакс размер запроса/uploadСтавь явно (20M/50M). Дефолт 1M — сломает загрузку файлов
gzip on;Сжатие ответаВсегда on для текста, off для картинок (они уже сжаты)
server_tokens off;Скрыть версию nginx в Server-заголовкеВсегда off на проде — security
error_log ... warn;Уровень логовwarn на проде, debug для отладки (только временно!)
client_max_body_size — самая частая забытая настройка. Дефолт 1MB. Загрузка аватара 2MB → 413 Payload Too Large. Ставь в http {} глобально (20M) или в конкретном location /upload {}.
Про worker_processes auto: Nginx с CPU 4-cores создаст 4 worker'а. Каждый может обслуживать до worker_connections. Итого лимит одновременных клиентов = worker_processes × worker_connections. Ядро при этом должно уметь держать столько дескрипторов — проверь ulimit -n.
Server blocks — виртуальные хосты
Что такое server block

Server block = виртуальный хост = конфигурация одного сайта. На одном сервере с одним IP могут крутиться десятки сайтов — Nginx различает их по заголовку Host, который присылает браузер.

Каждый сайт — свой файл в /etc/nginx/sites-available/, симлинк в sites-enabled/.

Минимальный server block
# /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;                    # IPv6
    server_name example.com www.example.com;

    root /var/www/example.com/public;
    index index.html index.htm index.php;

    location / {
        try_files $uri $uri/ =404;
    }
}
Ключевые директивы server-блока
ДирективаЧто делаетПримеры
listenНа каком порту/интерфейсе слушать80, 443 ssl http2, 127.0.0.1:8080, [::]:80
server_nameПо каким доменам этот сайт отвечаетexample.com www.example.com *.example.com
rootКорневая директория файлов/var/www/example.com/public
indexФайл по умолчанию для директорииindex.php index.html
access_log / error_logСвои логи для этого сайта/var/log/nginx/example.access.log
returnБыстрый ответ (редирект / статус)return 301 https://$host$request_uri;
Как Nginx выбирает server-блок для запроса

Пришёл запрос GET /page HTTP/1.1 с Host: example.com. Nginx:

  1. Смотрит на порт и IP из listen. Отбрасывает блоки, которые не слушают этот адрес.
  2. Из оставшихся сравнивает заголовок Host с server_name:
    • Точное совпадение (example.com)
    • Затем wildcard в начале (*.example.com)
    • Затем wildcard в конце (example.*)
    • Затем regex
  3. Если ничего не подошло — берётся default_server (в listen 80 default_server;), либо первый server-блок в конфиге.
Ловушка: при переходе на HTTPS не забудь про default_server. Иначе если кто-то попадёт на IP сервера без домена — увидит случайный сайт.
Типичный сценарий: редирект HTTP → HTTPS + www → non-www
# Блок 1: HTTP → HTTPS редирект
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Один return, всё остальное — незачем
    return 301 https://example.com$request_uri;
}

# Блок 2: www → non-www на HTTPS
server {
    listen 443 ssl http2;
    server_name www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    return 301 https://example.com$request_uri;
}

# Блок 3: основной сайт
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com/public;
    index index.php;

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

    # PHP — см. раздел про PHP-FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}
Практика: три блока — HTTP→HTTPS, www→non-www на HTTPS, основной. Три файла в sites-available/ либо один общий с тремя server{} — вопрос вкуса. Один файл проще для одного сайта.
Location блоки — маршрутизация запросов
Что такое location

location — правило для конкретного URL-пути внутри server. По одному URL может подходить несколько location'ов — Nginx выбирает один по приоритету.

5 типов location — по приоритету выбора
СинтаксисЧто означаетПриоритет
location = /pathТочное совпадение URI1 (высший)
location ^~ /pathПрефикс, при совпадении не проверять regex дальше2
location ~ patternRegex, case-sensitive3
location ~* patternRegex, case-insensitive3
location /pathОбычный префикс (самое длинное совпадение выигрывает)4
Правило: сначала Nginx ищет самый длинный префикс. Если ^~ — берёт его, дальше не смотрит. Иначе идёт по regex сверху вниз, первый совпавший выигрывает. Если regex не совпал — возвращается к найденному префиксу.
Практические примеры
server {
    listen 80;
    server_name example.com;
    root /var/www/example.com/public;

    # 1. Точное — favicon.ico. Отдать без логирования.
    location = /favicon.ico {
        access_log off;
        log_not_found off;
    }

    # 2. Префикс с приоритетом ^~ — вся /admin/ через специальный handler
    location ^~ /admin/ {
        auth_basic "Admin area";
        auth_basic_user_file /etc/nginx/.htpasswd;
    }

    # 3. Regex case-insensitive — статика с кешем на год
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # 4. Regex — PHP-файлы через FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    # 5. Дефолтный префикс — Laravel-роутинг: не файл? → index.php
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}
try_files — магия Laravel-роутинга

try_files — пробует найти файлы по очереди, отдаёт первый существующий. Последний аргумент — что делать, если ничего не нашлось.

try_files $uri $uri/ /index.php?$query_string;

Разбор: для запроса GET /blog/hello-world:

  1. $uri = /blog/hello-world — есть такой файл в root? Нет.
  2. $uri/ = /blog/hello-world/ — есть такая директория? Нет.
  3. Fallback: передать в /index.php?$query_string — Laravel возьмёт URL из REQUEST_URI и разберётся сам через свой роутер.
Это единственная строка, которая нужна для Laravel-роутинга. Без try_files Nginx будет отдавать 404 на любой URL кроме /, потому что физических файлов /blog/hello-world нет — они генерируются PHP.
root vs alias

Обе задают путь на диске, но по-разному считают URL:

root /var/www/sitealias /var/www/site
Полный путь root + полный URI alias + URI без префикса location
Запрос /images/photo.jpg ищет /var/www/site/images/photo.jpg для location /images/: ищет /var/www/site/photo.jpg
Типичное применение Корень сайта «Замапить» URL-путь на директорию с другим именем
# root — доступ к /static/logo.png → /var/www/site/static/logo.png
location /static/ {
    root /var/www/site;
}

# alias — тот же URL /static/logo.png → /var/www/assets/logo.png
location /static/ {
    alias /var/www/assets/;   # !! обязательно / в конце
}
Классическая ошибка с alias: забыть / в конце пути. С alias /var/www/assets; (без слэша) для запроса /static/logo.png Nginx будет искать /var/www/assetslogo.png — 404.
Реальные боли с location
1. Дублирующие правила. Два location ~ \.php$ в одном server-блоке — Nginx возьмёт первый. Отладь через rewrite_log on; и error_log ... notice;.
2. Regex переопределяет корневой location. location ~* \.(jpg|png)$ может украсть запрос, который ты хотел отдать через /. Правило приоритета — regex > префикс, если нет ^~.
3. rewrite ^/(.*)$ /index.php last; — устаревший синтаксис. В современном Nginx для Laravel используй try_files. rewrite оставь для сложных редиректов.
4. Слэш в конце и без слэша — разные location. location /api и location /api/ — не одно и то же. Для API обычно /api/ с трейлингом.
5. Забыт internal; для служебных путей. Если сделал location /storage/ и оттуда через X-Accel-Redirect отдаёшь файлы — обязательно internal;, иначе клиент попадёт напрямую.
PHP-FPM для Laravel — как это связано
Что такое PHP-FPM

PHP-FPM (FastCGI Process Manager) — отдельный сервис, который держит пул PHP-процессов и обрабатывает запросы по протоколу FastCGI. Nginx не умеет сам исполнять PHP — он передаёт запрос PHP-FPM'у, тот выполняет index.php, возвращает результат, Nginx отдаёт клиенту.

Browser │ HTTP GET /users/1 ▼ ┌───────┐ 1) *.php? Да │ NGINX │──────────────────────┐ └───────┘ │ ▲ │ FastCGI (unix socket / TCP) │ 4) HTTP response ▼ │ ┌──────────┐ └─────────────────────│ PHP-FPM │ выполняет index.php, 3) HTML/JSON └──────────┘ бегает Laravel-роутер
Установка PHP-FPM (Ubuntu 24.04)
# Добавляем PPA от Ondrej (все свежие версии PHP)
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update

# Ставим PHP 8.3 FPM + типовые расширения для Laravel
sudo apt install -y \
    php8.3-fpm php8.3-cli \
    php8.3-mysql php8.3-pgsql php8.3-sqlite3 \
    php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip \
    php8.3-gd php8.3-bcmath php8.3-intl php8.3-redis

# Автозапуск + старт
sudo systemctl enable php8.3-fpm
sudo systemctl start php8.3-fpm

# Проверка
sudo systemctl status php8.3-fpm
php -v
Файлы PHP-FPM
/etc/php/8.3/fpm/
├── php.ini                # общий php.ini для FPM (memory_limit, upload_max_filesize)
├── php-fpm.conf           # глобальные настройки FPM
└── pool.d/
    └── www.conf           # настройки пула по умолчанию (user, socket, pm.*)

/run/php/
└── php8.3-fpm.sock        # unix-сокет, через который Nginx стучится к FPM

/var/log/php8.3-fpm.log    # логи FPM (fatal ошибки PHP)
Ключевые настройки пула (www.conf)
[www]
user = www-data
group = www-data

# Слушать на unix-сокете (быстрее TCP на localhost)
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data

# Управление процессами
pm = dynamic          # dynamic / static / ondemand
pm.max_children = 20  # макс одновременно PHP-процессов
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500 # перезапускать процесс каждые 500 запросов (защита от утечек)

# Логи медленных запросов
slowlog = /var/log/php8.3-fpm.slow.log
request_slowlog_timeout = 5s
Как считать pm.max_children: (RAM_доступная − 1GB на ОС) ÷ (средний расход PHP-процесса, обычно 40–80MB). На сервере с 2GB RAM = (2048 − 1024) ÷ 60 ≈ 17 процессов. Если поставить больше — сервер начнёт свопиться при пиках.
Как Nginx стучится в PHP-FPM

Через unix-сокет (рекомендуется, если Nginx и FPM на одной машине):

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Через TCP (когда FPM на другом сервере):

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass 127.0.0.1:9000;
}

Содержимое snippets/fastcgi-php.conf — стандартный шаблон Ubuntu:

# Regex splits PATH_INFO from SCRIPT_FILENAME
fastcgi_split_path_info ^(.+?\.php)(/.*)$;

# Проверка что файл существует ДО передачи в FPM
try_files $fastcgi_script_name =404;

# Передаваемые FastCGI-параметры
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi_params;
Полный Laravel server-блок с PHP-FPM
server {
    listen 80;
    server_name example.com;
    root /var/www/example.com/public;
    index index.php;

    client_max_body_size 20M;
    charset utf-8;

    # Все URL Laravel: файл нет → отдать в index.php
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # favicon и robots — молча, без логов
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    # PHP через FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_read_timeout 300;   # для долгих операций (импорт CSV и т.п.)
    }

    # Запретить доступ к .htaccess и любым скрытым файлам
    location ~ /\.(?!well-known).* {
        deny all;
    }

    error_log /var/log/nginx/example.com.error.log;
    access_log /var/log/nginx/example.com.access.log;
}
Классические проблемы PHP-FPM + Nginx
1. 502 Bad Gateway. В 90% случаев PHP-FPM не запущен: sudo systemctl status php8.3-fpm. Реже — Nginx стучится не по тому сокету/порту.
2. connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied). Nginx (пользователь www-data) не может читать сокет. Проверь listen.owner / listen.group в www.conf.
3. 504 Gateway Timeout. PHP работает дольше fastcgi_read_timeout (по умолчанию 60с). Увеличь для тяжёлых операций либо унеси в очередь (Laravel Queue).
4. Меняешь php.ini — не применяется. Правишь /etc/php/8.3/cli/php.ini, а FPM читает /etc/php/8.3/fpm/php.ini. И после правки обязательно sudo systemctl reload php8.3-fpm.
5. Несколько версий PHP. Если стоят 8.1 и 8.3 — fastcgi_pass может указывать не на ту. Проверь ls /run/php/.
Мнемоника «502 → 4 команды»:
1. sudo systemctl status php8.3-fpm
2. sudo tail -f /var/log/php8.3-fpm.log
3. sudo tail -f /var/log/nginx/error.log
4. ls -la /run/php/php8.3-fpm.sock (владелец www-data?)
Статика + gzip + expires
В разработке. Раздел про эффективную отдачу статики: gzip уровень сжатия, brotli, expires-заголовки для CSS/JS/шрифтов/картинок, immutable-кеш с хешами в имени файла, отключение логирования для favicon/robots.
Reverse proxy — Node.js, Python, любой бэкенд
Forward proxy против reverse proxy

Оба называются «прокси» и оба пересылают трафик, но стоят по разные стороны и решают противоположные задачи.

Одной фразой. Forward proxy представляет клиента, reverse proxy представляет сервер. Forward — изнутри наружу, reverse — снаружи внутрь.
FORWARD PROXY (прямой, «обычный») Клиент ──→ Forward Proxy ──→ Интернет · стоит на стороне клиента · клиент настраивает его сам · скрывает КЛИЕНТА от внешних серверов · трафик идёт изнутри наружу REVERSE PROXY (обратный) Интернет ──→ Reverse Proxy ──→ Бэкенд (Node.js, Python, Java, PHP-FPM) · стоит на стороне сервера · клиент обычно не знает о его существовании и думает, что говорит напрямую с сайтом · скрывает СЕРВЕР от интернета · трафик идёт снаружи внутрь
Forward proxyReverse proxy
Чью сторону представляетКлиентаСервер
Где стоитРядом с клиентомПеред бэкендом
Кто его настраиваетСам клиентВладелец сервера
Кто о нём знаетКлиент знает, сервер — нетСервер знает, клиент — нет
Кого скрываетКлиентаСервер
НаправлениеИзнутри наружуСнаружи внутрь
Типичное применениеКорпоративный фильтр, обход блокировок, кеш исходящих запросов, анонимностьТерминация TLS, балансировка, отдача статики, защита бэкенда, единая точка входа
ПримерыSquid, корпоративный HTTP-прокси, VPN-шлюзNginx, HAProxy, Traefik, Cloudflare
Аналогия. Forward proxy — личный курьер: ты даёшь ему поручение, он идёт в магазин вместо тебя, и магазин видит курьера, а не тебя. Reverse proxy — ресепшен в бизнес-центре: посетитель приходит «в компанию», говорит с ресепшеном, и понятия не имеет, в каком кабинете на каком этаже сидит нужный сотрудник.
Почему Nginx — почти всегда reverse. Node.js, Python и Go умеют слушать порт сами, и сайт в принципе работает без Nginx. Но тогда прямо в интернет торчит приложение: оно само занимается TLS, медленными клиентами, отдачей статики и лимитами. Reverse proxy забирает всё это на себя и оставляет приложению только бизнес-логику.
proxy_pass — базовая схема
server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;

        # Без этих заголовков бэкенд увидит вместо клиента сам Nginx
        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_pass меняет поведение. proxy_pass http://app:3000; передаёт URI как есть. proxy_pass http://app:3000/; — со слешем — отрезает совпавшую часть location. Для location /api/ запрос /api/users уйдёт как /users. Перепутал — получишь 404 на ровном месте.
Заголовки: кто на самом деле клиент

После проксирования бэкенд видит TCP-соединение от Nginx. Настоящий IP, хост и схему нужно передать явно.

ЗаголовокЧто несёт
HostИсходный домен. Без него бэкенд сгенерирует ссылки на 127.0.0.1
X-Real-IPIP клиента одним значением
X-Forwarded-ForЦепочка IP через запятую; $proxy_add_x_forwarded_for дописывает текущий к уже имеющимся
X-Forwarded-Protohttp или https. Без него Laravel за TLS-терминацией будет строить ссылки по http и ломать редиректы
Доверять этим заголовкам можно только от своего прокси. Клиент может прислать X-Forwarded-For сам и подделать IP. В Laravel за это отвечает TrustProxies: пока прокси не перечислен как доверенный, фреймворк эти заголовки игнорирует — и это правильное поведение по умолчанию.
upstream — группа бэкендов
upstream backend {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;

    # Держать соединения открытыми — экономит TCP-рукопожатия
    keepalive 32;
}

server {
    location / {
        proxy_pass http://backend;

        # keepalive к upstream работает только с HTTP/1.1 и пустым Connection
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}
Почему proxy_http_version 1.1 обязателен. По умолчанию Nginx ходит к upstream по HTTP/1.0, где keep-alive не поддерживается — директива keepalive 32 в этом случае не даёт ничего. А Connection "" нужен, чтобы не ушёл унаследованный Connection: close.
WebSocket-проксирование

WebSocket начинается как обычный HTTP-запрос с просьбой сменить протокол. Nginx должен эту просьбу пропустить, иначе соединение оборвётся на рукопожатии.

location /ws {
    proxy_pass http://127.0.0.1:6001;

    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host       $host;

    # Иначе Nginx разорвёт простаивающий сокет через 60 секунд
    proxy_read_timeout 3600s;
}
Молчащий WebSocket рвётся по таймауту. Дефолтный proxy_read_timeout — 60 секунд. Соединение без трафика Nginx закроет, и клиент увидит обрыв без видимой причины. Лечится увеличением таймаута или heartbeat-пингами со стороны приложения.
Таймауты и типичные ошибки
ДирективаПо умолчаниюЗа что отвечает
proxy_connect_timeout60sУстановка соединения с бэкендом. Больше 75s ставить бессмысленно
proxy_send_timeout60sПередача запроса бэкенду
proxy_read_timeout60sОжидание ответа. Именно он даёт 504 на долгих операциях
ОшибкаПричина
502 Bad GatewayБэкенд не отвечает: не запущен, слушает другой порт, упал
504 Gateway TimeoutБэкенд жив, но не уложился в proxy_read_timeout
Ссылки ведут на 127.0.0.1Не передан Host
В логах один и тот же IPНе передан X-Real-IP / X-Forwarded-For
Бесконечный редирект на httpsНе передан X-Forwarded-Proto: приложение считает, что пришли по http
SSL/TLS + Let's Encrypt
Что такое TLS и при чём тут HTTPS

TLS (Transport Layer Security) — криптографический протокол, который даёт три вещи:

СвойствоЧто гарантирует
ШифрованиеДанные между клиентом и сервером нельзя прочитать по пути
ЦелостностьДанные нельзя незаметно подменить
АутентификацияКлиент убеждается, что сервер действительно тот, за кого себя выдаёт — через TLS-сертификат
HTTPS — это просто HTTP поверх TLS. Тот же самый HTTP-запрос, только внутри зашифрованного TLS-канала. Никакого отдельного «протокола HTTPS» не существует.
Почему «SSL» всё ещё везде пишут. SSL — предшественник TLS, последняя версия SSL 3.0 признана небезопасной ещё в 2015-м. Сейчас везде используется TLS, но слово «SSL» осталось в названиях директив (ssl_certificate), команд и в обиходе. Когда говорят «SSL-сертификат» — имеют в виду TLS.
TLS termination — прокси разрывает шифрование

Самая частая схема в проде: HTTPS заканчивается на reverse proxy, а дальше внутрь идёт обычный HTTP.

Клиент ──HTTPS/TLS──→ Reverse proxy ──HTTP──→ Бэкенд (Nginx) (Node.js, Python, держит Java, PHP-FPM) сертификаты

По шагам:

  1. Прокси держит сертификаты. Например для example.com. Сертификат содержит публичный ключ и подпись удостоверяющего центра (CA).
  2. Клиент подключается по HTTPS. Происходит TLS handshake: стороны договариваются о шифрах, клиент проверяет сертификат, обмениваются ключами.
  3. Прокси «разбирает» TLS — расшифровывает входящий трафик. Это и называется терминацией.
  4. Дальше идёт расшифрованный HTTP — обычно во внутреннюю сеть: http://backend:3000 или к PHP-FPM.
  5. Бэкенд не думает про сертификаты. Ему не нужно настраивать HTTPS, обновлять сертификаты и следить за версиями TLS — он получает обычный HTTP-запрос.
Зачем так делаютЧто это даёт
Централизованные сертификатыОдин прокси вместо десяти бэкендов
АвтопродлениеLet's Encrypt через Nginx, Caddy, Traefik — в одном месте
Снижение нагрузкиШифрование ложится на прокси, а не на приложение
Единая точка безопасностиУдобно вешать WAF, rate limit, логирование
Простота для бэкендаПриложение слушает обычный HTTP
После терминации трафик до бэкенда идёт незашифрованным. Это нормально, пока прокси и бэкенд в одной доверенной сети. Если сеть недоверенная — делают re-encryption: прокси расшифровывает TLS от клиента и снова шифрует до бэкенда по HTTPS. Либо поднимают mTLS, где стороны проверяют сертификаты друг друга.
ТерминЗначение
TLS terminationПрокси завершает TLS, дальше идёт HTTP
TLS offloadingТот же процесс с акцентом на снятие нагрузки с бэкенда
SSL terminationУстаревшее название того же самого — SSL давно мёртв, термин остался
Re-encryptionРасшифровал от клиента → зашифровал заново до бэкенда
TLS passthroughПрокси вообще не расшифровывает, пробрасывает TLS целиком на бэкенд
Бэкенд должен знать, что снаружи был HTTPS. После терминации приложение видит обычный HTTP и будет строить ссылки по http:// — отсюда бесконечные редиректы и «mixed content». Лечится заголовком X-Forwarded-Proto $scheme из раздела про reverse proxy плюс TrustProxies на стороне Laravel.
Let's Encrypt через certbot
# Установка (Debian/Ubuntu)
sudo apt install certbot python3-certbot-nginx -y

# Выпуск сертификата — certbot сам правит конфиг Nginx и перезагружает его
sudo certbot --nginx -d example.com -d www.example.com

# Только выпустить, конфиг не трогать
sudo certbot certonly --nginx -d example.com

# Проверить автопродление вхолостую
sudo certbot renew --dry-run

# Список выпущенных
sudo certbot certificates
Автопродление настраивается само. Пакет ставит systemd-таймер certbot.timer (или cron), который дважды в сутки проверяет срок. Сертификат Let's Encrypt живёт 90 дней, обновляется примерно за 30 до конца. Проверить таймер: systemctl list-timers | grep certbot.
Для выпуска нужен работающий HTTP на 80 порту. HTTP-01 challenge кладёт файл в /.well-known/acme-challenge/ и забирает его снаружи. Если 80-й закрыт файрволом или весь трафик редиректится на HTTPS без исключения для этого пути — выпуск упадёт.
Конфиг Nginx с TLS
# HTTP — только редирект на HTTPS
server {
    listen 80;
    server_name example.com www.example.com;

    # Исключение для certbot, иначе продление сломается
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com www.example.com;

    # Сертификат и приватный ключ
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Только современные версии — 1.0 и 1.1 устарели и небезопасны
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;

    # Кеш сессий — меньше полных handshake'ов
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP stapling — сервер сам прикладывает ответ о валидности
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

    # HSTS — браузер запомнит, что сюда только по HTTPS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    root /var/www/example.com/public;
    index index.php;
}
Файл Let's EncryptЧто это
fullchain.pemСертификат сайта + промежуточные CA. Именно его указывают в ssl_certificate
privkey.pemПриватный ключ. Наружу не отдаётся никогда
chain.pemТолько промежуточные CA — нужен для OCSP stapling
cert.pemТолько сертификат сайта, без цепочки. Для Nginx не подходит
Частая ошибка: указать cert.pem вместо fullchain.pem. В браузере всё будет выглядеть нормально, а мобильные приложения и curl начнут ругаться на недоверенный сертификат — без промежуточных CA цепочка не выстраивается.
HSTS и подводные камни
HSTS необратим на время max-age. Выставил max-age=31536000 — браузеры год будут ходить только по HTTPS, игнорируя твой конфиг. Сломался сертификат — сайт недоступен, и «просто откатить на HTTP» не выйдет. Начинай с малого: max-age=300, убедись что всё работает, и только потом поднимай до года.
includeSubDomains накрывает все поддомены разом. Включая те, где HTTPS ещё нет — какой-нибудь внутренний old.example.com просто перестанет открываться.
add_header не наследуется. Если в location есть свой add_header, все заголовки из server в этом location теряются — HSTS в том числе. Поэтому и пишут always, и дублируют заголовок там, где он нужен.
Проверка конфигурации: nginx -t для синтаксиса, openssl s_client -connect example.com:443 -servername example.com для цепочки и версии TLS, curl -sI https://example.com | grep -i strict для HSTS. Публичный аудит — SSL Labs.
Безопасность + rate limiting
Что такое rate limit

Rate limiting — ограничение количества запросов от одного клиента за промежуток времени. Проще: «не больше N запросов в секунду с одного IP, пользователя или API-ключа».

  • не больше 5 запросов в секунду с одного IP;
  • не больше 100 запросов в минуту на один API-ключ;
  • не больше 10 попыток логина в минуту с одного IP.

При превышении сервер обычно возвращает 429 Too Many Requests или 503, либо задерживает запрос, либо ненадолго ставит его в очередь.

Зачем нуженОт чего защищает
БрутфорсПеребор паролей на форме логина
DDoS и флудЛавина запросов с одного источника
СкрапингМассовый парсинг контента
Перегрузка бэкендаОдин клиент не кладёт приложение целиком
Справедливое распределениеРесурсы делятся между всеми клиентами
Контроль нагрузки на APIПредсказуемый расход квот и мощностей
limit_req в Nginx
# Зона объявляется в http-блоке, один раз
limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;

server {
    location /api/ {
        limit_req zone=api burst=10 nodelay;
        limit_req_status 429;        # по умолчанию 503
        proxy_pass http://backend;
    }
}
ПараметрЧто означает
$binary_remote_addrКлюч, по которому считаем. Обычно IP клиента. Бинарная форма занимает меньше памяти, чем $remote_addr
zone=api:10mИмя зоны и объём памяти под счётчики — 10 МБ хватает примерно на 160 тысяч IP
rate=5r/sНе больше 5 запросов в секунду. Можно 30r/m для минутных лимитов
burst=10Разрешает кратковременный всплеск — до 10 запросов сверх лимита
nodelayВсплеск не задерживать, а пропускать сразу в пределах burst
limit_req_statusКод ответа при превышении. 429 честнее, чем дефолтный 503
Что реально делают burst и nodelay. Без burst лимит жёсткий: шестой запрос в ту же секунду сразу отлетает. Реальные страницы грузят десяток ресурсов пачкой, и такой лимит будет рубить обычных пользователей. burst=10 даёт очередь на 10 запросов сверх нормы. Без nodelay они исполняются с задержкой, растягиваясь под rate — пользователь ждёт. С nodelay проходят мгновенно, а место в очереди освобождается по графику. На практике почти всегда нужен burst + nodelay.
За балансировщиком или Cloudflare лимит считается неправильно. $binary_remote_addr будет содержать IP самого балансировщика, и все клиенты посчитаются как один — лимит сработает сразу для всех. Нужен real_ip_module:

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Перечислять в set_real_ip_from надо только свои доверенные прокси — иначе клиент подделает X-Forwarded-For и обойдёт лимит.
limit_req против limit_conn
limit_reqlimit_conn
ОграничиваетСкорость запросовЧисло одновременных соединений
ЕдиницаЗапросов в секунду или минутуОткрытых соединений с одного ключа
Против чегоБрутфорс, флуд, скрапингКачание файлов в сто потоков, Slowloris
limit_conn_zone $binary_remote_addr zone=addr:10m;

server {
    limit_conn addr 10;          # максимум 10 соединений с одного IP
    limit_conn_status 429;
}
Эти две директивы не заменяют друг друга — их ставят вместе. limit_req держит частоту, limit_conn — параллелизм.
Алгоритмы rate limiting
АлгоритмКак работаетГде встречается
Token bucketВ ведро капают токены с постоянной скоростью, запрос забирает токен. Накопленные токены дают всплескAPI-шлюзы, Laravel RateLimiter
Leaky bucketЗапросы наливаются в ведро, вытекают с постоянной скоростью. Переполнилось — отказNginx limit_req
Fixed windowСчётчик на фиксированное окно: 100 запросов в минуту, на границе обнуляетсяПростые реализации на Redis
Sliding windowСкользящее окно — учитывает хвост предыдущего периода, считает плавнееCloudflare, зрелые API
Слабое место fixed window. На стыке двух окон клиент может отправить двойную норму: 100 запросов в последнюю секунду первой минуты и ещё 100 в первую секунду второй. Sliding window это сглаживает — потому его и выбирают там, где лимит обещан клиенту как SLA.
Скрытие версии и IP-фильтрация
# В http-блоке: убрать версию Nginx из заголовка Server и страниц ошибок
server_tokens off;

# Доступ по IP — порядок директив важен
location /admin/ {
    allow 203.0.113.0/24;
    allow 10.0.0.5;
    deny  all;                 # всё остальное — 403
}

# Закрыть служебные файлы
location ~ /\.(?!well-known) {
    deny all;
}
Что даёт server_tokens off. Заголовок меняется с Server: nginx/1.24.0 на просто Server: nginx. Полностью убрать имя без стороннего модуля нельзя. Это не защита сама по себе — атакующий всё равно определит версию по поведению — но лишает автоматические сканеры простого способа подобрать эксплойт под конкретную версию.
Исключение для .well-known обязательно. Правило location ~ /\. закрывает всё, что начинается с точки — включая /.well-known/acme-challenge/. Продление сертификата Let's Encrypt после этого молча сломается, и узнаешь об этом через 90 дней.
HTTP security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options    "nosniff" always;
add_header X-Frame-Options           "SAMEORIGIN" always;
add_header Referrer-Policy           "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy    "default-src 'self'" always;
ЗаголовокОт чего защищает
Strict-Transport-SecurityDowngrade на HTTP и перехват на первом запросе
X-Content-Type-Options: nosniffБраузер не угадывает тип файла — загруженная «картинка» не исполнится как скрипт
X-Frame-OptionsClickjacking: сайт нельзя встроить в чужой iframe
Referrer-PolicyУтечка путей и query-параметров в Referer при переходе наружу
Content-Security-PolicyXSS: ограничивает, откуда грузятся скрипты, стили и прочее
add_header не наследуется в location. Если внутри location есть хоть один свой add_header, все заголовки из server в этом location пропадают. Поэтому и пишут always, и дублируют набор там, где он нужен.
CSP ломает сайт при неаккуратном включении. default-src 'self' отрубит все внешние CDN, инлайновые скрипты и стили. Включать через Content-Security-Policy-Report-Only, посмотреть отчёты, и только потом переводить в боевой режим.
Slowloris и медленные запросы

Slowloris — атака медленными запросами. Атакующий открывает много соединений и шлёт заголовки по байту, не завершая запрос. Соединения висят, воркеры заняты, новые клиенты не обслуживаются. Трафика почти нет — обычные анти-DDoS срабатывают плохо.

client_header_timeout 10s;   # сколько ждём заголовки целиком
client_body_timeout   10s;   # сколько ждём тело запроса
send_timeout          10s;   # пауза между записями при ответе
keepalive_timeout     15s;   # дефолт 75s — слишком щедро

client_max_body_size  20m;   # заодно от залива огромных файлов

limit_conn_zone $binary_remote_addr zone=slow:10m;
limit_conn slow 20;
Почему Nginx и так неплохо держит Slowloris. Он асинхронный: одно соединение не занимает процесс, как у Apache с prefork. Тысячи висящих соединений для Nginx не смертельны. Но лимиты всё равно ставят — чтобы не упереться в worker_connections и не пропустить атаку дальше, к бэкенду.
Nginx не заменяет безопасность приложения

Самое частое заблуждение: «настроили rate limit и headers — безопасность закрыта». Nginx и приложение защищают разные вещи и работают вместе, а не вместо друг друга.

Интернет │ ▼ [Nginx] ← ВНЕШНИЙ ПЕРИМЕТР │ rate limit, IP-фильтр, TLS, │ security headers, WAF, slowloris ▼ [Приложение] ← ВНУТРЕННЯЯ ЛОГИКА │ аутентификация, авторизация, валидация, │ CSRF, SQL-инъекции, бизнес-правила ▼ [БД]
Nginx — периметрПриложение — логика
Отсекает флуд, брутфорс, DDoSАутентификация: кто ты
Блокирует по IP и геоАвторизация: что тебе можно
Терминирует TLSВалидация входных данных
Добавляет security headersЗащита от SQL-инъекций, XSS, CSRF
Прячет версии и внутреннюю структуруБизнес-правила
Защищает от slowlorisСессии, роли, права
Режет явно мусорные запросы (WAF, ModSecurity)
Почему нельзя только Nginx. Он не знает, что пользователь №5 не должен читать документ пользователя №7 — для него это обычный валидный GET. Он не отличит легитимный запрос от хитро сформированного, если тот выглядит нормально. И он не проверит ни одного бизнес-правила.
Почему нельзя только приложение. Без лимита на периметре один клиент положит бэкенд раньше, чем приложение успеет что-то проверить. Slowloris выест воркеры. TLS на каждом бэкенде превращает управление сертификатами в головную боль. DDoS-флуд дойдёт до приложения и сожрёт ресурсы на разборе запросов, которые надо было отбросить ещё на входе.
Аналогия. Nginx — охрана на входе в здание: проверяет пропуск, не пускает толпу, смотрит металлодетектором. Приложение — сотрудник внутри, который решает, есть ли у тебя доступ к конкретному кабинету и к конкретным документам. Охрана не заменяет сотрудника, сотрудник не заменяет охрану.
Это и называется defense in depth — эшелонированная оборона. Безопасность не одна стена, а набор слоёв, каждый из которых ловит то, что пропустил предыдущий. Nginx — только один из них.
Минимальный набор для прода
http {
    server_tokens off;

    client_header_timeout 10s;
    client_body_timeout   10s;
    keepalive_timeout     15s;
    client_max_body_size  20m;

    limit_req_zone  $binary_remote_addr zone=general:10m rate=30r/s;
    limit_req_zone  $binary_remote_addr zone=login:10m   rate=5r/m;
    limit_conn_zone $binary_remote_addr zone=addr:10m;

    limit_req_status  429;
    limit_conn_status 429;

    server {
        limit_req  zone=general burst=50 nodelay;
        limit_conn addr 20;

        # Логин — отдельный жёсткий лимит против брутфорса
        location = /login {
            limit_req zone=login burst=3 nodelay;
            try_files $uri /index.php?$query_string;
        }
    }
}
Проверять лимиты до прода. ab -n 200 -c 20 https://example.com/api/ или siege — и смотри, сколько ответов пришло с кодом 429. Настроенный «на глаз» лимит одинаково часто оказывается и бесполезным, и рубящим живых пользователей.
Кеширование
Кэш бывает на нескольких уровнях сразу

Уровни кэша не заменяют друг друга, а дополняют. Запрос может быть обслужен на любом из них — чем раньше, тем дешевле.

Клиент → Nginx → Бэкенд → Redis → БД (браузер) (proxy_cache / (PHP / Node / Memcached (кэш Cache-Control fastcgi_cache) Java) APCu запросов ETag внутри СУБД) │ │ │ │ └ не идёт └ не трогает └ не идёт └ не идёт на сервер бэкенд в БД на диск вообще
УровеньЧто кэширует
Браузерный — Cache-Control, ETagГотовый ответ. Клиент вообще не идёт на сервер
Nginx — proxy_cache, fastcgi_cacheГотовый HTTP-ответ бэкенда на диске. Отдаётся без обращения к PHP или Node — самый быстрый уровень после браузера
Приложение — Redis, Memcached, APCuУже не HTTP-ответ, а данные: результат запроса к БД, сериализованный объект
СУБДВнутренний кэш запросов и страниц данных
По умолчанию Nginx не кэширует ничего. Без явной настройки он просто проксирует запросы дальше. Кэширование включается только директивами proxy_cache или fastcgi_cache — само оно не появится.
proxy_cache — кэш ответов бэкенда
# В http-блоке: где хранить и сколько памяти под ключи
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m
                 max_size=1g inactive=60m use_temp_path=off;

server {
    location / {
        proxy_cache my_cache;
        proxy_cache_valid 200 301 10m;      # успешные — 10 минут
        proxy_cache_valid 404 1m;               # ошибки — минуту

        # Не отдавать кэш авторизованным
        proxy_cache_bypass $cookie_session;
        proxy_no_cache     $cookie_session;

        # Удобно видеть в ответе HIT / MISS / BYPASS
        add_header X-Cache-Status $upstream_cache_status;

        proxy_pass http://backend;
    }
}
ПараметрЧто делает
levels=1:2Двухуровневая раскладка по подпапкам — тысячи файлов в одной директории тормозят ФС
keys_zone=my_cache:10mИмя зоны и память под ключи: 10 МБ ≈ 80 тысяч записей
max_size=1gПотолок кэша на диске, дальше вытесняется по LRU
inactive=60mНе запрашивали час — удаляется, даже если срок валидности не истёк
proxy_cache_bypassНе читать из кэша при истинном значении
proxy_no_cacheНе записывать в кэш. Нужны обе — по отдельности толку мало
proxy_cache_bypass без proxy_no_cache — утечка чужих данных. Первая директива запрещает только чтение. Ответ авторизованного пользователя всё равно запишется в кэш и уедет следующему анониму с тем же URL. Ставить всегда вместе.
Ключ кэша и fastcgi_cache

Ключ по умолчанию — $scheme$proxy_host$request_uri. Всё, чего в ключе нет, кэш считает одинаковым.

# Для PHP-FPM напрямую, без промежуточного HTTP-бэкенда
fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=php:10m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

location ~ \.php$ {
    fastcgi_cache php;
    fastcgi_cache_valid 200 1m;        # микрокеширование
    fastcgi_cache_bypass $cookie_laravel_session;
    fastcgi_no_cache     $cookie_laravel_session;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Микрокеширование. Даже 1 секунда кэша на горячей странице срезает нагрузку на порядки: при 1000 запросов в секунду до PHP дойдёт один. Пользователь разницы не заметит — данные устарели максимум на секунду. Это самый дешёвый способ пережить всплеск трафика.
Забыть $request_method в ключе. Тогда ответ на GET и POST по одному URL попадёт в одну ячейку. По умолчанию Nginx кэширует только GET и HEAD, но стоит добавить proxy_cache_methods — и такой ключ выстрелит.
Что кэшировать в Nginx, а что в приложении
Что кэшируемГде лучше
Статика, картинки, публичные страницыNginx proxy_cache
Персонализированные страницыВ Nginx не кэшировать вообще — только в приложении
Результаты тяжёлых SQL-запросовRedis или Memcached
API-ответы, одинаковые для всехNginx proxy_cache
API-ответы, зависящие от пользователяRedis с ключом по user_id
Граница простая. Nginx кэширует HTTP-ответ целиком — значит он должен быть одинаков для всех, кто попадёт в этот ключ. Как только ответ зависит от пользователя, уровень Nginx заканчивается и начинается кэш данных в приложении.
Инвалидация

Бесплатного способа «сбросить один URL» в открытом Nginx нет — директива proxy_cache_purge есть только в коммерческом Nginx Plus. В open source три варианта:

СпособКак
Короткий TTLНе инвалидировать вовсе: proxy_cache_valid 200 30s. Самый надёжный подход
Удалить файлПуть считается как md5 от ключа. Найти и удалить вручную или скриптом
Модуль ngx_cache_purgeСторонний модуль, даёт PURGE-запросы. Требует пересборки Nginx
Снести всёrm -rf /var/cache/nginx/* и nginx -s reload. Грубо, но работает
Отладка. Заголовок X-Cache-Status показывает, что произошло: MISS — не было в кэше, HIT — отдано из кэша, BYPASS — запрещено читать, EXPIRED — протухло и обновлено, STALE — отдали устаревшее, пока бэкенд лежал.
Nginx вместо Varnish?

На небольших и средних проектах proxy_cache часто закрывает задачу полностью. Varnish мощнее и гибче — VCL, ESI, продвинутая инвалидация, — но Nginx проще по одной причине: он уже стоит как reverse proxy, и кэш включается парой директив вместо отдельного сервиса в стеке.

Правило: начинай с proxy_cache. Переходи на Varnish, когда упрёшься в то, чего Nginx не умеет — сложная логика кэширования на VCL или сборка страницы из кусков через ESI.
Load balancing (upstream)
В разработке. upstream-блок, алгоритмы (round-robin, least_conn, ip_hash), weight, backup, max_fails, fail_timeout, health-чеки, sticky sessions.
Логи + logrotate
В разработке. Форматы log_format, кастомные поля, JSON-логи, error_log уровни (debug/info/notice/warn/error/crit), logrotate-конфиг для nginx, отправка в syslog / ELK.
Полный продакшн-конфиг для Laravel
В разработке. Готовый server-блок «под ключ» с HTTPS, редиректом с HTTP, gzip, security headers, кешем статики, PHP-FPM, комментариями по каждой строке. Опциональные блоки: horizon UI, telescope, отладочные роуты.
Реальные боли и как их лечить
В разработке. Разбор частых ошибок с примерами лога и решением: 502 Bad Gateway, 504 Gateway Timeout, 413 Payload Too Large, 499 Client Closed Request, upstream sent too big header, permission denied on socket, «сайт то работает, то нет», mixed content после SSL.
Что такое Docker + VM vs Container
В разработке. Зачем контейнеризация, разница с VM (общее ядро vs полная ОС), image vs container, слои, реестры (Docker Hub, GHCR), базовые термины.
Основные команды Docker
В разработке. docker run, docker ps, docker exec, docker logs, docker stop/rm, docker pull/images/rmi, docker inspect, docker system prune.
Dockerfile
В разработке. FROM, WORKDIR, COPY vs ADD, RUN, ENV, ARG, EXPOSE, CMD vs ENTRYPOINT, HEALTHCHECK, USER, порядок инструкций для кеша слоёв, multi-stage builds.
docker compose
В разработке. compose.yaml: services, image/build, ports, volumes, environment, env_file, depends_on + healthcheck, networks, restart policy. Команды compose up/down/logs/exec.
Volumes + bind mounts
В разработке. Named volumes vs bind mounts vs tmpfs, где физически хранятся, права/владельцы, бэкап/восстановление, volumes для БД (persistence), bind для разработки (hot reload).
Networks
В разработке. Bridge/host/none, custom networks, DNS между контейнерами по имени сервиса, порты host:container, внутренние vs внешние сети, изоляция.
Laravel в Docker
В разработке. Готовый стек: nginx + php-fpm + mysql + redis через compose. Dockerfile для PHP с расширениями. Env-переменные, миграции, сидеры, artisan-команды через exec. Laravel Sail vs собственный compose.
Реальные боли Docker
В разработке. «Контейнер выходит через секунду», permission denied на volume, порты заняты, где логи, healthcheck failed, DNS не резолвит, кеш слоёв не работает, «на моей машине работает».
systemd глубже
В разработке. Unit-файлы, кастомные сервисы (для Laravel Queue Worker, Horizon), timers как замена cron, drop-in overrides, journalctl, автоперезапуск, зависимости between units.
SSH: deploy + key auth
В разработке. Генерация ключей, ~/.ssh/config, ssh-agent + forwarding, jump host (ProxyJump), rsync/scp, SSH для git (deploy keys), запрет root login, fail2ban базово.
Cron
В разработке. crontab -e vs /etc/cron.d/, синтаксис пяти полей, env-переменные, куда пишется stdout/stderr, cron для Laravel Scheduler, systemd timers как альтернатива.
Мониторинг сервера
В разработке. top/htop/btop, iostat/vmstat/iotop, df -h/du/ncdu, ss/netstat/lsof, journalctl -f, tail -f, nload, Prometheus + Grafana в двух словах.
CI/CD базово
В разработке. GitHub Actions базово: workflow.yml, jobs, steps, secrets. Пример pipeline: push в main → build → deploy на VPS через SSH. Zero-downtime deploy pattern.
Чек-лист боевого сервера
В разработке. Итоговый чек-лист по свежей VPS: ufw, fail2ban, SSH key-only + no root, автообновления безопасности, swap, ntp, timezone, nginx + php-fpm + mysql/postgres, certbot автопродление, backups, мониторинг, log rotation, cron/scheduler.