Веб-серверы и Reverse Proxy: Nginx до последней директивы
Nginx — это «выходные двери» 90% веб-приложений мира. Балансер, кэш, статика, TLS, WAF, rate limit — всё под одной крышей. Этот урок — детальное руководство: от установки до production-конфигов, без воды и без зубрёжки.
- Что такое веб-сервер, app server и reverse proxy
- Почему именно Nginx
- Архитектура Nginx — master и workers
- Установка и запуск
- Структура конфигурации
- Server blocks — виртуальные хосты
- Location matching — самая частая ошибка
- Отдача статики: root, alias, try_files
- Reverse proxy — proxy_pass до байта
- Заголовки при проксировании
- Upstream и алгоритмы балансировки
- Буферизация и таймауты
- Кэширование
- SSL/TLS — production-grade
- HTTP/2 и HTTP/3
- Сжатие: gzip и brotli
- Rate limiting
- WebSocket и gRPC
- Логи
- Security headers
- Тюнинг производительности
- Hot reload и graceful restart
- Реальные конфиги
- Антипаттерны
- Дебаг
- Альтернативы Nginx
- Что с этим делает DevOps
- Nginx Ingress Controller в Kubernetes
- stream{} — TCP/UDP проксирование
- fastcgi_pass и uwsgi_pass
- OpenResty и Lua
- NJS — JavaScript внутри Nginx
- ModSecurity + OWASP CRS — встроенный WAF
- Mutual TLS — клиентские сертификаты
- GeoIP и split_clients
- Кейс troubleshooting: 502 Bad Gateway
- Open Source vs Nginx Plus
- Чек-лист
1. Что такое веб-сервер, app server и reverse proxy
Слова «веб-сервер», «приложение», «прокси» путаются. Чёткое разделение:
🌐 Веб-сервер
Слушает HTTP, отдаёт статический контент (HTML, CSS, JS, картинки) с диска. Когда-то это была главная функция. Примеры: Apache, Nginx, IIS, Caddy.
🛠️ Application server
Выполняет твой код приложения: Python (uWSGI/Gunicorn), Node.js, Java (Tomcat), Go (встроенный net/http), Ruby (Puma). Не оптимизирован для отдачи статики.
🔁 Reverse proxy
Принимает запросы от клиентов и проксирует их в app server. Между ними — кэш, TLS-терминация, балансировка, фильтрация.
🎯 Все три в одном
Nginx умеет быть всеми тремя одновременно: статику отдаёт сам, динамику проксирует, между ними кэш, на входе TLS+WAF.
Forward vs reverse proxy — частая путаница:
- Forward proxy — стоит перед клиентами, скрывает их (Tor, корпоративный прокси, Squid).
- Reverse proxy — стоит перед серверами, скрывает их (Nginx перед приложением).
2. Почему именно Nginx
Игорь Сысоев написал Nginx в 2004 году как замену Apache, который захлёбывался под нагрузкой. Главные плюсы:
- Event-driven архитектура. Один worker обслуживает 10К соединений на event loop вместо «thread per connection».
- Минимальное потребление памяти. 10К соединений — десятки МБ RAM, а не гигабайты.
- Скорость отдачи статики. sendfile() — копирование с диска в сокет минуя userspace.
- Декларативная конфигурация. Простой, читаемый текстовый формат.
- Hot reload. Перезагрузка конфига без потери соединений.
- Стабильность. Crash в worker не валит сервер — мастер поднимает новый.
- Universal. Веб-сервер, прокси, балансер, кэш, mail-proxy, TCP-proxy.
По данным W3Techs (2024): Nginx обслуживает 34% всех веб-сайтов. Среди топ-100к — почти 50%. Знать его — обязательно для DevOps.
3. Архитектура Nginx — master и workers
Nginx работает по модели master + N workers:
Master process
- Запускается от root (нужно для bind на <1024 портах)
- Читает конфигурацию
- Открывает listening sockets
- Форкает workers
- Управляет ими: hot reload, graceful shutdown
- Сам не обслуживает запросы
Worker process
- Работает от непривилегированного пользователя (
nginx/www-data) - Делает всю работу: принимает соединения, парсит HTTP, отдаёт ответ
- Внутри — event loop на epoll (Linux) / kqueue (BSD)
- Один worker может обслуживать тысячи параллельных соединений
- Количество =
worker_processes auto(= кол-во CPU cores)
Зачем именно так
Master/worker даёт два больших плюса:
- Безопасность. Worker работает без root — даже если уязвимость, привилегий минимум.
- Hot reload. Master запускает новые workers с обновлённым конфигом, старые дорабатывают существующие соединения и завершаются. Никаких сброшенных запросов.
4. Установка и запуск
# Ubuntu / Debian
sudo apt update
sudo apt install nginx
# RHEL / Rocky / Alma
sudo dnf install nginx
# Свежая версия (mainline) с официального репозитория
# https://nginx.org/en/linux_packages.html
# Запуск
sudo systemctl enable --now nginx
sudo systemctl status nginx
# Базовые команды
sudo nginx -t # проверить конфиг — ОБЯЗАТЕЛЬНО ПЕРЕД РЕЛОУДОМ
sudo nginx -s reload # горячая перезагрузка конфига
sudo nginx -s reopen # переоткрыть лог-файлы (для logrotate)
sudo nginx -s stop # немедленная остановка
sudo nginx -s quit # graceful shutdown
nginx -V # версия + флаги сборки
После установки Nginx уже работает. По умолчанию слушает на 80, отдаёт «Welcome to nginx!» из /var/www/html/.
5. Структура конфигурации
Главный файл — /etc/nginx/nginx.conf. Внутри — контексты (блоки) и директивы.
# MAIN context (глобальный, без обёртки)
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
# EVENTS context — настройки event loop
events {
worker_connections 4096; # макс конн на worker
multi_accept on;
use epoll; # на Linux — оптимально
}
# HTTP context — всё, что касается HTTP-уровня
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on; # нулевая копия для статики
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/json application/javascript;
# Включить все конфиги сайтов
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
# SERVER context — виртуальный хост
server {
listen 80;
server_name example.com www.example.com;
# LOCATION context — правило для конкретного path
location / {
root /var/www/html;
index index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
}
}
Иерархия контекстов
main
├── events
└── http
├── upstream
└── server (один на каждый виртуальный хост)
└── location (одна или много)
└── location (вложенные тоже можно)
Директивы наследуются вниз. То, что определено в http, действует во всех server. Но более глубокий контекст может переопределить.
Структура файлов на Ubuntu/Debian
/etc/nginx/
├── nginx.conf # главный
├── mime.types
├── conf.d/ # дополнительные http-блоки
├── sites-available/ # все конфиги сайтов (хранение)
│ ├── default
│ └── example.com
├── sites-enabled/ # symlinks на включённые
│ └── example.com -> ../sites-available/example.com
└── snippets/ # переиспользуемые куски
├── snakeoil.conf
└── fastcgi-php.conf
Включить сайт = ln -s из sites-available в sites-enabled. Выключить = удалить ссылку.
6. Server blocks — виртуальные хосты
Один Nginx обслуживает много сайтов на одном IP. Различает по заголовку Host.
server {
listen 80;
server_name example.com;
root /var/www/example.com;
}
server {
listen 80;
server_name blog.example.com;
root /var/www/blog;
}
server {
listen 80 default_server; # дефолт, если Host не подошёл
server_name _; # «catch all»
return 444; # Nginx-specific: тихий разрыв
}
server_name — нюансы
server_name example.com www.example.com— несколько имёнserver_name *.example.com— wildcard, любой поддоменserver_name ~^(?<user>[a-z]+)\.example\.com$— regex с captureserver_name _— «catch all», не валидное имя → совпадёт со всемserver_name "";— для запросов без Host (HTTP/1.0)
Алгоритм выбора server
- Совпадение по IP:port из
listen - Точное совпадение
server_name - Wildcard, начинающийся с
* - Wildcard, заканчивающийся на
* - Regex (в порядке появления в конфиге)
- Если ничего не подошло —
default_serverдля этого listen
Без default_server — первый server в порядке появления становится дефолтным. Поэтому если у тебя 50 server-блоков, и первый — «catch-all» с return 444, никакой другой работать не будет. Будь внимателен с порядком.
7. Location matching — самая частая ошибка
Внутри server — блоки location, каждый описывает поведение для какого-то path. Это самая запутанная часть Nginx.
Модификаторы и приоритет
| Синтаксис | Тип | Приоритет |
|---|---|---|
location = /exact | Точное совпадение | 1 (самый высокий) |
location ^~ /prefix | Префиксное, остановить поиск regex | 2 |
location ~ /regex | Regex case-sensitive | 3 (в порядке) |
location ~* /regex | Regex case-insensitive | 3 (в порядке) |
location /prefix | Префиксное (без модификатора) | 4 (самый длинный выигрывает) |
Алгоритм выбора
- Если есть
location = /pathи он точно совпал — взять, остановиться - Найти самый длинный префикс из
location /иlocation ^~ / - Если найден
^~— взять, остановиться - Иначе — попробовать regex'ы в порядке появления. Первый match → взять
- Если regex не нашёлся — использовать самый длинный префикс из шага 2
Пример с разбором
server {
listen 80;
server_name example.com;
location = / { # только GET /
return 200 "homepage\n";
}
location / { # всё остальное
proxy_pass http://app;
}
location ^~ /static/ { # /static/* — без regex-проверки
root /var/www;
expires 1y;
}
location ~* \.(jpg|png|gif)$ { # любые картинки
root /var/www/images;
expires 30d;
}
}
Что произойдёт:
GET /→ location = / → homepageGET /static/app.css→ location ^~ /static/ → отдача файлаGET /static/img.png→ тоже location ^~ /static/, regex не проверяетсяGET /banner.jpg→ location ~* \.jpg$ → /var/www/images/GET /api/users→ location / → проксируется в app
8. Отдача статики: root, alias, try_files
Два способа сказать «бери файлы отсюда»:
root
location /images/ {
root /var/www; # файл: /var/www/images/cat.png
}
root приклеивается к полному URI. Запрос /images/cat.png → файл /var/www/images/cat.png.
alias
location /images/ {
alias /var/www/pics/; # файл: /var/www/pics/cat.png
}
alias заменяет часть URI, совпавшую с location. /images/cat.png → /var/www/pics/cat.png (без /images/).
С alias path в location должен заканчиваться слэшем ровно как у alias. Иначе ловишь странные баги. С root такой проблемы нет.
Правило: «не уверен — используй root».
try_files — главная директива для SPA
location / {
root /var/www/myapp;
try_files $uri $uri/ /index.html;
}
Что делает: пробует $uri (например /dashboard) → если нет файла, пробует /dashboard/ → если и его нет, возвращает /index.html.
Это стандарт для SPA (React/Vue/Angular): любой путь приводит к index.html, дальше JS-роутер сам разберётся.
Заголовки для статики
location /static/ {
root /var/www;
expires 1y; # Cache-Control: max-age=31536000
add_header Cache-Control "public, immutable";
access_log off; # не логировать каждый запрос — экономия
}
9. Reverse proxy — proxy_pass до байта
Главный модуль Nginx для DevOps: proxy_pass. На вид просто, но имеет нюанс, на котором ломаются 80% инженеров.
Базовый proxy
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
Со слэшем и без — критическая разница
Без слэша на конце
location /api/ {
proxy_pass http://app:8080;
}
Клиент: GET /api/users → upstream: GET /api/users
Со слэшем
location /api/ {
proxy_pass http://app:8080/;
}
Клиент: GET /api/users → upstream: GET /users (без префикса!)
То есть слэш на конце proxy_pass «отрезает» префикс location при проксировании. Это спасает, когда твоё приложение слушает на корне /, а наружу ты выставляешь под /api/.
Правило: если у upstream'а есть префикс, такой же как у location, — пиши без слэша. Иначе — со слэшем.
proxy_pass с переменными
Если в proxy_pass есть переменная — нужно явно указать resolver и upstream:
resolver 8.8.8.8 valid=300s;
location /proxy/ {
set $backend "https://api.example.com";
proxy_pass $backend; # DNS будет ре-резолвится
}
Без переменной Nginx резолвит DNS один раз при старте. Если IP upstream'а меняется (например, K8s service) — старый IP останется. Поэтому в облачных средах часто используют переменные.
10. Заголовки при проксировании
По умолчанию Nginx меняет некоторые заголовки. Самое важное:
Host
Без proxy_set_header Host, Nginx ставит Host: 127.0.0.1:8080 (адрес upstream'а). Это путает приложение, которое генерирует URL по Host. Всегда задавай явно:
proxy_set_header Host $host;
Реальный IP клиента
За прокси приложение видит как client IP — IP прокси. Чтобы передать настоящий:
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; # http / https
proxy_set_header X-Forwarded-Host $host;
$proxy_add_x_forwarded_for — это специальная переменная, которая берёт существующий X-Forwarded-For и добавляет к нему IP клиента. Поддерживает цепочку прокси.
Дефолтные настройки в snippet'е
Создай /etc/nginx/snippets/proxy_params с общими настройками:
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 X-Forwarded-Host $host;
proxy_set_header Connection ""; # для keepalive upstream
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
И в server-блоках:
location /api/ {
include /etc/nginx/snippets/proxy_params;
proxy_pass http://app:8080;
}
Это DRY — не дублируешь в каждом блоке.
11. Upstream и алгоритмы балансировки
До сих пор мы проксировали на один адрес. Но обычно за прокси — пул серверов. Для этого — upstream block:
upstream backend {
server 10.0.1.10:8080 weight=5;
server 10.0.1.11:8080 weight=5;
server 10.0.1.12:8080 weight=1 backup; # резервный
server 10.0.1.13:8080 down; # временно отключён
keepalive 32; # пул keep-alive
keepalive_timeout 60s;
}
server {
location / {
proxy_pass http://backend;
proxy_set_header Connection ""; # обязательно для keepalive
}
}
Алгоритмы
| Директива | Алгоритм | Когда применять |
|---|---|---|
| (дефолт) | Round-robin с весами | Универсально |
least_conn; | Меньше всего активных коннектов | Долгие запросы / WebSocket |
ip_hash; | По хэшу клиентского IP | Sticky sessions |
hash $request_uri; | По хэшу любой переменной | Распределение по URL — для кэшей |
hash $request_uri consistent; | Consistent hashing (ring) | Дешёвая ре-балансировка |
random two least_conn; | Power of two choices | Современный, эффективный |
Health check — passive
OSS Nginx поддерживает только passive health check: если запрос к upstream упал — пометить как недоступный на N секунд.
upstream backend {
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
}
3 ошибки за 30 секунд → исключаем из ротации на 30 секунд → пробуем снова.
Active health check (фоновые запросы) — только в Nginx Plus. В OSS — используют nginx_upstream_check_module (форкали Tengine) или alternative reverse-proxy.
12. Буферизация и таймауты
Буферизация
По умолчанию Nginx буферизует ответ от upstream'а: принимает в память (или диск), а потом отдаёт клиенту. Это позволяет освободить upstream быстро — пока медленный клиент тащит ответ через 3G, upstream уже свободен.
proxy_buffering on; # дефолт
proxy_buffer_size 4k; # для заголовков ответа
proxy_buffers 8 16k; # 8 буферов по 16K
proxy_busy_buffers_size 32k;
proxy_max_temp_file_size 1024m; # макс на диске
Когда буферизацию отключить
Для streaming ответов (SSE, long polling, gRPC, LLM streaming) — буферизация всё ломает: клиент видит ответ только когда весь готов.
location /events {
proxy_pass http://app;
proxy_buffering off; # отключаем
proxy_cache off;
proxy_set_header Connection "";
proxy_http_version 1.1;
}
Таймауты — самая частая ошибка
Дефолты Nginx — 60 секунд. Для production почти всегда нужно подкрутить:
proxy_connect_timeout 5s; # соединиться с upstream
proxy_send_timeout 30s; # отправить запрос
proxy_read_timeout 30s; # получить ответ
# Для long polling / SSE
proxy_read_timeout 3600s; # час
Каскадно: client → Nginx → upstream → БД. Каждый следующий таймаут короче предыдущего, иначе хвосты накапливаются.
Retry на следующий upstream
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;
«Если ошибка timeout/502/503/504 — попробуй другой upstream, но не более 2 раз и не более 10 секунд суммарно».
По умолчанию Nginx делает retry только для идемпотентных методов (GET, HEAD). Можно включить для POST через proxy_next_upstream_tries + non_idempotent, но это рискованно — двойное списание денег и т.д.
13. Кэширование
Nginx умеет кэшировать ответы upstream'а. Это превращает его в полноценный CDN local-уровня.
http {
proxy_cache_path /var/cache/nginx/cache
levels=1:2
keys_zone=app_cache:100m
max_size=10g
inactive=60m
use_temp_path=off;
server {
location / {
proxy_pass http://app;
proxy_cache app_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
proxy_cache_lock on; # не толкать сразу 100 запросов в upstream
proxy_cache_background_update on;
add_header X-Cache-Status $upstream_cache_status;
}
}
}
Что это даёт
- 10× меньше нагрузки на upstream под typical web-load
- Stale-while-revalidate — отдаём старое, в фоне обновляем
- Lock — на cache miss первый запрос идёт в upstream, остальные ждут его
- Use_stale — если upstream упал, отдаём из кэша (даже устаревшее)
Cache key
По умолчанию ключ — это URL. Но часто нужно отдельно кэшировать для разных пользователей или Accept-Encoding:
proxy_cache_key "$scheme$host$uri$is_args$args$http_authorization";
Будь осторожен — чем подробнее ключ, тем хуже cache hit rate.
Не кэшировать для авторизованных
proxy_cache_bypass $http_authorization $http_cookie;
proxy_no_cache $http_authorization $http_cookie;
Если есть Authorization или Cookie — пропустить кэш. Иначе можно отдать ответ одного пользователя другому. Это catastrophic.
14. SSL/TLS — production-grade
Современный production-конфиг TLS:
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;
# Только современные версии
ssl_protocols TLSv1.2 TLSv1.3;
# Шифры (Mozilla intermediate, актуально)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Session resumption
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 1.1.1.1 valid=60s;
resolver_timeout 2s;
# DH parameters для PFS (если RSA-сертификат)
ssl_dhparam /etc/ssl/dhparam.pem; # openssl dhparam -out ... 2048
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
location / {
proxy_pass http://app;
}
}
# Редирект HTTP → HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
Готовый production-конфиг можно сгенерировать в ssl-config.mozilla.org и не писать руками — Mozilla обновляет рекомендации.
Получение сертификата
# Let's Encrypt через certbot
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
# Авто-обновление
sudo certbot renew --dry-run
# Cron / systemd timer — автоматически каждый день
15. HTTP/2 и HTTP/3
HTTP/2 в Nginx — одна директива:
server {
listen 443 ssl;
http2 on; # Nginx 1.25.1+
# Старый синтаксис: listen 443 ssl http2;
...
}
HTTP/3
HTTP/3 требует QUIC — это UDP, не TCP. Поддержка в Nginx — с версии 1.25 mainline:
server {
listen 443 ssl;
listen 443 quic reuseport; # QUIC по UDP/443
http2 on;
http3 on;
ssl_protocols TLSv1.3; # HTTP/3 требует TLS 1.3
# Сказать клиенту: «у меня есть HTTP/3»
add_header Alt-Svc 'h3=":443"; ma=86400';
...
}
Не забудь открыть UDP/443 в firewall. И на хосте — net.core.rmem_max хорошо бы увеличить для QUIC.
16. Сжатие: gzip и brotli
Gzip
gzip on;
gzip_vary on; # корректный Vary: Accept-Encoding
gzip_proxied any; # сжимать ответы от upstream
gzip_comp_level 6; # 1-9, 6 — баланс
gzip_min_length 256; # не сжимать совсем мелкое
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
Brotli — лучше gzip
Brotli даёт на 20-30% лучшее сжатие при сравнимой скорости. Не входит в стандартный Nginx — нужен модуль:
sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
brotli on;
brotli_comp_level 6;
brotli_static on; # отдавать предсжатые .br файлы
brotli_types
text/plain text/css
application/json application/javascript
image/svg+xml;
Pre-compressed static — лайфхак: положи рядом с app.js файл app.js.br (сжатый максимальным уровнем). Nginx будет отдавать его без runtime-сжатия — бесплатное ускорение.
17. Rate limiting
Защита от ботов и DoS на L7:
http {
# Зоны — память для счётчиков
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; # строже для логина
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
server {
# Общий лимит на IP — 10 req/s + burst 20
location / {
limit_req zone=general burst=20 nodelay;
limit_req_status 429;
proxy_pass http://app;
}
# Жёсткий лимит на логин — 5 попыток в минуту
location /login {
limit_req zone=login burst=2;
proxy_pass http://app;
}
# Не больше 10 параллельных соединений с одного IP
location /download/ {
limit_conn conn_per_ip 10;
proxy_pass http://app;
}
}
}
burst и nodelay — что это
- rate=10r/s — token bucket с 10 токенами в секунду
- burst=20 — допустим всплеск до 20 запросов сверх лимита; они станут в очередь и обработаются с задержкой
- nodelay — не задерживать всплеск, а сразу обработать; следующие должны ждать
Запоминать: burst без nodelay = latency. С nodelay = всплеск разрешён, но «израсходовал бюджет».
18. WebSocket и gRPC
WebSocket
WebSocket начинается как HTTP/1.1 запрос с Upgrade-заголовком. Nginx надо явно сказать, что нужно пропускать эти заголовки:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s; # долгие сессии
}
}
gRPC
gRPC поверх HTTP/2 — отдельный модуль grpc_pass:
server {
listen 443 ssl;
http2 on;
location / {
grpc_pass grpc://grpc_backend;
grpc_set_header Host $host;
grpc_read_timeout 60s;
}
}
upstream grpc_backend {
server grpc1:50051;
server grpc2:50051;
}
Поддержка TLS до upstream'а — grpcs://.
19. Логи
Стандартные форматы
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
log_format detailed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'urt="$upstream_response_time" '
'cache=$upstream_cache_status';
access_log /var/log/nginx/access.log detailed;
error_log /var/log/nginx/error.log warn;
JSON для structured logging (рекомендую)
log_format json_combined escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"x_forwarded_for":"$http_x_forwarded_for",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent",'
'"request_time":$request_time,'
'"upstream_connect_time":"$upstream_connect_time",'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_status":"$upstream_status",'
'"cache_status":"$upstream_cache_status"'
'}';
access_log /var/log/nginx/access.json json_combined;
JSON-логи легко парсятся Promtail/Vector/Fluent Bit в Loki/Elasticsearch — становятся structured field'ы.
Уровни error_log
debug, info, notice, warn, error, crit, alert, emerg. В production — warn или error. debug требует пересборки Nginx с --with-debug и забивает диск.
Logrotate
Дистрибутив обычно ставит /etc/logrotate.d/nginx сам. После ротации шлёт USR1 сигнал — Nginx переоткрывает лог-файлы. Аналог: nginx -s reopen.
20. Security headers
Минимальный набор для production:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
# CSP — главный, но требует настройки под сайт
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-...'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com" always;
Слово always в конце — отправлять заголовок даже для ошибочных ответов (4xx/5xx). Без него — часть пропадёт.
В Nginx add_header в вложенном location полностью перезаписывает родительские. То есть в location /api/ { add_header X-Foo bar; } не наследуется HSTS из server-блока.
Лечится: дубликат всех заголовков в каждом location или модулем headers-more-nginx-module.
21. Тюнинг производительности
Параметры, которые реально двигают перформанс:
# Кол-во worker'ов = кол-во CPU cores
worker_processes auto;
worker_rlimit_nofile 65535; # макс файловых дескрипторов на worker
events {
worker_connections 65535; # макс соединений на worker
multi_accept on; # принимать все готовые соединения сразу
use epoll; # Linux native
}
http {
# Zero-copy для статики
sendfile on;
tcp_nopush on; # отправлять полные пакеты
tcp_nodelay on; # но не задерживать маленькие
# Keepalive с клиентами
keepalive_timeout 65;
keepalive_requests 1000;
# Кэш файловых дескрипторов
open_file_cache max=10000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# Размеры буферов
client_body_buffer_size 16k;
client_max_body_size 10m; # макс upload
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
output_buffers 2 32k;
# Таймауты
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
# Скрыть версию
server_tokens off;
}
Ядро Linux под Nginx
# /etc/sysctl.conf
# Backlog очередей
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
# TIME_WAIT
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
# Buffer sizes
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Эфемерные порты
net.ipv4.ip_local_port_range = 1024 65535
# BBR congestion control
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
# Применить
sudo sysctl -p
22. Hot reload и graceful restart
Nginx умеет менять конфиг без остановки и без потери соединений. Магия — в master-worker модели.
nginx -s reload
- Master читает новый конфиг, проверяет
- Если ок — запускает новые workers с новым конфигом
- Старые workers перестают принимать новые соединения
- Старые workers дорабатывают существующие соединения, потом завершаются
- На время — у тебя 2× worker'ов в памяти
Это zero-downtime. Никаких 502 во время reload.
nginx -s reopen
Переоткрыть лог-файлы (после logrotate). Workers не пересоздаются.
Upgrade бинарника без рестарта
Можно обновить сам бинарник Nginx без потери соединений (USR2 → master запускает новый master).
# Заменил бинарник /usr/sbin/nginx
sudo kill -USR2 $(cat /run/nginx.pid)
# Новый master запустился, старый ещё рядом
# Если всё ок — завершить старый
sudo kill -QUIT $(cat /run/nginx.pid.oldbin)
Это редко нужно (обычно systemd рестартует), но знать стоит — это пример того, как сделана продакшен-готовая инфраструктура.
23. Реальные конфиги
Single Page App (React/Vue)
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
root /var/www/app/dist;
index index.html;
# Статические ассеты — кэшируем долго
location ~* \.(js|css|woff2|webp|svg|png|jpg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# API — проксируем на backend
location /api/ {
proxy_pass http://backend;
include /etc/nginx/snippets/proxy_params;
}
# SPA-fallback — любой неизвестный путь → index.html
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store";
}
}
Микросервисный API gateway
upstream users-service { server users:8080; keepalive 32; }
upstream orders-service { server orders:8080; keepalive 32; }
upstream payments-service { server payments:8080; keepalive 32; }
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
include /etc/nginx/snippets/ssl_params;
include /etc/nginx/snippets/security_headers;
# Rate limit на api в целом
limit_req zone=api burst=50 nodelay;
location /users/ {
proxy_pass http://users-service;
include /etc/nginx/snippets/proxy_params;
}
location /orders/ {
proxy_pass http://orders-service;
include /etc/nginx/snippets/proxy_params;
}
location /payments/ {
proxy_pass http://payments-service;
include /etc/nginx/snippets/proxy_params;
# Особо жёсткий лимит — нельзя ретраить
limit_req zone=payments burst=5;
proxy_next_upstream off;
}
location /healthz {
access_log off;
return 200 "ok\n";
}
}
WebSocket-чат
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream chat_backend {
ip_hash; # sticky — клиент держит один сервер
server chat-1:3000;
server chat-2:3000;
server chat-3:3000;
}
server {
listen 443 ssl;
http2 on;
server_name chat.example.com;
include /etc/nginx/snippets/ssl_params;
location /ws/ {
proxy_pass http://chat_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location / {
root /var/www/chat;
try_files $uri /index.html;
}
}
Caching reverse proxy перед slow backend
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=catalog:200m max_size=20g inactive=24h use_temp_path=off;
upstream backend {
server backend:8080;
keepalive 32;
}
server {
listen 443 ssl;
http2 on;
server_name shop.example.com;
include /etc/nginx/snippets/ssl_params;
location / {
proxy_pass http://backend;
include /etc/nginx/snippets/proxy_params;
proxy_cache catalog;
proxy_cache_key "$scheme$host$request_uri";
proxy_cache_valid 200 1h;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
proxy_cache_lock on;
proxy_cache_background_update on;
proxy_cache_bypass $cookie_session;
add_header X-Cache-Status $upstream_cache_status;
}
# Очистка по тегу (через ngx_cache_purge)
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge catalog "$scheme$host$1";
}
}
24. Антипаттерны
- if внутри location.
ifв Nginx — evil. Часто работает не так, как ожидаешь. Документация прямо предупреждает. Используйmapилиtry_files. - Дублирование add_header. В nested location переопределяет, а не добавляет. Часто security-заголовки «теряются» в /api/.
- Reload без -t. Уронишь production. Всегда:
nginx -t && nginx -s reload. - Слишком короткий keepalive_timeout. Дефолт 65с — обычно ок. Меньше — теряешь оптимизацию.
- worker_processes 1. Если у тебя 8 CPU, ставь
auto. Иначе 7 ядер простаивают. - access_log на всё подряд. Логирование статики типа .png — съедает диск.
access_log offв location для статики. - Огромный client_max_body_size. 100M по умолчанию = риск DoS. Точечно увеличивай для upload-эндпоинтов.
- Прямой proxy_pass на IP без upstream-блока. Теряешь возможность retry, healthcheck, keepalive pool.
- Не указанный Host при proxy. Приложение генерирует ссылки с localhost'ом.
- Self-signed certs в production. Только Let's Encrypt / ACM / купленные.
25. Дебаг
# Проверка конфига
sudo nginx -t # синтаксис + проверка путей
sudo nginx -T # весь итоговый конфиг с include'ами
# Статус процессов
ps auxf | grep nginx
systemctl status nginx
# Сокеты слушаются?
ss -tlnp | grep nginx
# Связи к upstream'ам
ss -tan state established 'sport = :8080'
# Логи
tail -F /var/log/nginx/access.log
tail -F /var/log/nginx/error.log
# Только ошибки за последние X минут
journalctl -u nginx --since "10 minutes ago"
# Тест с разными host-headers
curl -v -H "Host: example.com" http://127.0.0.1/
# Какой server-блок отработает
nginx -T | grep -A5 "server_name example.com"
# Module stub_status — встроенная метрика
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
# curl http://localhost/nginx_status
# Active connections: 291
# server accepts handled requests
# 16630948 16630948 31070465
# Reading: 6 Writing: 179 Waiting: 106
Debug log
Если нужно очень детально — debug-log. Требует Nginx с --with-debug:
nginx -V 2>&1 | grep -o with-debug
error_log /var/log/nginx/debug.log debug;
# Только для конкретного клиента
events {
debug_connection 1.2.3.4;
}
26. Альтернативы Nginx
| Инструмент | Особенности | Когда применять |
|---|---|---|
| Caddy | Автоматические TLS-сертификаты, простой конфиг, HTTP/3 из коробки | Маленькие проекты, домашние серверы |
| HAProxy | Жёсткая фокусировка на балансировке, лучший на L4, продвинутый health check | Финтех, высокая нагрузка на чистый load balancing |
| Envoy | L7 для микросервисов, gRPC native, основа Istio | Service mesh, сложная маршрутизация |
| Traefik | Cloud-native, авто-discovery в Docker/K8s | Динамические окружения |
| Apache | Древний, гибкий через .htaccess, но медленнее | Shared hosting, legacy |
| Nginx Unit | App + proxy в одном, поддержка runtimes (PHP, Python, Go) | Замена php-fpm + nginx |
Nginx остаётся стандартом для общего случая. В K8s часто берут Nginx Ingress Controller, в service mesh — Envoy.
27. Что с этим делает DevOps
- Reverse proxy перед приложением. Всегда. Не выставляй Node.js / Python наружу напрямую.
- TLS termination на Nginx. Один точка управления сертификатами.
- Caching layer. Снимает 70-90% нагрузки с приложения.
- Static serving. Не отдавай статику через Node — отдай через Nginx.
- Rate limit. Защита API от ботов.
- Snippets / templates. DRY-конфиги, Ansible/Salt/Helm-шаблоны.
- Health check эндпоинт.
/healthzдля balancer'а / K8s probe. - JSON access log. Парсится в Loki/Elastic.
- Метрики. Nginx Prometheus Exporter + Grafana — RPS, errors, latency.
- Hot reload в CI/CD. Развёртывание новой версии конфига =
nginx -t && nginx -s reload. - Backup config. Конфиги — в Git. Никаких ручных правок в проде.
- Тест в стейджинге. Любая правка — сначала в staging, потом prod.
- Nginx Ingress в K8s. Если работаешь с Kubernetes — это самый частый Ingress Controller.
28. Nginx Ingress Controller в Kubernetes
В K8s «голый» Nginx не разворачивают. Его берут в виде Ingress Controller — оператор, который превращает Kubernetes-объекты Ingress в реальную Nginx-конфигурацию.
Есть два разных проекта с похожими именами:
- ingress-nginx (от K8s community) — open source, самый распространённый. Репозиторий:
kubernetes/ingress-nginx. - nginx-ingress (от F5/Nginx Inc) — есть OSS и Plus версии. Репозиторий:
nginxinc/kubernetes-ingress.
Они разные. Конфиги, аннотации, фичи — отличаются. Стандарт по умолчанию — ingress-nginx.
Установка через Helm
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.replicaCount=3 \
--set controller.service.type=LoadBalancer
Базовый Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 8080
tls:
- hosts: [api.example.com]
secretName: api-tls
Annotations — главное оружие
Через annotations можно настраивать почти любую директиву Nginx, не редактируя ConfigMap. Самые частые:
| Annotation | Что делает |
|---|---|
proxy-body-size: 100m | client_max_body_size |
proxy-read-timeout: 60 | timeout до ответа upstream |
rewrite-target: / | перезапись URI |
ssl-redirect: "true" | HTTP → HTTPS |
force-ssl-redirect: "true" | даже за внешним прокси |
configuration-snippet | произвольный кусок nginx-конфига в location |
server-snippet | в server-блок |
limit-rps: "10" | встроенный rate limit |
auth-url / auth-signin | внешняя аутентификация (oauth2-proxy) |
canary: "true" + canary-weight: "10" | canary deployment 10% |
whitelist-source-range | IP allowlist |
backend-protocol: "HTTPS" | проксировать в backend по HTTPS |
backend-protocol: "GRPC" | gRPC backend |
ConfigMap — глобальные настройки
Аннотации действуют на один Ingress. Глобальные параметры (worker_processes, gzip, log format) — через ConfigMap контроллера:
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
worker-processes: "auto"
use-gzip: "true"
gzip-level: "6"
proxy-buffering: "on"
log-format-escape-json: "true"
log-format-upstream: '{"time":"$time_iso8601","host":"$host","method":"$request_method","uri":"$request_uri","status":$status,"req_time":$request_time,"upstream_time":"$upstream_response_time"}'
enable-real-ip: "true"
use-forwarded-headers: "true"
ssl-protocols: "TLSv1.2 TLSv1.3"
hsts: "true"
hsts-include-subdomains: "true"
Canary deployment одним манифестом
Без service mesh, без сложных tools — двумя Ingress'ами на тот же host:
# Основной (stable, 100% по умолчанию)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: {name: api-v1, port: {number: 8080}}
---
# Canary, 10% трафика на v2
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: {name: api-v2, port: {number: 8080}}
Альтернативы canary-аннотации: canary-by-header: X-Canary (header-based) и canary-by-cookie — сначала видят канарейку отдельные пользователи.
Когда стоит уходить с ingress-nginx
- Сложная маршрутизация (header-based, gRPC reflection, многошаговая) → Gateway API + Contour / Envoy Gateway
- Service mesh уже есть → его Ingress (Istio Gateway)
- Нужны active health checks без хаков → HAProxy Ingress или Nginx Plus
- Хочется WAF из коробки → AWS ALB Controller + AWS WAF, либо Cloudflare
29. stream{} — TCP/UDP проксирование
До сих пор мы говорили про HTTP. Но Nginx умеет балансировать и обычный TCP / UDP — для этого есть stream модуль (включён в дистрибутив, но отдельно от http).
Зачем это нужно:
- Проксирование БД (PostgreSQL, MySQL, Redis) — выставить один endpoint наружу, за ним пул реплик
- SMTP, IMAP — mail-proxy
- MQTT, MQTT-TLS — для IoT
- DNS-over-TCP / DNS-over-TLS
- gRPC raw (если не хочешь HTTP/2-обработку)
- Балансировка SSH, RDP
Структура
# Совершенно отдельная секция от http {}
stream {
upstream postgres {
server pg-primary:5432 max_fails=3 fail_timeout=10s;
server pg-replica1:5432 backup;
server pg-replica2:5432 backup;
}
server {
listen 5432;
proxy_pass postgres;
proxy_connect_timeout 5s;
proxy_timeout 3600s; # долгие соединения к БД
}
# TLS termination для PostgreSQL?
server {
listen 5433 ssl;
ssl_certificate /etc/ssl/db.crt;
ssl_certificate_key /etc/ssl/db.key;
proxy_pass pg-primary:5432;
}
# UDP пример: DNS
server {
listen 53 udp;
proxy_pass dns_upstream;
proxy_responses 1;
proxy_timeout 5s;
}
upstream dns_upstream {
server 1.1.1.1:53;
server 8.8.8.8:53;
}
# Лог отдельно для stream
log_format basic '$remote_addr [$time_local] $protocol $status '
'$bytes_sent $bytes_received $session_time';
access_log /var/log/nginx/stream.log basic;
}
Ограничения stream{}
- Балансировка — round-robin / least_conn / hash, без L7 (нет path/header)
- Нет ip_hash, но есть
hash $remote_addr consistent - UDP — без подтверждений, только проксирование
- SSL passthrough (
ssl_preread) позволяет рутить по SNI, не расшифровывая
SNI-routing без расшифровки
Можно проксировать TLS по имени сервера, не терминируя:
stream {
map $ssl_preread_server_name $upstream_pool {
api.example.com api_backend;
web.example.com web_backend;
default default_backend;
}
upstream api_backend { server api:443; }
upstream web_backend { server web:443; }
server {
listen 443;
ssl_preread on; # читаем SNI из TLS ClientHello
proxy_pass $upstream_pool;
}
}
Полезно когда TLS-терминирует само приложение или mTLS должен быть сквозным.
30. fastcgi_pass и uwsgi_pass
Не все приложения говорят по HTTP. Многие используют бинарные протоколы — это эффективнее, чем HTTP-парсинг.
FastCGI
Бинарный протокол для CGI-приложений. Используется PHP (php-fpm).
uWSGI
Свой бинарный протокол uWSGI server'а. Используется в Python (Django/Flask с uWSGI).
SCGI
Старый, редкий, упоминается ради полноты.
memcached / Redis
Nginx умеет ходить прямо в кэш через memcached_pass, минуя приложение.
PHP-FPM конфиг — классика
server {
listen 443 ssl;
http2 on;
server_name shop.example.com;
root /var/www/shop;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include /etc/nginx/snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock; # Unix-socket — быстрее TCP
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_read_timeout 120;
fastcgi_buffer_size 16k;
fastcgi_buffers 16 16k;
}
# Защита от выполнения PHP в upload-папке
location ~* /uploads/.*\.php$ {
return 403;
}
}
Python uWSGI
upstream django {
server unix:/run/uwsgi/django.sock;
}
server {
listen 443 ssl;
server_name app.example.com;
location / {
include uwsgi_params;
uwsgi_pass django;
uwsgi_read_timeout 60s;
}
location /static/ {
alias /var/www/django/static/;
expires 1y;
}
location /media/ {
alias /var/www/django/media/;
}
}
В современных проектах Python/Node редко используют FastCGI/uWSGI напрямую — обычно Gunicorn/uvicorn/Node слушают HTTP, и Nginx делает обычный proxy_pass. FastCGI остаётся в основном с PHP.
31. OpenResty и Lua
OpenResty — это «суперзаряженный Nginx». Сборка Nginx + LuaJIT + десятки модулей + базовая стандартная библиотека. Запускается так же, как Nginx, но внутри можно писать Lua-код прямо в директивах.
Зачем это нужно:
- Программируемый прокси без отдельного app-сервера
- Кастомная аутентификация (JWT, OIDC) на edge
- Сложная маршрутизация по бизнес-логике
- API rate limiting с хранением в Redis
- API composition (один запрос → 3 backend'а → объединить ответы)
На OpenResty построены Kong, APISIX, 3scale — все эти API-gateway по сути Nginx + Lua-логика.
Простой пример: JWT-валидация в Nginx
location /api/ {
access_by_lua_block {
local jwt = require "resty.jwt"
local auth = ngx.var.http_authorization or ""
local token = string.match(auth, "Bearer%s+(.+)")
if not token then
ngx.status = 401
ngx.say('{"error":"missing token"}')
return ngx.exit(401)
end
local jwt_obj = jwt:verify("my-secret-key", token)
if not jwt_obj.verified then
ngx.status = 401
ngx.say('{"error":"invalid token"}')
return ngx.exit(401)
end
-- Передать user_id в upstream через заголовок
ngx.req.set_header("X-User-Id", jwt_obj.payload.sub)
}
proxy_pass http://app;
}
Фазы выполнения Lua
В Nginx обработка запроса делится на фазы. Lua-код можно вставлять в нужную:
set_by_lua_block # при назначении переменной
rewrite_by_lua_block # до выбора location
access_by_lua_block # проверка доступа — здесь auth
content_by_lua_block # вместо proxy_pass — генерация ответа
header_filter_by_lua_block # перед отправкой заголовков
body_filter_by_lua_block # изменение тела ответа
log_by_lua_block # при логировании
Когда брать OpenResty
- Нужна программируемость, но не хочется ставить отдельный сервис
- API gateway в малой команде
- Edge-логика (auth/rate-limit/transformation) поверх обычного Nginx
Минус — Lua это отдельный язык, добавляет операционную сложность. Если масштаб подходит — лучше готовый Kong / APISIX.
32. NJS — JavaScript внутри Nginx
В 2017 Nginx Inc выпустила NJS — подмножество JavaScript (ES6+) для встраивания в Nginx. Похоже на OpenResty/Lua, но на JS. Преимущество: фронтендеры уже знают JS.
NJS — subset JS, без npm, без event loop в стиле Node, без файловой системы. Только то, что нужно для модификации запроса/ответа.
Установка
sudo apt install nginx-module-njs
# В nginx.conf:
load_module modules/ngx_http_js_module.so;
Пример: добавить request_id
# /etc/nginx/njs/request_id.js
function generate_request_id(r) {
const ts = Date.now().toString(36);
const rnd = Math.random().toString(36).substring(2, 10);
return `${ts}-${rnd}`;
}
export default { generate_request_id };
# nginx.conf
js_path "/etc/nginx/njs/";
js_import main from request_id.js;
server {
location / {
js_set $request_id main.generate_request_id;
proxy_set_header X-Request-Id $request_id;
add_header X-Request-Id $request_id always;
proxy_pass http://app;
}
}
NJS vs Lua
| NJS | OpenResty/Lua | |
|---|---|---|
| Язык | JS subset | Lua + LuaJIT |
| Производительность | Хорошая | Лучшая (JIT) |
| Экосистема | Мало модулей | Огромная (lua-resty-*) |
| Кривая входа | JS-фронтендеры освоят | Lua — отдельный язык |
| Зрелость | Молодой | 10+ лет в продакшене |
Для простых задач (модификация заголовков, request_id, A/B по логике) — NJS отличный выбор. Для серьёзной программируемости — Lua.
33. ModSecurity + OWASP CRS — встроенный WAF
Хочешь WAF, но не платить Cloudflare? ModSecurity — open source WAF, который встраивается в Nginx как модуль. С набором правил OWASP CRS покрывает большинство атак из OWASP Top 10.
Установка
# ModSecurity 3 (libmodsecurity)
sudo apt install libmodsecurity3 libnginx-mod-http-modsecurity
# OWASP CRS
sudo mkdir -p /etc/nginx/modsec
cd /etc/nginx/modsec
sudo git clone https://github.com/coreruleset/coreruleset.git
sudo cp coreruleset/crs-setup.conf.example crs-setup.conf
Базовый конфиг
# /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/nginx/modsec/crs-setup.conf
Include /etc/nginx/modsec/coreruleset/rules/*.conf
# /etc/nginx/modsec/modsecurity.conf — основной
SecRuleEngine On # или DetectionOnly для логирования без блока
SecRequestBodyAccess On
SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml application/json
SecAuditLog /var/log/nginx/modsec_audit.log
SecAuditLogParts ABIJDEFHZ
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecPcreMatchLimit 100000
SecPcreMatchLimitRecursion 100000
# nginx.conf — подключение к server-блоку
server {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
location / {
proxy_pass http://app;
}
}
Paranoia levels
OWASP CRS работает в режиме anomaly scoring. Каждое правило добавляет очки, превышение порога — блок. Управляется через paranoia level в crs-setup.conf:
SecAction \
"id:900000,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.paranoia_level=1" # 1=мягко, 4=параноидально
SecAction \
"id:900110,\
phase:1,\
nolog,\
pass,\
setvar:tx.inbound_anomaly_score_threshold=5,\
setvar:tx.outbound_anomaly_score_threshold=4"
Стратегия внедрения
- SecRuleEngine DetectionOnly. Включи на пару дней — логируешь, но не блокируешь.
- Смотришь audit-лог. Какие правила срабатывают на легитимный трафик.
- Делаешь exclusions. Для каждого ложного срабатывания — точечно отключаешь правило для конкретного path/параметра.
- Включаешь enforcing.
SecRuleEngine On. - Поднимаешь paranoia постепенно. PL2, PL3 — после стабилизации.
Performance impact
ModSecurity — не бесплатный. Дополнительные 5-20% CPU на каждый запрос. На маленьких VM может стать узким местом. Альтернатива — WAF на уровне CDN (Cloudflare, Cloudfront WAF), там стоимость на edge размазана.
34. Mutual TLS — клиентские сертификаты
Обычный TLS аутентифицирует сервер. mTLS — обе стороны. Клиент тоже предъявляет сертификат, Nginx проверяет, что он подписан доверенным CA.
Где применяется:
- B2B API — выдаёшь партнёру cert, отозвал = доступ закрыт
- Внутренние сервисы — каждый сервис имеет свой cert, идентичность ≠ IP
- Admin-эндпоинты — требуют клиентский сертификат вместо пароля
- IoT — устройство имеет персональный cert
Конфигурация
server {
listen 443 ssl;
http2 on;
server_name partners.example.com;
# Серверный сертификат (как обычно)
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
# CA, которому доверяем для проверки клиентских сертификатов
ssl_client_certificate /etc/ssl/clients-ca.crt;
ssl_verify_client on; # on = обязательно, optional = опционально
ssl_verify_depth 2; # глубина цепочки
# CRL — список отозванных (опционально)
ssl_crl /etc/ssl/clients-revoked.crl;
location / {
proxy_pass http://app;
# Передать инфу о клиенте в upstream
proxy_set_header X-Client-Cert-Subject $ssl_client_s_dn;
proxy_set_header X-Client-Cert-Issuer $ssl_client_i_dn;
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_set_header X-Client-Serial $ssl_client_serial;
proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
}
}
Авторизация по subject
Можно ограничить, какие именно сертификаты принимать — например, по CN или OU:
map $ssl_client_s_dn $allowed_partner {
default 0;
"~CN=partner1\.example\.com,O=ACME Corp" 1;
"~CN=partner2\.example\.com,O=Globex" 1;
}
server {
...
ssl_verify_client on;
if ($allowed_partner != 1) {
return 403;
}
location / { proxy_pass http://app; }
}
Optional mTLS
Иногда хочется — клиент с cert получает API-доступ, без cert — только публичная часть:
ssl_verify_client optional;
location /api/admin/ {
if ($ssl_client_verify != "SUCCESS") { return 403; }
proxy_pass http://app;
}
location /api/public/ {
proxy_pass http://app;
}
35. GeoIP и split_clients
GeoIP2 — геолокация по IP
Nginx умеет определять страну/город/ASN клиента по его IP, используя базу MaxMind. Полезно для geoblocking, региональных правил, аналитики.
# Установить модуль
sudo apt install libnginx-mod-http-geoip2
# Скачать базу (нужен MaxMind account — free)
wget https://download.maxmind.com/.../GeoLite2-Country.mmdb
sudo mv GeoLite2-Country.mmdb /etc/nginx/geoip/
load_module modules/ngx_http_geoip2_module.so;
http {
geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
$geoip2_country_code source=$remote_addr country iso_code;
$geoip2_country_name source=$remote_addr country names en;
}
map $geoip2_country_code $blocked_country {
default 0;
KP 1; # North Korea
IR 1; # Iran
SY 1; # Syria
}
server {
if ($blocked_country) {
return 451; # 451 Unavailable For Legal Reasons
}
# Передать страну в upstream
proxy_set_header X-Country $geoip2_country_code;
location / {
proxy_pass http://app;
}
}
}
split_clients — A/B testing без mesh
Хочешь раздавать 10% трафика на новую версию? Без service mesh, без Argo Rollouts — две строчки в Nginx:
split_clients "${remote_addr}${http_user_agent}" $variant {
10% "v2";
* "v1";
}
upstream app_v1 { server v1:8080; }
upstream app_v2 { server v2:8080; }
map $variant $upstream_pool {
"v1" app_v1;
"v2" app_v2;
}
server {
location / {
proxy_pass http://$upstream_pool;
add_header X-Served-By $variant always;
}
}
split_clients — детерминистический хэш. Один и тот же клиент всегда попадает в одну группу — это критично для UX.
36. Кейс troubleshooting: «502 Bad Gateway»
Пользователи жалуются: «у вас 502 Bad Gateway». Покажу системный алгоритм поиска причины — как реально дебажат.
-
Шаг 1: error.log Nginx
Что искать:sudo tail -50 /var/log/nginx/error.logconnect() failed (111: Connection refused)→ upstream не слушаетupstream timed out→ upstream живой, но медленныйno live upstreams→ все upstream'ы в fail_timeoutupstream sent invalid header→ app возвращает мусорupstream prematurely closed connection→ app падает посреди ответа
-
Шаг 2: жив ли upstream
# Из машины с Nginx curl -v http://app:8080/healthz ss -tlnp | grep 8080 # слушает ли локально # Если в K8s kubectl get pods -l app=backend kubectl logs deploy/backend --tail=100 -
Шаг 3: connectivity
Если в облаке — Security Groups / NetworkPolicy могут блокировать.ping app nc -zv app 8080 traceroute app -
Шаг 4: timeout
Если запрос медленный — app не успевает, Nginx режет. Смотри upstream_response_time в логах:
tail -100 /var/log/nginx/access.log | awk '{print $NF}' | sort -n # или для JSON-лога: tail -100 /var/log/nginx/access.json | jq -r .upstream_response_time | sort -nЕсли 90% запросов 30+ секунд → крути таймауты Nginx или фикси приложение.
-
Шаг 5: SSL/протокол
Если upstream — HTTPS, и серт самоподписан:
proxy_ssl_verify off; # только для дебага, не в prod proxy_ssl_server_name on; proxy_ssl_name $host;Если upstream HTTP/2-only и ты шлёшь HTTP/1.1 — нужен
proxy_http_version 1.1;, а для gRPC —grpc_passвместоproxy_pass. -
Шаг 6: размер заголовков
Если в логах
upstream sent too big header:proxy_buffer_size 16k; proxy_buffers 8 32k; proxy_busy_buffers_size 32k;Распространено когда JWT в Cookie/Authorization вырос больше 8K (default).
-
Шаг 7: keep-alive проблемы
Если 502 редкие и не воспроизводятся — может быть race: upstream закрыл keepalive-соединение, Nginx ещё не знает, шлёт запрос — получает FIN. Лечится:
upstream backend { server app:8080; keepalive 32; keepalive_timeout 30s; # должен быть КОРОЧЕ, чем у upstream } proxy_next_upstream error timeout http_502; proxy_next_upstream_tries 2; -
Шаг 8: tcpdump последний резерв
sudo tcpdump -i any -nn 'host app and port 8080' -w /tmp/dump.pcap # потом анализируй в Wireshark
В 80% случаев ответ находится на шагах 1-2. Если нет — переходи дальше системно, не пропуская.
37. Open Source vs Nginx Plus
Nginx — коммерческий продукт у F5. Есть OSS и Plus. Полезно знать, чего в open source нет:
| Фича | OSS | Plus |
|---|---|---|
| Базовое HTTP / proxy / TLS | ✓ | ✓ |
| upstream / load balancing | ✓ | ✓ |
| HTTP/2 / HTTP/3 | ✓ | ✓ |
| Active health checks | ✗ (только passive) | ✓ |
| Dynamic config через API | ✗ | ✓ |
| DNS resolution for upstream без перезагрузки | ограниченно | ✓ |
| Session persistence (cookie, learn) | только ip_hash | ✓ полный |
| JWT validation | через OpenResty/NJS | встроено |
| OIDC из коробки | nginx-openidc или OpenResty | ✓ |
| Dashboard / live monitoring | только stub_status | ✓ красивый dashboard |
| Поддержка F5 | community | SLA |
Чем заменить Plus, оставаясь в OSS
Active health checks
Tengine (форк Nginx с активными health checks), nginx_upstream_check_module, либо вынести HC на HAProxy / Envoy.
Dynamic config
OpenResty + Lua для динамического upstream'a из Redis/etcd. Либо использовать готовый APISIX.
JWT / OIDC
OpenResty модуль lua-resty-jwt или lua-resty-openidc. Либо oauth2-proxy перед Nginx.
Dashboard
Nginx Prometheus Exporter + Grafana. Бесплатно, гибко, лучше чем родной dashboard.
Альтернативы — когда Plus нет смысла
Если тебе реально нужны фичи Plus — посмотри также:
- Caddy — бесплатно, автоматические сертификаты, dynamic config
- HAProxy — лучший в активной балансировке, open source без ограничений
- Envoy — динамический конфиг через xDS API родной
- Traefik — Docker/K8s discovery из коробки
- OpenResty — программируемый Nginx
- APISIX — готовый API gateway на OpenResty
В реальной практике Plus покупают, когда нужна поддержка F5 и активные health checks без хаков. Большинству хватает OSS + грамотной архитектуры.
38. Чек-лист «понимаю Nginx»
- ☐ Объясню master/worker архитектуру и зачем именно так
- ☐ Помню структуру конфига: main / events / http / server / location
- ☐ Знаю алгоритм выбора server и location
- ☐ Различаю модификаторы location: =, ^~, ~, ~*
- ☐ Помню разницу между root и alias
- ☐ Использую try_files для SPA
- ☐ Помню, что слэш в конце proxy_pass меняет поведение
- ☐ Настраиваю X-Real-IP, X-Forwarded-For, X-Forwarded-Proto
- ☐ Знаю upstream-алгоритмы: round-robin, least_conn, ip_hash
- ☐ Понимаю proxy_buffering и когда его отключать
- ☐ Использую proxy_cache с lock и use_stale
- ☐ Настроил TLS по Mozilla intermediate, HSTS, OCSP stapling
- ☐ Включил HTTP/2 и хотя бы знаю про HTTP/3
- ☐ Применил gzip + brotli (предсжатый)
- ☐ Настроил limit_req с burst и nodelay
- ☐ Сделал WebSocket proxy с Upgrade-заголовками
- ☐ Знаю JSON access_log и зачем
- ☐ Настроил security headers с always
- ☐ Знаю sysctl-настройки под высокую нагрузку
- ☐ Сделал hot reload без падений
- ☐ Помню антипаттерны (if, дублирование add_header, ...)
- ☐ Использую
nginx -tиnginx -Tдля дебага - ☐ Знаю разницу между ingress-nginx и nginx-ingress в K8s
- ☐ Помню 5+ полезных аннотаций ingress-nginx
- ☐ Сделаю canary через ingress-nginx без mesh
- ☐ Знаю, для чего нужен
stream{}модуль - ☐ Понимаю SNI-routing через
ssl_preread - ☐ Объясню, в чём смысл fastcgi_pass и где он нужен
- ☐ Знаю, что такое OpenResty и какие фазы Lua-кода
- ☐ Различаю OpenResty/Lua vs NJS
- ☐ Помню стратегию ModSecurity (DetectionOnly → exclusions → Enforce)
- ☐ Настрою mTLS на Nginx и передам $ssl_client_s_dn в upstream
- ☐ Использовал GeoIP2 или split_clients для A/B/гео-логики
- ☐ Помню алгоритм дебага «502 Bad Gateway» из 8 шагов
- ☐ Знаю, чего в Nginx OSS нет vs Plus, и чем заменить