Files
vidconf/deploy/nginx/nginx.conf.template
Max Ronzhin 991e5cd756 nginx: /openapi.json перехватывался SPA-фолбэком вместо схемы backend
FastAPI отдаёт /openapi.json на корневом пути (маршруты backend объявлены
с явным префиксом /api/... в декораторах, а не через APIRouter(prefix=)),
поэтому запрос попадал в catch-all location / (раздача index.html фронта)
раньше, чем мог бы дойти до backend — снаружи схема API была недоступна,
что мешало отладке.

Добавлен точный location = /openapi.json, проксирующий на backend:8000
(матчится раньше catch-all за счёт приоритета exact-match в nginx).
2026-07-26 02:57:56 +03:00

192 lines
11 KiB
Plaintext
Raw Permalink 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;
}
# --- Схема OpenAPI: FastAPI отдаёт её на КОРНЕВОМ пути /openapi.json (не
# под /api/ — маршруты backend объявлены с явным префиксом /api/... в
# декораторах, а не через APIRouter(prefix=...), см. backend/main.py),
# поэтому без этого location запрос попадал бы в SPA-фолбэк ниже
# (`location /` отдаёт index.html фронта вместо схемы — мешало отладке).
location = /openapi.json {
set $backend_upstream http://backend:8000;
proxy_pass $backend_upstream;
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;
}
# --- Медиа (аватары): раздача напрямую из 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;
}
}