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 (сейчас не
  используются кодом, оставлены про запас).
This commit is contained in:
2026-07-25 21:57:46 +03:00
parent 896455381a
commit 79003eb56c
10 changed files with 343 additions and 124 deletions

View File

@@ -1,38 +0,0 @@
# Dev config for coturn. Verified against the official documentation
# (github.com/coturn/coturn/wiki/turnserver, docker/coturn/README.md).
# Runs with network_mode: host in docker-compose.yml (recommended by coturn
# docs for large UDP relay port ranges).
#
# Secrets (realm / static-auth-secret) come from .env via TURN_REALM /
# TURN_STATIC_AUTH_SECRET; coturn substitutes $(VAR) at startup when invoked
# through the image's docker-entrypoint.sh, which evaluates each CLI arg.
# Since we pass a config file instead of CLI flags, keep real secrets in the
# .env and inject them here through docker-compose "environment" + a
# lightweight envsubst step if/when TLS certs are added; for the dev/plain
# profile the plaintext defaults below are fine (`change-me` values only).
listening-port=3478
# Set to 443 in production (TURN over TLS) once real TLS certs are mounted.
tls-listening-port=5349
# Relay port range for TURN allocations.
min-port=49160
max-port=49200
# --- Long-term credential mechanism via shared secret (TURN REST API) ---
use-auth-secret
static-auth-secret=change-me-turn-secret
realm=vidconf.local
# Required by WebRTC clients (adds STUN FINGERPRINT attribute).
fingerprint
# No CLI/telnet admin interface in this dev deployment.
no-cli
# Uncomment and mount real certs to enable TURN over TLS on 443:
# cert=/etc/coturn/certs/cert.pem
# pkey=/etc/coturn/certs/key.pem
log-file=stdout
simple-log

View File

@@ -0,0 +1,47 @@
# Конфигурация coturn. Верифицировано по официальной документации
# (github.com/coturn/coturn/wiki/turnserver, docker/coturn/README.md).
# Запускается с network_mode: host в docker-compose.yml (рекомендация coturn
# для широких диапазонов UDP relay-портов).
#
# coturn подставляет $(VAR) при старте ТОЛЬКО когда вызывается через
# CLI-флаги entrypoint'а образа — при передаче файла конфигурации (как
# здесь, `-c /etc/coturn/turnserver.conf`) подстановка не работает. Поэтому
# реальные static-auth-secret/realm/external-ip рендерятся из этого шаблона
# в turnserver.conf скриптом deploy/render-templates.sh (envsubst) ПЕРЕД
# `docker compose up`. Не редактируйте turnserver.conf напрямую — правки
# затрутся при следующем рендере.
listening-port=3478
# Установить 5349 в проде для TURN over TLS, если будут смонтированы
# реальные TLS-сертификаты (см. закомментированные cert/pkey ниже).
tls-listening-port=5349
# Диапазон relay-портов для TURN-аллокаций.
min-port=49160
max-port=49200
# --- Long-term credential mechanism через shared secret (TURN REST API) ---
use-auth-secret
static-auth-secret=${TURN_STATIC_AUTH_SECRET}
realm=${TURN_REALM}
# Обязателен для WebRTC-клиентов (добавляет STUN FINGERPRINT атрибут).
fingerprint
# Без CLI/telnet admin-интерфейса в этой поставке.
no-cli
# Раскомментировать и смонтировать реальные сертификаты, чтобы включить
# TURN over TLS на 443:
# cert=/etc/coturn/certs/cert.pem
# pkey=/etc/coturn/certs/key.pem
log-file=stdout
simple-log
# Внешний IP сервера — обязателен для клиентов вне docker-сети
# (network_mode: host здесь не даёт coturn определить публичный IP
# автоматически). Для локальной разработки (без внешних участников)
# достаточно 127.0.0.1 (значение по умолчанию в .env.example); в проде —
# реальный внешний IP/домен сервера.
external-ip=${TURN_EXTERNAL_IP}

View File

@@ -51,8 +51,8 @@ services:
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-change-me-livekit-secret}
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:?LIVEKIT_API_KEY не задан в .env}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:?LIVEKIT_API_SECRET не задан в .env}
LIVEKIT_PUBLIC_URL: ${LIVEKIT_PUBLIC_URL:-ws://localhost:7880}
# Внутренний server-to-server адрес LiveKit (RoomService/Egress API).
# Жёстко перекрывает значение из .env: там LIVEKIT_URL=ws://localhost:7880
@@ -299,25 +299,52 @@ services:
# Собственный образ: nginx со вкомпилированной статикой фронтенд-SPA
# (frontend/Dockerfile, multi-stage: Vite-сборка → nginx). Раздаётся во
# ВСЕХ пресетах (сервис без profiles), поэтому http://localhost/ отдаёт
# рабочий фронт, а не заглушку. Конфиг реверс-прокси по-прежнему
# монтируется томом ниже — правки прокси не требуют пересборки фронта.
# рабочий фронт, а не заглушку.
build:
context: ../frontend
dockerfile: Dockerfile
args:
# Инлайнится Vite на этапе сборки (см. frontend/Dockerfile) — сейчас
# фронтенд получает LiveKit URL в рантайме от backend (join.livekit_url,
# см. backend/services/conference_access.py: LIVEKIT_PUBLIC_URL), эти
# build-args пока ни на что не влияют (см. комментарий в
# frontend/Dockerfile), переданы про запас на будущее.
VITE_LIVEKIT_URL: ${VITE_LIVEKIT_URL:-}
NEXT_PUBLIC_LIVEKIT_URL: ${NEXT_PUBLIC_LIVEKIT_URL:-}
image: vidconf-nginx:latest
restart: unless-stopped
environment:
# Подставляются штатным entrypoint'ом образа nginx (envsubst-on-templates,
# см. deploy/nginx/nginx.conf.template) — список доменов и имя каталога
# сертификата вынесены в параметры, а не хардкод.
NGINX_SERVER_NAMES: ${NGINX_SERVER_NAMES:?NGINX_SERVER_NAMES не задан в .env (список доменов через пробел)}
NGINX_CERT_NAME: ${NGINX_CERT_NAME:?NGINX_CERT_NAME не задан в .env (каталог в /etc/letsencrypt/live/)}
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
# Готовит /etc/nginx/certs/{fullchain,privkey}.pem до старта nginx: либо
# копирует боевой сертификат из /etc/letsencrypt, либо (если его нет —
# локальная разработка) генерирует самоподписанный. См. сам скрипт.
- ./nginx/docker-entrypoint-certs.sh:/docker-entrypoint.d/15-vidconf-certs.sh:ro
# Аватары: тот же volume, что у backend — nginx раздаёт файлы
# напрямую (`location /media/`, alias), в обход backend-процесса.
- media:/media:ro
# Реальные сертификаты Let's Encrypt — с хоста, только на чтение (в dev
# без ./certbot-webroot Docker создаст /etc/letsencrypt пустым — скрипт
# выше в этом случае сгенерирует самоподписанный сертификат).
- /etc/letsencrypt:/etc/letsencrypt:ro
# Webroot для ACME http-01 challenge (certbot renew).
- ./certbot-webroot:/var/www/certbot:rw
ports:
- "80:80"
- "443:443"
depends_on:
backend:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:80/"]
# /healthz (не /) — после добавления HTTP→HTTPS редиректа (443) `/`
# на порту 80 отдаёт 301 вместо 200, что уронило бы healthcheck;
# /healthz отвечает 200 без редиректа.
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:80/healthz"]
interval: 10s
timeout: 5s
retries: 5
@@ -329,6 +356,10 @@ services:
image: livekit/livekit-server:latest
restart: unless-stopped
command: --config /etc/livekit.yaml
# Реальный livekit.yaml генерируется из шаблона перед стартом
# (deploy/render-templates.sh, вызывается install.sh) — use_external_ip/
# node_ip/webhook.api_key статичны и не читают env напрямую, в отличие
# от LIVEKIT_KEYS ниже (см. комментарий в livekit.yaml.template).
volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
environment:
@@ -336,7 +367,7 @@ services:
# иначе LiveKit падает в crash-loop с "Could not parse keys". Значение
# обязательно в кавычках: YAML запрещает ": " внутри неквотированного
# plain-скаляра.
LIVEKIT_KEYS: "${LIVEKIT_API_KEY:-devkey}: ${LIVEKIT_API_SECRET:-change-me-livekit-secret}"
LIVEKIT_KEYS: "${LIVEKIT_API_KEY:?LIVEKIT_API_KEY не задан в .env}: ${LIVEKIT_API_SECRET:?LIVEKIT_API_SECRET не задан в .env}"
ports:
- "7880:7880" # HTTP/WebSocket signaling
- "7881:7881" # RTC TCP fallback
@@ -361,11 +392,14 @@ services:
image: coturn/coturn:latest
restart: unless-stopped
command: -c /etc/coturn/turnserver.conf
# Секреты (static-auth-secret/realm/external-ip) НЕ передаются через
# environment — coturn читает их ТОЛЬКО из файла конфигурации, запущенного
# через `-c` (при CLI-флагах он умеет $(VAR), при файле — нет, см.
# комментарий в deploy/coturn/turnserver.conf.template). Реальный
# turnserver.conf генерируется из шаблона перед стартом
# (deploy/render-templates.sh, вызывается install.sh).
volumes:
- ./coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro
environment:
TURN_REALM: ${TURN_REALM:-vidconf.local}
TURN_STATIC_AUTH_SECRET: ${TURN_STATIC_AUTH_SECRET:-change-me-turn-secret}
network_mode: host
# Образ coturn/coturn — минимальный (debian-slim), в нём нет pgrep/ps/nc/
# curl/wget, поэтому проверка процесса по имени не работает
@@ -414,8 +448,8 @@ services:
# api_key/api_secret/ws_url — обязательные переменные окружения
# (см. deploy/egress/egress.yaml); ws_url — внутренний адрес сервиса
# livekit в docker-сети, НЕ LIVEKIT_PUBLIC_URL для браузера.
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-change-me-livekit-secret}
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:?LIVEKIT_API_KEY не задан в .env}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:?LIVEKIT_API_SECRET не задан в .env}
LIVEKIT_WS_URL: ${LIVEKIT_WS_URL:-ws://livekit:7880}
depends_on:
# required: false — livekit живёт в профиле `media`, а не

View File

@@ -1,60 +0,0 @@
# Dev config for LiveKit SFU. Verified against the official documentation
# (github.com/livekit/livekit config-sample.yaml + configuration.md).
# Real API key/secret must come from .env (LIVEKIT_API_KEY / LIVEKIT_API_SECRET);
# this file intentionally omits `keys:` so it can be supplied via the
# LIVEKIT_KEYS env var (format "key:secret") set in docker-compose.yml.
port: 7880
rtc:
tcp_port: 7881
# Диапазон сужен для dev (см. комментарий в docker-compose.yml); в проде
# расширить и синхронизировать с пробросом портов.
port_range_start: 54000
port_range_end: 54100
# use_external_ip: false + node_ip — dev-режим по докам LiveKit
# (rtc.node_ip / use_external_ip в config-sample.yaml): use_external_ip
# определяет публичный IP через STUN, что в контейнере Docker Desktop на
# macOS даёт недостижимый изнутри хоста внутренний IP (172.18.x.x) — из-за
# этого DTLS-хендшейк по reliable/lossy data-каналам не проходит (см.
# "dtls timeout" в логах). node_ip: 127.0.0.1 работает, потому что порты
# 7881/tcp и 54000-54100/udp проброшены Docker Desktop на loopback хоста,
# а браузер-клиент запускается на том же хосте. В проде (клиенты снаружи
# хоста) node_ip заменить на реальный внешний IP/домен либо вернуть
# use_external_ip: true, если сервер не за NAT с пробросом портов 1:1.
use_external_ip: false
node_ip: 127.0.0.1
# Redis обязателен для сервиса egress (см. deploy/egress/) — он использует
# его как pub/sub и key-value хранилище состояния запущенных записей;
# без него egress не может получать room/track-события от LiveKit
# (проверено по официальной документации livekit/egress, раздел "Running
# locally"). LiveKit сам по себе тоже использует redis для координации
# между узлами кластера (здесь один узел, но сервис оставлен включённым).
redis:
address: redis:6379
# TURN is handled by the standalone coturn service (deploy/coturn) in the
# `media` profile. When exposing LiveKit publicly, put coturn/TURN-TLS on 443
# and keep this section disabled to avoid double TURN servers.
turn:
enabled: false
# Webhook-приёмник backend'а: события room_started/participant_joined/
# participant_left/room_finished подписываются ключом api_key, который
# должен совпадать с одним из ключей в LIVEKIT_KEYS (см. выше).
webhook:
api_key: devkey
urls:
- http://backend:8000/api/v1/livekit/webhook
# Если backend запускается на хосте (uvicorn вне docker-compose, а
# LiveKit — внутри), используйте вместо этого:
# - http://host.docker.internal:8000/api/v1/livekit/webhook
logging:
level: info
json: false
# Prometheus metrics (scraped by the monitoring profile).
prometheus:
port: 6789

View File

@@ -0,0 +1,68 @@
# LiveKit SFU. Верифицировано по официальной документации
# (github.com/livekit/livekit config-sample.yaml + configuration.md).
# Реальный API key/secret — из .env (LIVEKIT_API_KEY / LIVEKIT_API_SECRET);
# этот файл намеренно не содержит `keys:` — ключи передаются через
# LIVEKIT_KEYS (см. docker-compose.yml, формат "key: secret").
#
# LiveKit НЕ читает переменные окружения внутри своего YAML-конфига (в
# отличие от `keys:`, которые подаются отдельно через LIVEKIT_KEYS) —
# статические поля (use_external_ip/node_ip/webhook.api_key) рендерятся
# из этого шаблона в livekit.yaml скриптом deploy/render-templates.sh
# (envsubst) ПЕРЕД `docker compose up`. Не редактируйте livekit.yaml
# напрямую — правки затрутся при следующем рендере.
port: 7880
rtc:
tcp_port: 7881
# Диапазон сужен для dev (см. комментарий в docker-compose.yml); в проде
# расширить и синхронизировать с пробросом портов.
port_range_start: 54000
port_range_end: 54100
# use_external_ip: false + node_ip=127.0.0.1 — режим для локальной
# разработки (Docker Desktop): use_external_ip=true определяет публичный
# IP через STUN, что в контейнере на macOS даёт недостижимый изнутри хоста
# внутренний IP (172.18.x.x) — DTLS-хендшейк по data-каналам не проходит
# ("dtls timeout" в логах). node_ip=127.0.0.1 работает, потому что порты
# 7881/tcp и 54000-54100/udp проброшены на loopback хоста, а браузер-клиент
# запускается на том же хосте.
# В проде (LIVEKIT_USE_EXTERNAL_IP=true, LIVEKIT_NODE_IP=<внешний IP/домен
# сервера> в .env) клиенты снаружи хоста подключаются по этому адресу —
# сервер не должен быть за NAT без проброса портов 1:1.
use_external_ip: ${LIVEKIT_USE_EXTERNAL_IP}
node_ip: ${LIVEKIT_NODE_IP}
# Redis обязателен для сервиса egress (см. deploy/egress/) — он использует
# его как pub/sub и key-value хранилище состояния запущенных записей;
# без него egress не может получать room/track-события от LiveKit
# (проверено по официальной документации livekit/egress, раздел "Running
# locally"). LiveKit сам по себе тоже использует redis для координации
# между узлами кластера (здесь один узел, но сервис оставлен включённым).
redis:
address: redis:6379
# TURN обслуживает отдельный сервис coturn (профиль `media`, deploy/coturn).
# При публичной экспозиции LiveKit разместите coturn/TURN-TLS на 443 и
# держите эту секцию отключённой, чтобы не поднимать два TURN-сервера.
turn:
enabled: false
# Webhook-приёмник backend'а: события room_started/participant_joined/
# participant_left/room_finished подписываются ключом api_key, который
# должен совпадать с одним из ключей в LIVEKIT_KEYS (см. выше и
# docker-compose.yml) — берём тот же LIVEKIT_API_KEY из .env.
webhook:
api_key: ${LIVEKIT_API_KEY}
urls:
- http://backend:8000/api/v1/livekit/webhook
# Если backend запускается на хосте (uvicorn вне docker-compose, а
# LiveKit — внутри), используйте вместо этого:
# - http://host.docker.internal:8000/api/v1/livekit/webhook
logging:
level: info
json: false
# Prometheus metrics (scraped by the monitoring profile).
prometheus:
port: 6789

View File

@@ -0,0 +1,35 @@
#!/bin/sh
# Готовит /etc/nginx/certs/{fullchain,privkey}.pem ДО того, как штатный
# entrypoint официального образа nginx (20-envsubst-on-templates.sh, потом
# сам nginx) прочитает конфиг с `ssl_certificate` (см.
# deploy/nginx/nginx.conf.template). Монтируется томом в docker-compose.yml
# в /docker-entrypoint.d/ (рядом с nginx.conf.template — конфиг и
# entrypoint-скрипты nginx монтируются, а не запекаются в образ, чтобы правки
# не требовали пересборки фронта), где нативный entrypoint nginx исполняет
# все *.sh по алфавиту до старта самого nginx.
#
# Прод: реальный сертификат смонтирован с хоста в /etc/letsencrypt (ro,
# см. docker-compose.yml) — копируем его.
# Dev (docs/deploy/dev-setup.md, `docker compose up` без реальных сертификатов
# Let's Encrypt): каталог пуст/отсутствует — генерируем самоподписанный
# сертификат, чтобы локальный стек стартовал без ручной настройки TLS.
set -e
CERT_NAME="${NGINX_CERT_NAME:-localhost}"
SRC="/etc/letsencrypt/live/${CERT_NAME}"
DEST=/etc/nginx/certs
mkdir -p "$DEST"
if [ -f "$SRC/fullchain.pem" ] && [ -f "$SRC/privkey.pem" ]; then
echo "[nginx-certs] Использую боевой сертификат из $SRC"
cp "$SRC/fullchain.pem" "$DEST/fullchain.pem"
cp "$SRC/privkey.pem" "$DEST/privkey.pem"
else
echo "[nginx-certs] $SRC не найден — генерирую самоподписанный сертификат (ТОЛЬКО для локальной разработки, в проде смонтируйте /etc/letsencrypt)"
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout "$DEST/privkey.pem" -out "$DEST/fullchain.pem" \
-subj "/CN=${CERT_NAME}" 2>/dev/null
fi
chmod 644 "$DEST/fullchain.pem" "$DEST/privkey.pem"

View File

@@ -1,6 +1,14 @@
# Dev reverse proxy: backend API + раздача фронтенд-SPA (статика в образе).
# Завершение TLS для production документировано в docs/deploy/dev-setup.md
# и должно добавляться отдельным server-блоком (443) с реальными сертификатами.
# 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 в памяти. После пересоздания
@@ -17,9 +25,57 @@ map $http_upgrade $connection_upgrade {
'' close;
}
# ==========================================
# БЛОК 1: HTTP (порт 80)
# Задача: ACME-challenge для Certbot + редирект всего остального на HTTPS.
# ==========================================
server {
listen 80;
server_name _;
# Список доменов — параметр (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 с переменной НЕ выполняет
@@ -42,11 +98,13 @@ server {
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 для чата и LiveKit signaling через nginx ---
# --- WebSocket upgrade для чата ---
location /ws/ {
set $backend_upstream http://backend:8000;
proxy_pass $backend_upstream;
@@ -54,6 +112,9 @@ server {
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;
}
@@ -78,6 +139,7 @@ server {
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;
}
@@ -112,9 +174,4 @@ server {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /healthz {
return 200 "ok\n";
add_header Content-Type text/plain;
}
}

52
deploy/render-templates.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Рендерит статические конфиги coturn/LiveKit из шаблонов (deploy/**/*.template)
# в реальные файлы, которые монтирует docker-compose. Оба образа (coturn,
# livekit-server) НЕ поддерживают подстановку переменных окружения в файле
# конфигурации (см. комментарии в *.template) — единственный способ вынести
# секреты/IP в .env и не хранить их в git — сгенерировать файл перед стартом.
#
# Вызывается автоматически из install.sh. При ручном запуске (без install.sh)
# запускайте ПЕРЕД `docker compose up` (и после каждого изменения .env,
# влияющего на эти конфиги) — из корня репозитория:
# ./deploy/render-templates.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"
if [ ! -f "$ENV_FILE" ]; then
echo "render-templates.sh: не найден $ENV_FILE — сначала запустите ./install.sh или скопируйте .env.example в .env" >&2
exit 1
fi
# Читаем только нужные ключи через grep/cut (НЕ `source .env`) — .env содержит
# значения вроде `SMTP_FROM=VidConf <no-reply@vidconf.example>`, где `<` —
# валидный литерал для docker-compose/pydantic, но невалидный bash-синтаксис
# (интерпретируется как редирект) при попытке `source` файла целиком.
env_var() {
grep -E "^${1}=" "$ENV_FILE" 2>/dev/null | tail -1 | cut -d= -f2-
}
TURN_STATIC_AUTH_SECRET="$(env_var TURN_STATIC_AUTH_SECRET)"
TURN_REALM="$(env_var TURN_REALM)"
TURN_EXTERNAL_IP="$(env_var TURN_EXTERNAL_IP)"
LIVEKIT_API_KEY="$(env_var LIVEKIT_API_KEY)"
LIVEKIT_NODE_IP="$(env_var LIVEKIT_NODE_IP)"
LIVEKIT_USE_EXTERNAL_IP="$(env_var LIVEKIT_USE_EXTERNAL_IP)"
: "${TURN_STATIC_AUTH_SECRET:?TURN_STATIC_AUTH_SECRET не задан в .env}"
: "${TURN_REALM:?TURN_REALM не задан в .env}"
: "${TURN_EXTERNAL_IP:?TURN_EXTERNAL_IP не задан в .env (внешний IP/домен сервера для TURN-релея)}"
: "${LIVEKIT_API_KEY:?LIVEKIT_API_KEY не задан в .env}"
: "${LIVEKIT_NODE_IP:?LIVEKIT_NODE_IP не задан в .env (внешний IP/домен для ICE)}"
: "${LIVEKIT_USE_EXTERNAL_IP:?LIVEKIT_USE_EXTERNAL_IP не задан в .env (true/false)}"
export TURN_STATIC_AUTH_SECRET TURN_REALM TURN_EXTERNAL_IP LIVEKIT_API_KEY LIVEKIT_NODE_IP LIVEKIT_USE_EXTERNAL_IP
envsubst '${TURN_STATIC_AUTH_SECRET} ${TURN_REALM} ${TURN_EXTERNAL_IP}' \
< "$SCRIPT_DIR/coturn/turnserver.conf.template" > "$SCRIPT_DIR/coturn/turnserver.conf"
echo "[render] deploy/coturn/turnserver.conf готов"
envsubst '${LIVEKIT_USE_EXTERNAL_IP} ${LIVEKIT_NODE_IP} ${LIVEKIT_API_KEY}' \
< "$SCRIPT_DIR/livekit/livekit.yaml.template" > "$SCRIPT_DIR/livekit/livekit.yaml"
echo "[render] deploy/livekit/livekit.yaml готов"