Nginx глубоко: архитектура, TLS и стратегии деплоя
Nginx — самый распространённый фронтенд веб-инфраструктуры: по статистике, за ним стоит треть топовых сайтов мира. В краткой версии ты настроил reverse proxy, получил сертификат от Certbot и включил gzip. Но конфиг «сработал» и конфиг «правильный» — разные вещи: первый выдержит нагрузку твоего ноутбука, второй — внезапный всплеск трафика, медленного клиента с мобильного интернета и попытку брутфорса логина. Разница в понимании того, как Nginx устроен внутри.
Разберём архитектуру: почему один master и несколько worker-процессов — это фундамент его производительности и как правильно управлять конфигурацией без даунтайма. Соберём полный production-конфиг: upstream-балансировка со стратегиями round-robin/least_conn/ip_hash и весами, gzip и brotli, кэширование статики, rate limiting зонами. Настроим TLS по всем канонам: протоколы, шифры, HSTS, OCSP stapling, HTTP→HTTPS редирект. Завершим стратегиями выкатки — rolling, blue-green, canary с взвешенным upstream — и местом Cloudflare как CDN/WAF перед origin.
Архитектура: master и workers
Заголовок раздела «Архитектура: master и workers»Nginx работает как набор процессов, а не потоков. Один master читает конфигурацию, открывает порты 80/443, порождает workers и никогда не обрабатывает соединения сам. Workers (по числу ядер CPU) — обработчики: каждый ведёт тысячи соединений через event-driven цикл epoll/kqueue. Это и есть причина его скорости: один процесс обрабатывает соединения асинхронно, блокируясь только на реальных операциях ввода-вывода.
ps aux | grep nginx# root 812 nginx: master process /usr/sbin/nginx# www-data 813 nginx: worker process# www-data 814 nginx: worker processУправление без даунтайма:
sudo nginx -t # ВСЕГДА перед изменением конфигаsudo systemctl reload nginx # master перечитывает конфиг, workers старые завершают свои соединения, новые берут обновлённую конфигурациюsudo systemctl restart nginx # жёсткий рестарт — рвёт соединенияReload — золотой стандарт: пользователи не замечают деплоя конфигурации. В Ansible это handler с state: reloaded, а не restarted.
Полный конфиг: reverse proxy, балансировка, сжатие, кэш
Заголовок раздела «Полный конфиг: reverse proxy, балансировка, сжатие, кэш»Собираем production-конфиг для pet-проекта по кирпичикам.
Upstream: стратегии балансировки
Заголовок раздела «Upstream: стратегии балансировки»Upstream — группа бэкендов (ngx_http_upstream_module). Nginx поддерживает три базовые стратегии и веса:
# 1. Round-robin (по умолчанию) — по кругу. Просто, но слепо к нагрузке.upstream app_roundrobin { server 127.0.0.1:3000; server 127.0.0.1:3001;}
# 2. least_conn — следующий запрос идёт к серверу с наименьшим числом активных соединений.# Лучший выбор для неравномерных по длительности запросов (Long Polling, тяжёлые отчёты).upstream app_least_conn { least_conn; server 127.0.0.1:3000 max_fails=3 fail_timeout=30s; server 127.0.0.1:3001 max_fails=3 fail_timeout=30s; keepalive 32; # переиспользование соединений к бэкенду}
# 3. ip_hash — клиент привязывается к серверу по хешу IP. Для stateful-сессий# в памяти приложения (но ломается за NAT-корпоративных сетей — все ходят с одного IP).upstream app_ip_hash { ip_hash; server 127.0.0.1:3000; server 127.0.0.1:3001;}
# 4. Взвешенный round-robin — трафик распределяется пропорционально весам.# Основа для canary-деплоя (подробности ниже).upstream app_weighted { server 127.0.0.1:3000 weight=95; # стабильная версия, 95% server 127.0.0.1:3001 weight=5; # canary-версия, 5%}max_fails=3 fail_timeout=30s — пассивный health-check: три неудачи за 30 секунд, и нода выводится из ротации до конца fail_timeout. Для активных проверок нужен модуль nginx_upstream_check_module или внешний мониторинг (см. главу про метрики).
Сжатие: gzip и brotli
Заголовок раздела «Сжатие: gzip и brotli»gzip on;gzip_vary on; # добавляет Vary: Accept-Encoding (критично для CDN)gzip_min_length 1024; # не жать маленькие ответы — оверхед больше выгодыgzip_comp_level 5; # 1-9: выше = лучше сжатие, но больше CPUgzip_types text/css application/javascript application/json image/svg+xml text/xml;gzip_proxied any; # сжимать и ответы за прокси (Cloudflare → origin)Brotli — преемник gzip от Google, выигрывает 15-20% на тексте, но требует динамического модуля (libnginx-mod-brotli):
brotli on;brotli_comp_level 5;brotli_types text/css application/javascript application/json image/svg+xml;Заметь порядок: Nginx отдаёт brotli браузерам, которые его поддерживают (Chrome, Firefox), gzip — остальным. Детекция по Accept-Encoding.
Кэширование статики
Заголовок раздела «Кэширование статики»Статику (CSS/JS/шрифты/картинки) должен отдавать Nginx напрямую с диска, не нагружая приложение:
location /static/ { root /opt/pet-app/public; # или alias — в зависимости от структуры expires 30d; # заголовок Expires add_header Cache-Control "public, immutable"; # браузер и CDN не проверяют условия access_log off; # не писать в access log — меньше I/O sendfile on; # zero-copy отдача файла tcp_nopush on;}immutable корректен только для файлов с хешем в имени (app.a1b2c3.js от Vite/webpack) — тогда обновление версии = новый URL. Для index.html кэширование минимальное (expires -1 или короткое), иначе пользователи получат старый HTML со ссылками на удалённые ассеты.
Rate limiting зонами
Заголовок раздела «Rate limiting зонами»# 1. Объявляем зоны в http-контексте (nginx.conf или conf.d/*)limit_req_zone $binary_remote_addr zone=global:10m rate=20r/s;limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;limit_conn_zone $binary_remote_addr zone=addr:10m;
# 2. Применяем в server/locationserver { location /api/login { limit_req zone=login burst=5 nodelay; # 2 r/s + всплеск 5 без задержки proxy_pass http://app_least_conn; }
location / { limit_req zone=global burst=40 nodelay; limit_conn addr 20; # не более 20 одновременных соединений с IP proxy_pass http://app_least_conn; }}Zone — разделяемая память (10m хватает на ~160k IP-адресов). rate задаётся в r/s или r/m. burst=N — очередь из N запросов, которые ждут свободного слота; nodelay — обработать их немедленно, не ждя, с риском превышения rate кратковременно. Без nodelay пользователи заметят задержки (недопустимо для API). Превышение — 503 Service Temporarily Unavailable. Полная семантика зон, burst и nodelay — в ngx_http_limit_req_module.
Полный server-блок (HTTP, перед TLS)
Заголовок раздела «Полный server-блок (HTTP, перед TLS)»server { listen 80; listen [::]:80; server_name pet.darkpix.dev;
# Логи с разделением времени: Nginx vs upstream log_format main '$remote_addr - $status "$request" rt=$request_time ' 'urt=$upstream_response_time "$http_user_agent"'; access_log /var/log/nginx/access.log main;
limit_req_zone $binary_remote_addr zone=global:10m rate=20r/s;
location /api/ { limit_req zone=global burst=40 nodelay; proxy_pass http://app_least_conn; proxy_http_version 1.1; proxy_set_header Connection ""; # keepalive к бэкенду работает 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; # приложение знает про HTTPS proxy_read_timeout 30s; proxy_buffering on; # Nginx буферизует ответ — бэкенд не держит медленных клиентов }
location /static/ { root /opt/pet-app/public; expires 30d; add_header Cache-Control "public, immutable"; access_log off; }
location / { proxy_pass http://app_least_conn; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
# Health-check для мониторинга (Prometheus, LB) location /healthz { access_log off; return 200 "ok\n"; }}Ключевые механики: proxy_set_header X-Forwarded-* — без них приложение генерирует http:// ссылки и теряет реальный IP клиента; proxy_buffering on — Nginx собирает ответ от бэкенда и отдаёт клиенту своим темпом: медленный смартфон не держит бэкенд-воркер занятым; $upstream_response_time в логе разделяет вину: большой urt при маленьком rt — тормозит приложение, оба большие — сеть или очередь соединений. Все proxy-директивы и заголовки — в ngx_http_proxy_module.
TLS: certbot, полный SSL-блок, редирект
Заголовок раздела «TLS: certbot, полный SSL-блок, редирект»Получение сертификата: standalone и webroot
Заголовок раздела «Получение сертификата: standalone и webroot»Certbot выпускает бесплатные сертификаты Let’s Encrypt на 90 дней с авто-продлением. Два режима:
# 1. standalone — certbot сам поднимает временный веб-сервер на 80 порту.# Подходит для первого выпуска, когда Nginx ещё не настроен или остановлен.sudo certbot certonly --standalone -d pet.darkpix.dev
# 2. webroot — certbot кладёт challenge-файл в каталог Nginx.# Безопаснее: не конфликтует с работающим Nginx, сертификат продлевается без остановки сервиса.sudo certbot certonly --webroot -w /var/www/certbot -d pet.darkpix.dev
# Авто-продление: systemd-таймер certbot.timer или cronsudo certbot renew --dry-run # проверка продленияДля webroot нужен location:
location /.well-known/acme-challenge/ { root /var/www/certbot;}Полный production SSL-блок
Заголовок раздела «Полный production SSL-блок»server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name pet.darkpix.dev;
ssl_certificate /etc/letsencrypt/live/pet.darkpix.dev/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/pet.darkpix.dev/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/pet.darkpix.dev/chain.pem; # для OCSP stapling
# Протоколы: TLS 1.2 минимум, 1.3 предпочтителен ssl_protocols TLSv1.2 TLSv1.3;
# Шифры для TLS 1.2 (1.3 игнорирует эту директиву и имеет собственный набор) ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers off; # отдать выбор клиенту (для TLS 1.3 — безразлично, он быстрее)
# Сессии: кэширование session tickets ускоряет повторные соединения ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # отключить tickets — они stateless и не отзываются при компрометации ключа
# OCSP stapling: Nginx сам проверяет сертификат у Let's Encrypt # и прикрепляет подписанный ответ — клиент не ходит на OCSP-сервер сам, # нет утечки приватности (какие сайты посещает пользователь) и быстрее ssl_stapling on; ssl_stapling_verify on; resolver 1.1.1.1 1.0.0.1 valid=300s; # резолвер для проверки OCSP
# HSTS: браузер будет ходить на сайт ТОЛЬКО по HTTPS add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Безопасность по умолчанию add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; add_header Referrer-Policy strict-origin-when-cross-origin;
# ... остальной конфиг (location /api/, /static/ и т.д.) — как в HTTP-блоке}Проверка снаружи: curl -sI https://pet.darkpix.dev | grep -i strict и SSL Labs — цель A/A+.
HTTP→HTTPS редирект
Заголовок раздела «HTTP→HTTPS редирект»server { listen 80; listen [::]:80; server_name pet.darkpix.dev;
# challenge для продления сертификата — без редиректа location /.well-known/acme-challenge/ { root /var/www/certbot; }
location / { return 301 https://$server_name$request_uri; }}Важно: challenge-location должен идти ДО редиректа, иначе certbot не сможет продлить сертификат (Let’s Encrypt ходит по HTTP и получит 301 вместо challenge-файла).
Стратегии деплоя
Заголовок раздела «Стратегии деплоя»Rolling update (по одному)
Заголовок раздела «Rolling update (по одному)»Обновляем реплики по одной: убиваем одну, поднимаем новую, ждём health-check, следующая. Дефолт в Docker Swarm и Kubernetes. На одном VPS с Nginx:
# 1. Поднимаем новую версию на свободном портуdocker run -d --name app-v2 -p 3001:3000 pet-app:1.5.0
# 2. Проверяем health-checkcurl -f http://127.0.0.1:3001/healthz
# 3. Переключаем upstream (правим конфиг, reload)# upstream app { server 127.0.0.1:3001; } # v2 полностью заменила v1sudo nginx -t && sudo systemctl reload nginx
# 4. Убиваем старуюdocker stop app-v1 && docker rm app-v1Для двух реплик: docker compose up -d --no-deps --scale app=2 app — поднимается вторая, health-check, затем переключение upstream и уменьшение до одной новой. Даунтайма нет, но если новая версия падает после переключения — откат долгий (пересоздание старой).
Blue-green (две среды, мгновенный откат)
Заголовок раздела «Blue-green (две среды, мгновенный откат)»Две полные среды: blue (текущая) и green (новая). Переключение мгновенное: upstream на green, всё ок — оставили; проблемы — вернули на blue за секунду.
# upstream управляется симлинком: current -> /etc/nginx/upstreams/blue.conf | green.confupstream app_bluegreen { include /etc/nginx/upstreams/current;}
# blue.conf:# server 127.0.0.1:3000;# green.conf:# server 127.0.0.1:3001;# Деплой greendocker run -d --name app-green -p 3001:3000 pet-app:1.5.0curl -f http://127.0.0.1:3001/healthz
# Переключение — атомарно для новых соединенийln -sf /etc/nginx/upstreams/green.conf /etc/nginx/upstreams/currentsudo nginx -t && sudo systemctl reload nginx
# Откат, если что-то не так — мгновенныйln -sf /etc/nginx/upstreams/blue.conf /etc/nginx/upstreams/currentsudo systemctl reload nginxЦена: двойной расход ресурсов на время выкатки (RAM, CPU, соединения к БД). Для stateful-приложений с сессиями в памяти — синхронизация сессий или sticky sessions через ip_hash.
Canary (взвешенный трафик)
Заголовок раздела «Canary (взвешенный трафик)»Новая версия получает 1-5% трафика. Метрики (error rate, latency) сравниваются со старой. Если в норме — доля растёт до 100%. Самая безопасная: при плохом раскладе пострадает только 1-5% запросов.
upstream app_canary { server 127.0.0.1:3000 weight=95; # стабильная версия, 95% трафика server 127.0.0.1:3001 weight=5; # canary, 5%}# Деплой canarydocker run -d --name app-canary -p 3001:3000 pet-app:1.5.0curl -f http://127.0.0.1:3001/healthzsudo nginx -t && sudo systemctl reload nginx # upstream выше уже активен
# Мониторинг: error rate на 3001 vs 3000 (см. главу про метрики)# Если ошибок < 1% и latency в норме 15 минут — увеличиваем вес:# sed -i 's/weight=5/weight=50/' /etc/nginx/sites-available/pet && reload# Затем weight=100, старую версию убиваемВеса в Nginx — приблизительные: распределение не идеально точное (особенно на малых весах). Для точного canary (по процентам с точностью до запроса) — Envoy, Traefik или Kubernetes с Argo Rollouts. Для pet-проекта на одном VPS — Nginx-весов достаточно.
| Стратегия | Даунтайм | Откат | Стоимость | Сложность |
|---|---|---|---|---|
| Rolling | Нет | Медленный (пересоздание) | Базовая | Низкая |
| Blue-green | Нет | Мгновенный | 2× ресурсы | Средняя |
| Canary | Нет | Мгновенный (уменьшение веса) | Нужна метрика | Высокая |
Cloudflare как CDN и WAF перед origin
Заголовок раздела «Cloudflare как CDN и WAF перед origin»Переведи NS домена на Cloudflare (оранжевое облачко) — и получаешь:
- CDN: статика кэшируется на 300+ пулах по миру, пользователь получает её из ближайшего дата-центра.
- WAF: фильтрация SQL-инъекций, XSS, ботов на уровне edge — до того, как запрос дошёл до твоего сервера.
- DDoS-гасение: трафик атаки поглощается инфраструктурой Cloudflare (террабиты), origin не видит его.
- TLS на edge: Cloudflare терминирует HTTPS у себя и может ходить на origin по HTTP (Flexible) или HTTPS (Full/Strict — рекомендуется).
Нюансы при настройке:
# Nginx должен доверять заголовкам Cloudflare, а не клиента# Список IP-диапазонов Cloudflare: https://www.cloudflare.com/ips/set_real_ip_from 173.245.48.0/20;set_real_ip_from 103.21.244.0/22;set_real_ip_from 103.22.200.0/22;# ... остальные диапазоныreal_ip_header CF-Connecting-IP; # Cloudflare передаёт реальный IP в этом заголовке
# Rate limiting теперь работает с реальными IP, а не адресами Cloudflarelimit_req_zone $binary_remote_addr zone=global:10m rate=20r/s;Если пропустить set_real_ip_from — все клиенты будут видны как IP Cloudflare: rate limiting превратится в глушение всего трафика, а аналитика покажет один IP. Для SSL между Cloudflare и origin — режим Full (Strict) с самоподписанным или Cloudflare Origin Certificate, иначе возможен downgrade до HTTP на последней миле.
Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»systemctl restart nginxвместоreloadпри деплое конфига. Рвётся десятки тысяч соединений — пользователи видят ошибку. Хорошо: handler сstate: reloaded,nginx -tперед любым изменением.- Потеря
X-Forwarded-Proto→ бесконечный HTTP→HTTPS редирект за Cloudflare. Приложение не знает, что исходный запрос был HTTPS, генерируетhttp://ссылки, Nginx редиректит снова. Хорошо: всегда передаватьX-Forwarded-Proto $scheme, в приложении читатьreq.protocol. - HSTS с
max-age=63072000с первого дня. Сломал HTTPS-сертификат — браузер пользователя на два года запомнил «только HTTPS» и не даст зайти даже по HTTP для диагностики. Хорошо: начать сmax-age=300, увеличивать постепенно. - OCSP stapling без
resolver. Nginx не сможет резолвить адрес OCSP-сервера Let’s Encrypt, stapling молча не работает. Хорошо: явныйresolver 1.1.1.1 1.0.0.1и проверкаopenssl s_client -connect pet.darkpix.dev:443 -status | grep -A 2 "OCSP response". - Rate limiting без учёта Cloudflare.
limit_reqвидит тысячи запросов с IP Cloudflare, блокирует «одного пользователя» (весь трафик). Хорошо:set_real_ip_from+ список диапазонов CF, либо rate limiting на уровне Cloudflare (Rules), а на origin — только как запасной вариант. - Canary с весом 5 без мониторинга. 5% трафика идёт на новую версию, которая падает с 500 — но ты узнаешь об этом из жалоб, а не из метрик. Хорошо: dashboard с error rate по
upstream_addr(3000 vs 3001), алерт на разницу, и только потом увеличение веса. - Кэширование
index.htmlсexpires 30d. Пользователи получают старый HTML со ссылками на несуществующие JS/CSS-бандлы. Хорошо:immutableтолько для хешированных ассетов, для HTML —no-cacheили короткий TTL.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»- Как работает модель master/worker в Nginx и почему она быстрая? Master читает конфиг, открывает порты и управляет workers, не обрабатывая соединения. Workers ведут event-driven цикл (epoll), обрабатывая тысячи соединений на процесс без потоков. Это убирает overhead контекстного переключения потоков и блокировки — скорость на статике и keepalive-соединениях.
- Разница между
least_connиip_hash: когда что использовать?least_conn— распределяет нагрузку по актуальной занятости серверов, хорош для неравномерных запросов (отчёты, Long Polling).ip_hash— привязывает клиента к серверу, нужен для stateful-сессий в памяти, но ломается за NAT (много клиентов с одного IP уйдут на одну ноду). - Что такое
proxy_bufferingи почему он важен? Nginx собирает ответ от бэкенда в буфер и отдаёт клиенту своим темпом. Бэкенд освобождается сразу после полного ответа в буфер — не держит воркер занятым на медленного клиента (смартфон в метро). Без буферинга бэкенд занят на всё время скачивания клиентом. - Как работает OCSP stapling и зачем он нужен? Nginx периодически запрашивает у Let’s Encrypt подписанный ответ о валидности сертификата и прикрепляет его к TLS-handshake. Клиент проверяет его локально, не обращаясь к OCSP-серверу: быстрее и без утечки данных о посещаемых сайтах третьей стороне.
- Чем blue-green отличается от canary? Blue-green — две полные среды, переключение мгновенное и обратимое, но новая версия получает 100% трафика сразу. Canary — новая версия получает долю трафика (1-5%), постепенно увеличиваемую по результатам метрик; безопаснее для рискованных изменений, требует наблюдаемости.
- Как Nginx определяет реальный IP клиента за Cloudflare? Cloudflare передаёт IP в заголовке
CF-Connecting-IP. Nginx с директивамиset_real_ip_from <диапазоны CF>иreal_ip_header CF-Connecting-IPзаменяет$remote_addrна реальный IP, доверяя только запросам с адресов Cloudflare. - Зачем нужен
keepaliveв upstream иproxy_http_version 1.1? HTTP/1.0 по умолчанию закрывает соединение после каждого запроса.proxy_http_version 1.1+proxy_set_header Connection ""+keepalive Nвключают переиспользование соединений к бэкенду — без тройного TCP-handshake на каждый запрос. - Почему
nginx -tобязателен перед reload? Один неверный символ в конфиге — и reload оставит Nginx со старой конфигурацией (или вовсе не применит её), а restart — не стартует вообще.nginx -tпроверяет синтаксис до применения, спасая от человеческой ошибки в продакшене.
Практика
Заголовок раздела «Практика»- Настрой upstream с двумя инстансами приложения (3000/3001), стратегия
least_conn,max_fails=3 fail_timeout=30s,keepalive 32. Убей один инстанс и прогониhey -n 1000 http://localhost/— убедись, что запросы идут только на живой (в логеurtиupstream_addr). - Включи gzip (level 5, типы css/js/json/svg) и сравни размер ответа:
curl -s -H "Accept-Encoding: gzip" -o /dev/null -w '%{size_download}' http://localhost/api/dataдо и после. Затем добавь brotli и повтори сAccept-Encoding: br. - Настрой две зоны rate limiting:
global(20 r/s) на/иlogin(2 r/s) на/api/login. Прогони 50 параллельных запросов (hey -n 50 -c 10) на оба эндпоинта, изучи 503 в логе и убедись, что/api/loginограничивается жёстче. - Выпусти сертификат через
certbot --webroot, собери полный SSL-блок с HSTS (max-age=300первые 3 дня), OCSP stapling иssl_session_tickets off. Проверь на SSL Labs — цель A. Настрой авто-продление через systemd-таймер и проверьcertbot renew --dry-run. - Реализуй blue-green деплой на одном VPS:
app-blueна 3000,app-greenна 3001, upstream через include симлинкаcurrent. Отработай сценарий: деплой green → health-check → переключение → обнаружение ошибки → откат на blue за < 10 секунд. - Настрой canary с весами 95/5, подними метрики error rate по
upstream_addr(Prometheus + nginx log parsing или две метрики приложения). Сымитируй ошибку в canary-версии (50% 500) и убедись, что dashboard показывает рост error rate на 3001, а на 3000 — нет. Уменьши вес до 0 без даунтайма.
Что почитать
Заголовок раздела «Что почитать»- Nginx — Reverse Proxy и HTTP Load Balancing
- Nginx — Rate Limiting — механика зон и burst
- Certbot — инструкции и Let’s Encrypt — как это работает
- Mozilla SSL Configuration Generator — актуальные шифры и протоколы
- martinfowler.com — BlueGreenDeployment
- Cloudflare — IP ranges и Learning Center