Половина конфигов Nginx, которые вы находите в интернете, работают по случайности. Они ставят proxy_pass, добавляют пару заголовков — и двигаются дальше, пока не истекает таймаут загрузки, не обрывается WebSocket или в логах не появляется клиентский IP вида 127.0.0.1. Разница между «работает» и «работает и говорит правду» — всего несколько директив. Вот конфиг, который я реально запускаю, и обоснование каждого фрагмента.
Зачем вообще нужен reverse proxy
Один сервер, много приложений, один терминатор TLS. В этом вся идея. Вы запускаете каждое приложение на loopback-порту (3000, 8000, 8080), а Nginx маршрутизирует по server_name или location. SSL, переписывание заголовков, gzip и rate limiting живут в одном месте, а не дублируются в каждом приложении.
example.com → 127.0.0.1:3000 (Next.js)
api.example.com → 127.0.0.1:8000 (Node API)
Блок заголовков, который действительно важен
Поведение прокси по умолчанию многое отбрасывает. Ответ работает, но вышестоящее приложение не знает, кто спрашивает и был ли это HTTP или HTTPS, — что ломает редиректы, rate limiting и логи. Всегда передавайте реальные значения:
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;
X-Real-IP— это IP пира, который увидел Nginx.X-Forwarded-For— строится через$proxy_add_x_forwarded_for, добавляя реальный клиентский IP к уже существующей цепочке — корректно, когда спереди стоит ещё один прокси.X-Forwarded-Proto— тот, который все забывают. Без него приложение за TLS видит обычный HTTP и строитhttp://-редиректы и canonical-URL. Express/Fastify явно читают этот заголовок, чтобы решить, защищён ли запрос.
Для WebSocket (а также dev-режима HMR в Next.js) добавьте заголовки upgrade и принудительно HTTP/1.1:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Без proxy_http_version 1.1 вышестоящий сервер получает HTTP/1.0, и Connection: upgrade бесполезен. Это самая частая причина «socket hang up» в проксированном WebSocket-приложении.
Полный укреплённый блок сервера
Вот конфиг для одного приложения, HTTPS-first, со всеми деталями:
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com www.example.com;
# Certbot заменит этот блок при первой настройке TLS
return 301 https://example.com$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;
# Скорость: держим соединение с приложением живым
keepalive_timeout 65;
# Проксируем в приложение
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header 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_set_header X-Forwarded-Proto $scheme;
# Даём приложению закончить, не убиваем длинные запросы
proxy_read_timeout 60s;
}
# Базовые заголовки безопасности
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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Сжатие
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
}
Ловушка со слешем в конце, о которую спотыкаются все
proxy_pass трактует завершающий слеш как префикс URL. Это классический источник сбивающих с толку 404:
# передаёт /app/foo → http://upstream/app/foo
proxy_pass http://127.0.0.1:3000/;
# передаёт /app/foo → http://upstream/foo
proxy_pass http://127.0.0.1:3000;
При location /app/ { proxy_pass http://127.0.0.1:3000/; } совпадение из location заменяет префикс пути location /app/ на то, что идёт после слеша в proxy_pass. Хотите убрать /app — используйте завершающий слеш. Хотите его сохранить — не используйте. Сомневаетесь — проверьте через curl -v и посмотрите строку запроса, которую получает вышестоящий сервер.
TLS: используйте Let’s Encrypt, а не полунастроенный сертификат
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
Плагин nginx у Certbot — это installer: он редактирует ваш блок сервера, чтобы отдавать HTTPS с новым сертификатом.[1] После первого запуска верните свои кастомные директивы (типа http2 on и заголовки безопасности) — Certbot их за вас не вставит. Проверьте итоговый блок, прежде чем считать задачу сделанной.
Несколько заметок по TLS, которые легко переусложнить:
Strict-Transport-Security(max-age=31536000) говорит браузерам требовать HTTPS. Это безопасно, как только ваш сертификат работает, но не задавайте его, пока не убедитесь, что все поддомены отдают HTTPS, иначе закроете доступ к незащищённой странице.- Оставьте значения
ssl_protocols/ ciphers по умолчанию; современный nginx со свежим сертификатом Let’s Encrypt уже набирает A+ на обычном тесте SSL, если вы не принудительно включили старые TLS 1.0/1.1.
Тюнинг, который действительно двигает стрелку
Keepalive вышестоящих серверов — пул соединений, чтобы Nginx не открывал новое TCP-рукопожатие к вашему приложению на каждый запрос. Поместите это в блок upstream и обращайтесь по имени:
upstream myapp {
server 127.0.0.1:3000;
keepalive 16;
}
server {
location / {
proxy_pass http://myapp;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Установка Connection "" внутри location держит вышестоящее соединение живым (заголовок upgrade выше — исключение; делайте так только когда нужны WebSocket).
proxy_buffering — при проксировании потока (SSE, большая загрузка) или long-poll-эндпоинта может понадобиться proxy_buffering off;, чтобы байты шли потоком, а не буферизовались и доставлялись с задержкой.
client_max_body_size — умолчание 1 МБ. Любой загрузочный API за прокси отдаст 413, как только вы его превысите. Задайте то, что реально принимает ваше приложение:
client_max_body_size 50m;
worker_connections — в nginx.conf это ограничивает число одновременных соединений на один воркер; по умолчанию 512.[2] Для небольшого сайта этого достаточно, но при всплеске нагрузки это первое, что стоит проверить. Задавайте его исходя из ulimit -n сервера, а не из круглого числа из туториала.
Выбор цели proxy_pass
http://127.0.0.1:3000— самое простое и переносимое.http://unix:/run/myapp.sock— Unix-сокет, быстрее loopback и не нужно сторожить TCP-порт. Чуть больше конфигурации на стороне приложения, и обязательно настройте права между пользователем приложения и Nginx.
Форма с loopback-TCP — правильный вариант по умолчанию. Переключайтесь на сокет, только если гонитесь за последними миллисекундами.
Короткая версия
Nginx как reverse proxy — это тридцать строк и несколько решений. Передавайте Host, X-Real-IP, X-Forwarded-For и X-Forwarded-Proto; принудительно HTTP/1.1 и заголовки upgrade для WebSocket; следите за завершающим слешем в proxy_pass; задайте разумный client_max_body_size; TLS пусть делает Certbot. Это закрывает подавляющее большинство случаев «локально работало, а на сервере сломалось».
Источники
- Certbot — Using Certbot (authenticators vs installers; nginx plugin modifies server config). https://eff-certbot.readthedocs.io/en/stable/using.html — проверено 2026-09-27
- nginx — Core functionality (
worker_connections, default 512). https://nginx.org/en/docs/ngx_core_module.html — проверено 2026-09-27