NginxSSLDevOps

Nginx как reverse proxy и Let's Encrypt: конфиг и автопродление SSL

5 мин чтения

Приложение слушает localhost:3000, а наружу нужно отдать его на 80 и 443, с доменом и HTTPS. Эту задачу решает reverse proxy, и в России это почти всегда nginx. Поставить его несложно, сложнее не наступить на главную мину: сертификат, который однажды тихо не продлился и положил прод среди ночи.

Разберём рабочий конфиг nginx как reverse proxy, выпуск сертификата Let's Encrypt и, самое важное, настройку автопродления, которое не ломается.

Зачем reverse proxy

Приложение можно было бы выставить наружу напрямую, но reverse proxy перед ним делает сразу несколько полезных вещей:

  • терминирует TLS (HTTPS снаружи, обычный HTTP до приложения по localhost);
  • маршрутизирует домены и пути на разные сервисы;
  • отдаёт статику, не нагружая приложение;
  • добавляет заголовки, лимиты, сжатие и буферизацию.

Базовый конфиг proxy_pass

Минимальный server-блок, который проксирует домен на приложение по localhost:3000:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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 это не формальность. Без них приложение увидит IP самого nginx вместо реального клиента и решит, что работает по HTTP, даже когда снаружи HTTPS, а это ломает редиректы и генерацию ссылок. Проверить и перезагрузить конфиг:

sudo nginx -t          # проверка синтаксиса
sudo systemctl reload nginx

SSL через certbot

Бесплатный сертификат от Let's Encrypt выпускается утилитой certbot. Плагин для nginx сам пропишет TLS в конфиг:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

certbot проверит, что домен указывает на этот сервер (HTTP-01 challenge по порту 80), выпустит сертификат, добавит в server-блок listen 443 ssl, пути к сертификату и редирект с HTTP на HTTPS. После этого сайт открывается по HTTPS.

Автопродление, которое не ломается

Сертификаты Let's Encrypt живут 90 дней. Это сделано намеренно, чтобы продление было автоматическим, а не ручным раз в год. certbot при установке заводит systemd-таймер (или cron-job), который дважды в день пытается продлить всё, чему осталось меньше 30 дней.

Проверить, что механизм жив:

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

--dry-run прогоняет продление вхолостую против тестового сервера. Если он проходит, настоящее продление, скорее всего, тоже пройдёт. Если падает, лучше узнать об этом сейчас, а не за день до истечения.

Почему автопродление всё-таки ломается чаще всего:

  • закрыт порт 80. HTTP-01 challenge ходит на 80; если файрвол его закрыл «за ненадобностью», продление молча падает.
  • nginx не перезагрузился после обновления сертификата: новый файл выпущен, но процесс держит в памяти старый. Лечится deploy-хуком: certbot renew --deploy-hook "systemctl reload nginx".
  • никто не смотрит на результат. Падение продления ничем себя не проявляет, пока сертификат не протухнет. Стоит повесить внешний мониторинг срока действия сертификата и алерт за неделю до истечения.

WebSocket и long-polling

Если приложение использует WebSocket (чаты, real-time, часть ботов), стандартного proxy_pass мало: нужно разрешить апгрейд протокола:

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_read_timeout 3600s;   # не рвать долгие соединения
}

Без заголовков Upgrade/Connection соединение установится как обычный HTTP и тут же оборвётся, а в логах будет загадочный 400.

Типичные ошибки

  • 502 Bad Gateway: nginx жив, а приложение за ним лежит или слушает другой порт. Смотреть надо на upstream, а не на nginx.
  • Цикл редиректов: приложение не видит X-Forwarded-Proto, считает соединение незащищённым и снова шлёт на HTTPS.
  • Mixed content: страница по HTTPS грузит ресурсы по HTTP; помогает корректный X-Forwarded-Proto и относительные пути.
  • Тихо протухший сертификат: самый дорогой случай, когда всё работало месяцами, продление сломалось незаметно, прод встал. Поэтому --dry-run и внешний мониторинг важнее, чем сам выпуск.

Чеклист

  • В proxy_pass проброшены заголовки Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto.
  • Сертификат выпущен и HTTP редиректит на HTTPS.
  • certbot renew --dry-run проходит без ошибок.
  • Порт 80 открыт для HTTP-01 challenge.
  • Настроен deploy-хук, перезагружающий nginx после продления.
  • Есть внешний мониторинг срока действия сертификата.
  • Для WebSocket добавлены заголовки Upgrade/Connection и увеличен proxy_read_timeout.

Reverse proxy и сертификаты это классический пример работы, которую делаешь один раз, а потом годами надеешься, что автопродление не сломается. На managed-платформе этот слой закрыт по умолчанию: на Hostim маршрутизация и SSL поднимаются автоматически (Traefik + Let's Encrypt), сертификаты продлеваются сами, а WebSocket работает без ручной правки конфигов, а отдельный nginx и certbot держать не нужно.

Читать дальше