📚ByteGuide
Назад к статьям
DevOpsNginxNext.js

Настройка Nginx для Next.js в продакшене: от SSL до оптимизации

Полный гайд по настройке Nginx для Next.js приложения с SSL, кешированием, security headers и решением реальных проблем из практики.

15 минут чтения

Зачем это нужно?

Когда я впервые деплоил 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 даёт производительность, безопасность и масштабируемость из коробки.

Удачи с деплоем! 🚀