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:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -35,9 +35,17 @@ frontend/coverage/
|
|||||||
# Docker
|
# Docker
|
||||||
deploy/**/*.local.yml
|
deploy/**/*.local.yml
|
||||||
|
|
||||||
|
# Сгенерированные из *.template скриптом deploy/render-templates.sh —
|
||||||
|
# содержат реальные секреты/IP, рендерятся перед `docker compose up`.
|
||||||
|
deploy/coturn/turnserver.conf
|
||||||
|
deploy/livekit/livekit.yaml
|
||||||
|
|
||||||
# Артефакты Celery beat
|
# Артефакты Celery beat
|
||||||
celerybeat-schedule*.db
|
celerybeat-schedule*.db
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
|
|
||||||
# Загруженные медиа-файлы (аватары) — dev-каталог backend/media
|
# Загруженные медиа-файлы (аватары) — dev-каталог backend/media
|
||||||
backend/media/
|
backend/media/
|
||||||
|
|
||||||
|
# Рабочая папка Claude Code (промпты, агенты, заметки) — не в репозиторий
|
||||||
|
.forcc/
|
||||||
|
|||||||
@@ -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
|
|
||||||
47
deploy/coturn/turnserver.conf.template
Normal file
47
deploy/coturn/turnserver.conf.template
Normal 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}
|
||||||
@@ -51,8 +51,8 @@ services:
|
|||||||
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
|
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
|
||||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||||
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
|
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
|
||||||
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
|
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:?LIVEKIT_API_KEY не задан в .env}
|
||||||
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-change-me-livekit-secret}
|
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:?LIVEKIT_API_SECRET не задан в .env}
|
||||||
LIVEKIT_PUBLIC_URL: ${LIVEKIT_PUBLIC_URL:-ws://localhost:7880}
|
LIVEKIT_PUBLIC_URL: ${LIVEKIT_PUBLIC_URL:-ws://localhost:7880}
|
||||||
# Внутренний server-to-server адрес LiveKit (RoomService/Egress API).
|
# Внутренний server-to-server адрес LiveKit (RoomService/Egress API).
|
||||||
# Жёстко перекрывает значение из .env: там LIVEKIT_URL=ws://localhost:7880
|
# Жёстко перекрывает значение из .env: там LIVEKIT_URL=ws://localhost:7880
|
||||||
@@ -299,25 +299,52 @@ services:
|
|||||||
# Собственный образ: nginx со вкомпилированной статикой фронтенд-SPA
|
# Собственный образ: nginx со вкомпилированной статикой фронтенд-SPA
|
||||||
# (frontend/Dockerfile, multi-stage: Vite-сборка → nginx). Раздаётся во
|
# (frontend/Dockerfile, multi-stage: Vite-сборка → nginx). Раздаётся во
|
||||||
# ВСЕХ пресетах (сервис без profiles), поэтому http://localhost/ отдаёт
|
# ВСЕХ пресетах (сервис без profiles), поэтому http://localhost/ отдаёт
|
||||||
# рабочий фронт, а не заглушку. Конфиг реверс-прокси по-прежнему
|
# рабочий фронт, а не заглушку.
|
||||||
# монтируется томом ниже — правки прокси не требуют пересборки фронта.
|
|
||||||
build:
|
build:
|
||||||
context: ../frontend
|
context: ../frontend
|
||||||
dockerfile: Dockerfile
|
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
|
image: vidconf-nginx:latest
|
||||||
restart: unless-stopped
|
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:
|
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 раздаёт файлы
|
# Аватары: тот же volume, что у backend — nginx раздаёт файлы
|
||||||
# напрямую (`location /media/`, alias), в обход backend-процесса.
|
# напрямую (`location /media/`, alias), в обход backend-процесса.
|
||||||
- media:/media:ro
|
- 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:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
healthcheck:
|
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
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -329,6 +356,10 @@ services:
|
|||||||
image: livekit/livekit-server:latest
|
image: livekit/livekit-server:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: --config /etc/livekit.yaml
|
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:
|
volumes:
|
||||||
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
|
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
|
||||||
environment:
|
environment:
|
||||||
@@ -336,7 +367,7 @@ services:
|
|||||||
# иначе LiveKit падает в crash-loop с "Could not parse keys". Значение
|
# иначе LiveKit падает в crash-loop с "Could not parse keys". Значение
|
||||||
# обязательно в кавычках: YAML запрещает ": " внутри неквотированного
|
# обязательно в кавычках: YAML запрещает ": " внутри неквотированного
|
||||||
# plain-скаляра.
|
# 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:
|
ports:
|
||||||
- "7880:7880" # HTTP/WebSocket signaling
|
- "7880:7880" # HTTP/WebSocket signaling
|
||||||
- "7881:7881" # RTC TCP fallback
|
- "7881:7881" # RTC TCP fallback
|
||||||
@@ -361,11 +392,14 @@ services:
|
|||||||
image: coturn/coturn:latest
|
image: coturn/coturn:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: -c /etc/coturn/turnserver.conf
|
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:
|
volumes:
|
||||||
- ./coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro
|
- ./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
|
network_mode: host
|
||||||
# Образ coturn/coturn — минимальный (debian-slim), в нём нет pgrep/ps/nc/
|
# Образ coturn/coturn — минимальный (debian-slim), в нём нет pgrep/ps/nc/
|
||||||
# curl/wget, поэтому проверка процесса по имени не работает
|
# curl/wget, поэтому проверка процесса по имени не работает
|
||||||
@@ -414,8 +448,8 @@ services:
|
|||||||
# api_key/api_secret/ws_url — обязательные переменные окружения
|
# api_key/api_secret/ws_url — обязательные переменные окружения
|
||||||
# (см. deploy/egress/egress.yaml); ws_url — внутренний адрес сервиса
|
# (см. deploy/egress/egress.yaml); ws_url — внутренний адрес сервиса
|
||||||
# livekit в docker-сети, НЕ LIVEKIT_PUBLIC_URL для браузера.
|
# livekit в docker-сети, НЕ LIVEKIT_PUBLIC_URL для браузера.
|
||||||
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
|
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:?LIVEKIT_API_KEY не задан в .env}
|
||||||
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-change-me-livekit-secret}
|
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:?LIVEKIT_API_SECRET не задан в .env}
|
||||||
LIVEKIT_WS_URL: ${LIVEKIT_WS_URL:-ws://livekit:7880}
|
LIVEKIT_WS_URL: ${LIVEKIT_WS_URL:-ws://livekit:7880}
|
||||||
depends_on:
|
depends_on:
|
||||||
# required: false — livekit живёт в профиле `media`, а не
|
# required: false — livekit живёт в профиле `media`, а не
|
||||||
|
|||||||
@@ -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
|
|
||||||
68
deploy/livekit/livekit.yaml.template
Normal file
68
deploy/livekit/livekit.yaml.template
Normal 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
|
||||||
35
deploy/nginx/docker-entrypoint-certs.sh
Executable file
35
deploy/nginx/docker-entrypoint-certs.sh
Executable 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"
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
# Dev reverse proxy: backend API + раздача фронтенд-SPA (статика в образе).
|
# Reverse proxy: backend API + раздача фронтенд-SPA (статика в образе) +
|
||||||
# Завершение TLS для production документировано в docs/deploy/dev-setup.md
|
# LiveKit SFU signaling. TLS-терминация — здесь же (см. блок HTTPS ниже),
|
||||||
# и должно добавляться отдельным server-блоком (443) с реальными сертификатами.
|
# сертификаты берутся из /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
|
# ВАЖНО (stale DNS): статический `upstream { server backend:8000; }` nginx
|
||||||
# резолвит один раз при старте/reload и держит IP в памяти. После пересоздания
|
# резолвит один раз при старте/reload и держит IP в памяти. После пересоздания
|
||||||
@@ -17,9 +25,57 @@ map $http_upgrade $connection_upgrade {
|
|||||||
'' close;
|
'' close;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# БЛОК 1: HTTP (порт 80)
|
||||||
|
# Задача: ACME-challenge для Certbot + редирект всего остального на HTTPS.
|
||||||
|
# ==========================================
|
||||||
server {
|
server {
|
||||||
listen 80;
|
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 ---
|
# --- Backend API ---
|
||||||
# Проброс через переменную: proxy_pass с переменной НЕ выполняет
|
# Проброс через переменную: proxy_pass с переменной НЕ выполняет
|
||||||
@@ -42,11 +98,13 @@ server {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# $scheme здесь равен 'https' — критично, чтобы бэкенд (FastAPI)
|
||||||
|
# генерировал корректные абсолютные ссылки и принимал Secure-куки.
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 3600s;
|
proxy_read_timeout 3600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- WebSocket upgrade для чата и LiveKit signaling через nginx ---
|
# --- WebSocket upgrade для чата ---
|
||||||
location /ws/ {
|
location /ws/ {
|
||||||
set $backend_upstream http://backend:8000;
|
set $backend_upstream http://backend:8000;
|
||||||
proxy_pass $backend_upstream;
|
proxy_pass $backend_upstream;
|
||||||
@@ -54,6 +112,9 @@ server {
|
|||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
proxy_set_header Host $host;
|
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;
|
proxy_read_timeout 3600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +139,7 @@ server {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 3600s;
|
proxy_read_timeout 3600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +174,4 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
try_files $uri $uri/ /index.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
52
deploy/render-templates.sh
Executable 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 готов"
|
||||||
@@ -3,14 +3,19 @@
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Образ раздачи фронтенда VidConf: SPA (React + Vite) собирается в статику и
|
# Образ раздачи фронтенда VidConf: SPA (React + Vite) собирается в статику и
|
||||||
# вкомпилируется в nginx. Конфиг реверс-прокси (проксирование /api, /livekit,
|
# вкомпилируется в nginx. Конфиг реверс-прокси (проксирование /api, /livekit,
|
||||||
# /media и т.п.) НЕ хранится в образе — он монтируется томом в
|
# /media и т.п.) НЕ хранится в образе — он монтируется томом (шаблоном) в
|
||||||
# docker-compose (`./nginx/nginx.conf` → /etc/nginx/conf.d/default.conf),
|
# docker-compose (`./nginx/nginx.conf.template` → entrypoint nginx
|
||||||
# чтобы правки прокси не требовали пересборки фронта. Здесь только статика.
|
# генерирует /etc/nginx/conf.d/default.conf), чтобы правки прокси не требовали
|
||||||
|
# пересборки фронта. Здесь только статика.
|
||||||
#
|
#
|
||||||
# Фронт обращается к API по ОТНОСИТЕЛЬНЫМ путям (`/api/v1`, `/api/health` —
|
# Фронт обращается к API по ОТНОСИТЕЛЬНЫМ путям (`/api/v1`, `/api/health` —
|
||||||
# см. frontend/src/api/client.ts), поэтому build-time переменные окружения
|
# см. frontend/src/api/client.ts), поэтому build-time переменные окружения
|
||||||
# (VITE_*) не нужны: собранная статика ходит на /api своего origin, который
|
# (VITE_*) не нужны: собранная статика ходит на /api своего origin, который
|
||||||
# nginx проксирует на backend.
|
# nginx проксирует на backend. LiveKit URL фронт тоже получает в рантайме от
|
||||||
|
# backend (join.livekit_url ← LIVEKIT_PUBLIC_URL), а не из build-time env —
|
||||||
|
# ARG/ENV ниже сейчас ни на что не влияют (в исходниках frontend/src нет ни
|
||||||
|
# одной ссылки на import.meta.env.VITE_LIVEKIT_URL), приняты для совместимости
|
||||||
|
# на случай, если фронтенд когда-нибудь начнёт их читать на build-time.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# --- Стадия сборки: Vite/React/TS → статика в /app/dist ---
|
# --- Стадия сборки: Vite/React/TS → статика в /app/dist ---
|
||||||
@@ -24,8 +29,19 @@ RUN npm ci
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
# `npm run build` = `tsc -b && vite build` (см. package.json) → /app/dist
|
# `npm run build` = `tsc -b && vite build` (см. package.json) → /app/dist
|
||||||
|
ARG VITE_LIVEKIT_URL
|
||||||
|
ENV VITE_LIVEKIT_URL=${VITE_LIVEKIT_URL}
|
||||||
|
ARG NEXT_PUBLIC_LIVEKIT_URL
|
||||||
|
ENV NEXT_PUBLIC_LIVEKIT_URL=${NEXT_PUBLIC_LIVEKIT_URL}
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# --- Стадия раздачи: nginx со статикой SPA внутри образа ---
|
# --- Стадия раздачи: nginx со статикой SPA внутри образа ---
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# openssl — для генерации самоподписанного dev-сертификата, если реальный
|
||||||
|
# Let's Encrypt не смонтирован (см. deploy/nginx/docker-entrypoint-certs.sh,
|
||||||
|
# который docker-compose.yml монтирует в /docker-entrypoint.d/ — рядом с
|
||||||
|
# nginx.conf.template: конфиг/скрипты entrypoint'а nginx монтируются томом,
|
||||||
|
# а не запекаются в образ, чтобы правки не требовали пересборки фронта).
|
||||||
|
RUN apk add --no-cache openssl
|
||||||
|
|||||||
Reference in New Issue
Block a user