Files
vidconf/deploy/nginx/nginx.conf.template
Max Ronzhin 79003eb56c deploy: harden nginx/coturn/livekit configs (TLS on 443, templates, required secrets)
Переносит рабочие правки продакшен-сервера в репозиторий воспроизводимо:

- nginx: HTTPS-блок на 443 (TLS1.2/1.3, http->https redirect, ACME
  webroot, X-Forwarded-Proto) добавлен в nginx.conf.template. Список
  доменов и имя каталога сертификата — параметры (NGINX_SERVER_NAMES,
  NGINX_CERT_NAME), не хардкод. Рендерится штатным entrypoint'ом образа
  nginx (envsubst-on-templates).
- Самоподписанный dev-сертификат генерируется на старте контейнера
  (docker-entrypoint-certs.sh), если реальный Let's Encrypt не смонтирован
  — иначе `docker compose up` без профиля ломался бы локально без
  реальных сертификатов.
- healthcheck nginx переключён на /healthz (без TLS-редиректа), иначе
  http://127.0.0.1:80/ после добавления 443-редиректа всегда unhealthy.
- coturn/turnserver.conf и livekit/livekit.yaml переведены в *.template —
  оба образа не читают env для статических полей (static-auth-secret,
  use_external_ip/node_ip, webhook.api_key), поэтому реальные файлы
  генерируются перед стартом стека скриптом deploy/render-templates.sh
  (вызывается install.sh). Секреты/IP — только в .env, не в git.
- docker-compose.yml: LIVEKIT_API_KEY/SECRET везде (backend, livekit,
  egress) через ${VAR:?} без небезопасных дефолтов; порт 443 + монтирование
  /etc/letsencrypt (ro) и certbot-webroot у nginx; build-args
  VITE_LIVEKIT_URL/NEXT_PUBLIC_LIVEKIT_URL для frontend (сейчас не
  используются кодом, оставлены про запас).
2026-07-25 21:57:46 +03:00

178 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Reverse proxy: backend API + раздача фронтенд-SPA (статика в образе) +
# LiveKit SFU signaling. TLS-терминация — здесь же (см. блок HTTPS ниже),
# сертификаты берутся из /etc/letsencrypt (см. docker-compose.yml, volume ro).
#
# Шаблон рендерится штатным entrypoint-скриптом официального образа nginx
# (docker-entrypoint.d/20-envsubst-on-templates.sh): при старте контейнера
# envsubst подставляет ТОЛЬКО переменные, реально заданные в окружении
# контейнера (см. `environment:` сервиса nginx в docker-compose.yml), поэтому
# нативные nginx-переменные ($scheme, $host, $http_upgrade, $remote_addr,
# $connection_upgrade и т.п.) остаются нетронутыми — с ними коллизий по
# именам нет. Результат пишется в /etc/nginx/conf.d/default.conf.
#
# ВАЖНО (stale DNS): статический `upstream { server backend:8000; }` nginx
# резолвит один раз при старте/reload и держит IP в памяти. После пересоздания
# контейнера backend (`docker compose up -d --force-recreate backend`) Docker
# выдаёт ему новый IP, а nginx продолжает стучаться по старому → 502, пока
# nginx не перезапустят. Чтобы резолвить имя заново на каждый запрос,
# используем embedded DNS Docker (127.0.0.11) через directive `resolver` и
# ПЕРЕМЕННУЮ в proxy_pass — переменные nginx не кэшируются на старте и
# резолвятся заново по истечении `valid=`.
resolver 127.0.0.11 valid=10s ipv6=off;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# ==========================================
# БЛОК 1: HTTP (порт 80)
# Задача: ACME-challenge для Certbot + редирект всего остального на HTTPS.
# ==========================================
server {
listen 80;
# Список доменов — параметр (NGINX_SERVER_NAMES в .env), не хардкод.
server_name ${NGINX_SERVER_NAMES};
# Раздача файлов для (авто)обновления Certbot — ОБЯЗАТЕЛЬНО оставить
# для работы webroot-плагина.
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Healthcheck контейнера (см. docker-compose.yml) ходит сюда напрямую по
# HTTP — редирект ниже иначе увёл бы его на HTTPS и ломал бы wget без -L.
location /healthz {
return 200 "ok\n";
add_header Content-Type text/plain;
}
# Жёсткий редирект всего остального на HTTPS.
location / {
return 301 https://$host$request_uri;
}
}
# ==========================================
# БЛОК 2: HTTPS (порт 443)
# Задача: вся основная логика приложения — API, фронтенд, WebSocket, LiveKit.
# ==========================================
server {
listen 443 ssl;
server_name ${NGINX_SERVER_NAMES};
# Путь СТАТИЧЕН (не envsubst) — реальный источник переменный:
# /docker-entrypoint.d/15-vidconf-certs.sh (запускается раньше рендера
# этого шаблона) копирует сюда боевой сертификат из
# /etc/letsencrypt/live/${NGINX_CERT_NAME}, если он смонтирован с хоста
# (см. docker-compose.yml), иначе генерирует самоподписанный — это
# позволяет `docker compose up` стартовать локально (dev) БЕЗ реальных
# сертификатов Let's Encrypt (см. docs/deploy/dev-setup.md).
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# --- Backend API ---
# Проброс через переменную: proxy_pass с переменной НЕ выполняет
# автоматическую подстановку URI (в отличие от статического
# `proxy_pass http://upstream/prefix/;`), поэтому URI-часть в proxy_pass
# не указываем — nginx передаёт исходный URI запроса как есть (path +
# query), что и сохраняет прежний маппинг /api/ -> backend:8000/api/
# (префикс совпадает 1-в-1, подмены не требовалось и раньше).
# Upgrade-заголовки нужны и обычным HTTP-запросам (без `Upgrade` в запросе
# `map $http_upgrade $connection_upgrade` выше подставляет `close`, что не
# ломает keep-alive обычных ответов), и WS-эндпоинту чата конференции
# (`WS /api/v1/conferences/{id}/chat`) — он живёт под тем же
# префиксом /api/, а не под отдельным /ws/.
location /api/ {
set $backend_upstream http://backend:8000;
proxy_pass $backend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $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;
# $scheme здесь равен 'https' — критично, чтобы бэкенд (FastAPI)
# генерировал корректные абсолютные ссылки и принимал Secure-куки.
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
# --- WebSocket upgrade для чата ---
location /ws/ {
set $backend_upstream http://backend:8000;
proxy_pass $backend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $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 3600s;
}
# --- LiveKit SFU signaling (WebSocket) ---
# Доступен только когда стек запущен с профилем `media`
# (см. docker-compose.yml) — без него resolver вернёт NXDOMAIN на первый
# запрос к этому location, что ожидаемо.
# Здесь префикс /livekit/ нужно СНЯТЬ (как раньше делал
# `proxy_pass http://livekit_upstream/;`). С переменной в proxy_pass
# автоматическая подмена недоступна, поэтому переписываем URI явно через
# rewrite ... break — nginx передаст на upstream уже переписанный $uri.
# ВАЖНО: `set` должен идти ДО `rewrite ... break` — break прерывает
# выполнение всех последующих директив модуля rewrite (включая set) в
# этом location, иначе переменная останется неинициализированной.
location /livekit/ {
set $livekit_upstream http://livekit:7880;
rewrite ^/livekit/(.*)$ /$1 break;
proxy_pass $livekit_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $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 3600s;
}
# --- Метрики backend: `GET /metrics` без авторизации
# внутри приложения (backend/api/metrics.py) — снаружи периметра нарочно
# закрываем явным запретом. Это ОБЯЗАТЕЛЬНО именно exact-match
# (`location = /metrics`): ниже есть catch-all `location /` (раздача
# фронтенд-SPA), который иначе отдал бы на /metrics файл фронта (или
# index.html через try_files). Exact-match в nginx приоритетнее любого
# prefix-location, поэтому 403 срабатывает раньше SPA-раздачи.
# Prometheus (профиль compose `monitoring`) ходит в backend НАПРЯМУЮ по
# внутренней docker-сети (`backend:8000/metrics`), минуя nginx.
location = /metrics {
return 403;
}
# --- Медиа (аватары): раздача напрямую из volume, в обход backend. ---
location /media/ {
alias /media/;
autoindex off;
}
# --- Frontend SPA (React + Vite): статика вкомпилирована в образ nginx
# (frontend/Dockerfile) в /usr/share/nginx/html. `try_files` с
# history-fallback на /index.html нужен для клиентского роутинга
# react-router (deep-ссылки вида /conferences/<id>, /admin/settings —
# при перезагрузке страницы отдаётся index.html, дальше роутит SPA).
# Более специфичные location выше (/api/, /media/, = /metrics, /livekit/,
# /ws/) матчатся раньше этого catch-all, поэтому API и метрики не
# перехватываются фронтом.
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}