Настройка Nginx для Next.js в продакшене: от SSL до оптимизации
Полный гайд по настройке Nginx для Next.js приложения с SSL, кешированием, security headers и решением реальных проблем из практики.
Зачем это нужно?
Когда я впервые деплоил Next.js приложение на VPS, я думал: "Docker Compose поднял контейнер на порту 3000, всё работает локально - значит и в продакшене будет также". Спойлер: не будет.
Нужен был Nginx как reverse proxy, SSL сертификаты, настройка кеширования статики, security headers, rate limiting... В общем, стандартный production setup. Но дьявол, как всегда, в деталях.
Что получим в итоге
- ✅ HTTPS с автоматическим обновлением сертификатов (Let's Encrypt)
- ✅ Кеширование статики Next.js на год (с правильным cache busting)
- ✅ Security headers для рейтинга A+ на SSL Labs
- ✅ Rate limiting для защиты от DDoS
- ✅ Оптимизация производительности (gzip, HTTP/2)
- ✅ Понимание как это всё работает и почему
Архитектура: как это работает
Типичная схема деплоя Next.js приложения выглядит так:
┌─────────────┐
│ Интернет │
└──────┬──────┘
│ :80, :443
▼
┌─────────────────┐
│ Nginx (host) │ ← SSL termination, caching, security
└──────┬──────────┘
│ :3000 (localhost)
▼
┌─────────────────┐
│ Next.js (Docker)│ ← Приложение в контейнере
└─────────────────┘Важный нюанс: Nginx установлен на хосте (не в Docker), а Next.js крутится в контейнере. Поэтому в upstream используем localhost:3000, а не имя Docker сервиса.
Типичная ошибка #1
Если скопируете конфиг из интернета с server backend:3000 или server app:3000, получите ошибку: "host not found in upstream". Nginx на хосте не видит Docker network. Используйте localhost:3000.
Установка Nginx
Начнём с базовой установки на Ubuntu/Debian:
# Обновляем систему sudo apt update && sudo apt upgrade -y # Устанавливаем Nginx sudo apt install nginx -y # Проверяем версию nginx -v # nginx version: nginx/1.18.0 # Включаем автозапуск sudo systemctl enable nginx # Открываем порты в firewall sudo ufw allow 'Nginx Full'
Временная конфигурация (для получения SSL)
Сначала нужна простая HTTP конфигурация, чтобы Let's Encrypt мог проверить домен:
# Создаём директории
sudo mkdir -p /var/www/certbot
# Создаём временный конфиг
sudo tee /etc/nginx/sites-available/example.com > /dev/null <<'EOF'
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Для Certbot challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
try_files $uri =404;
}
# Временный проброс на приложение
location / {
proxy_pass http://localhost:3000;
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;
}
}
EOF
# Активируем конфигурацию
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
# Удаляем дефолтный конфиг
sudo rm -f /etc/nginx/sites-enabled/default
# Проверяем синтаксис
sudo nginx -t
# Перезагружаем Nginx
sudo systemctl reload nginxПолучение SSL сертификата
Используем Certbot для бесплатного SSL от Let's Encrypt:
# Устанавливаем Certbot sudo apt install certbot python3-certbot-nginx -y # Получаем сертификат sudo certbot certonly --webroot \ -w /var/www/certbot \ -d example.com \ -d www.example.com \ --email your-email@example.com \ --agree-tos \ --no-eff-email # Проверяем что сертификат получен sudo ls -la /etc/letsencrypt/live/example.com/ # fullchain.pem - цепочка сертификатов # privkey.pem - приватный ключ # chain.pem - промежуточные сертификаты # cert.pem - сам сертификат
Типичная ошибка #2
Если Certbot не может получить сертификат, проверьте:
- DNS A-запись действительно указывает на ваш сервер (
nslookup example.com) - Порт 80 открыт в firewall (
sudo ufw status) - Nginx проксирует
/.well-known/acme-challenge/правильно
Production конфигурация Nginx
Теперь создаём полноценную конфигурацию с SSL, кешированием и безопасностью:
# /etc/nginx/sites-available/example.com
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;
# Cache для статики
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=static_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
# Upstream
upstream app_backend {
server localhost:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# HTTP -> HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
try_files $uri =404;
}
location / {
return 301 https://example.com$request_uri;
}
}
# HTTPS server
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
# SSL certificates
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL configuration (Mozilla Intermediate)
ssl_protocols TLSv1.2 TLSv1.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;
# SSL session
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Logging
access_log /var/log/nginx/example_access.log combined;
error_log /var/log/nginx/example_error.log warn;
# Compression
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_comp_level 6;
gzip_types text/plain text/css text/javascript application/json application/javascript application/xml;
# Health check (без rate limiting)
location /api/health {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
access_log off;
}
# Next.js static files - долгое кеширование
location /_next/static/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# Кеш на 1 год (immutable)
proxy_cache static_cache;
proxy_cache_valid 200 365d;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header X-Cache-Status $upstream_cache_status;
}
# Другая статика
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot|css|js)$ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache static_cache;
proxy_cache_valid 200 30d;
add_header Cache-Control "public, max-age=2592000";
add_header X-Cache-Status $upstream_cache_status;
}
# API routes - rate limiting
location /api/ {
limit_req zone=api burst=10 nodelay;
limit_req_status 429;
proxy_pass http://app_backend;
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_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Все остальные запросы
location / {
limit_req zone=general burst=20 nodelay;
proxy_pass http://app_backend;
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_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}
# www -> non-www redirect
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
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;
}Что тут происходит?
Rate Limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;
Создаём две зоны для rate limiting:
- general - 10 запросов в секунду для обычных страниц
- api - 5 запросов в секунду для API (строже)
Параметр burst=20 позволяет кратковременные всплески (например, загрузка страницы с множеством ресурсов).
Кеширование статики Next.js
location /_next/static/ {
proxy_cache static_cache;
proxy_cache_valid 200 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
}Next.js складывает билды в /_next/static/ с уникальными хешами. Файлы никогда не меняются (immutable), поэтому можем кешировать на год.
Почему это работает
Next.js автоматически добавляет хеши к статическим файлам:
/_next/static/chunks/app-123abc.js ← хеш меняется при изменении файла /_next/static/css/456def.css ← новый хеш = новый URL
Старые версии просто перестают запрашиваться, а новые получают новый URL. Cache busting из коробки!
Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always;
- HSTS - браузер будет использовать только HTTPS (даже если пользователь вбил http://)
- X-Frame-Options - защита от clickjacking (нельзя встроить в iframe)
- X-Content-Type-Options - браузер не будет угадывать MIME-type
С этими заголовками получите рейтинг A+ на SSL Labs.
Применение конфигурации
# Создаём директории для кеша sudo mkdir -p /var/cache/nginx # Проверяем синтаксис sudo nginx -t # Если всё ОК - перезагружаем sudo systemctl reload nginx # Проверяем что работает curl -I https://example.com # HTTP/2 200 # strict-transport-security: max-age=31536000; includeSubDomains; preload # x-frame-options: SAMEORIGIN
Автообновление SSL сертификатов
Certbot автоматически добавляет задачу в cron, но лучше перепроверить:
# Тестируем обновление (dry run) sudo certbot renew --dry-run # Добавляем перезагрузку Nginx после обновления sudo crontab -e # Добавьте строку: 0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
Проверка работы
1. HTTP → HTTPS редирект
curl -I http://example.com # HTTP/1.1 301 Moved Permanently # Location: https://example.com/
2. WWW → non-WWW редирект
curl -I https://www.example.com # HTTP/2 301 # Location: https://example.com/
3. Кеширование статики
curl -I https://example.com/_next/static/chunks/app-abc123.js # cache-control: public, max-age=31536000, immutable # x-cache-status: HIT ← файл отдан из кеша Nginx
4. SSL рейтинг
Проверьте на SSL Labs — должны получить A+.
Типичные проблемы и решения
502 Bad Gateway
Nginx не может достучаться до приложения.
# Проверяем что приложение работает curl http://localhost:3000/api/health # Проверяем Docker контейнер docker ps | grep frontend # Смотрим логи Nginx sudo tail -f /var/log/nginx/example_error.log
Deprecated "listen ... http2" directive
В новых версиях Nginx синтаксис HTTP/2 изменился.
# Старый синтаксис (deprecated) listen 443 ssl http2; # Новый синтаксис listen 443 ssl; http2 on;
Статика не кешируется
Проверьте заголовки и очистите кеш.
# Смотрим заголовки curl -I https://example.com/_next/static/chunks/app-abc.js | grep -i cache # Очищаем кеш Nginx sudo rm -rf /var/cache/nginx/* sudo systemctl reload nginx
Мониторинг и логи
# Access log
sudo tail -f /var/log/nginx/example_access.log
# Error log
sudo tail -f /var/log/nginx/example_error.log
# Топ 10 IP по количеству запросов
sudo awk '{print $1}' /var/log/nginx/example_access.log | sort | uniq -c | sort -nr | head -10
# Количество ошибок 5xx
sudo grep " 5[0-9][0-9] " /var/log/nginx/example_access.log | wc -lЧто дальше?
- CDN: Подключите Cloudflare для дополнительного кеширования и защиты от DDoS
- Monitoring: Настройте мониторинг (Prometheus + Grafana или UptimeRobot)
- Fail2ban: Установите для защиты от brute-force атак
- WAF: Рассмотрите ModSecurity для более серьёзной защиты
Выводы
Настройка Nginx для Next.js - это не просто "поставил и работает". Нужно понимать:
- Как работает архитектура (host vs Docker network)
- Какие файлы Next.js можно кешировать долго (а какие нельзя)
- Зачем нужны security headers и как они защищают
- Как работает rate limiting и от чего он спасает
Стоит ли заморачиваться? Если проект серьёзный - однозначно да. Правильная настройка Nginx даёт производительность, безопасность и масштабируемость из коробки.
💡 Полезные ссылки
Удачи с деплоем! 🚀