Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 270926cc96 | |||
| e018837a1d | |||
| 705f160912 | |||
| 7a5e9d2d8a | |||
| eb4e5ea83f | |||
| b528785249 | |||
| a53ba7c827 | |||
| 71f150d1b6 |
@@ -112,7 +112,7 @@ SMTP_TIMEOUT_S=30
|
|||||||
# --- Версия инстанса (релиз v0.0.1) ---
|
# --- Версия инстанса (релиз v0.0.1) ---
|
||||||
# install.sh копирует значение из корневого файла VERSION при каждой
|
# install.sh копирует значение из корневого файла VERSION при каждой
|
||||||
# установке/обновлении — руками менять не нужно.
|
# установке/обновлении — руками менять не нужно.
|
||||||
VIDCONF_VERSION=0.0.12
|
VIDCONF_VERSION=0.0.14
|
||||||
|
|
||||||
# --- Профили compose. Дефолт ниже (`media,monitoring`) — только для ручного
|
# --- Профили compose. Дефолт ниже (`media,monitoring`) — только для ручного
|
||||||
# `docker compose up` БЕЗ install.sh: медиа (LiveKit+coturn) + мониторинг,
|
# `docker compose up` БЕЗ install.sh: медиа (LiveKit+coturn) + мониторинг,
|
||||||
|
|||||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -3,6 +3,63 @@
|
|||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
||||||
проект придерживается [семантического версионирования](https://semver.org/lang/ru/).
|
проект придерживается [семантического версионирования](https://semver.org/lang/ru/).
|
||||||
|
|
||||||
|
## [0.0.14] — 2026-07-28
|
||||||
|
|
||||||
|
TURN-фолбэк для участников из сетей с жёстким NAT.
|
||||||
|
|
||||||
|
### Исправлено
|
||||||
|
- LiveKit теперь анонсирует клиентам внешний TURN-сервер (`rtc.turn_servers`
|
||||||
|
в конфигурации, UDP и TCP на 3478). Раньше coturn поднимался и был healthy,
|
||||||
|
но клиенты о нём не знали: внешний TURN в конфиге объявлен не был, встроенный
|
||||||
|
выключен, а фронтенд `iceServers` не задаёт. За всё время работы в логах
|
||||||
|
coturn не было ни одной аллокации — то есть relay не использовался никогда,
|
||||||
|
и участники из сетей, где прямое UDP-соединение не проходит, теряли связь
|
||||||
|
(`PEER_CONNECTION_DISCONNECTED`). На нагрузочном тесте 28.07 все такие
|
||||||
|
разрывы пришлись на внешних участников и ни одного — на офисных.
|
||||||
|
- Credentials TURN генерируются по механизму TURN REST API из общего
|
||||||
|
`TURN_STATIC_AUTH_SECRET`, то есть тот же секрет, что и у coturn.
|
||||||
|
|
||||||
|
### Изменено
|
||||||
|
- `deploy/render-templates.sh` подставляет `TURN_EXTERNAL_IP` и
|
||||||
|
`TURN_STATIC_AUTH_SECRET` также в конфигурацию LiveKit.
|
||||||
|
- Раздел 8 руководства по развёртыванию переписан: TURN больше не «план на
|
||||||
|
будущее», а рабочая конфигурация. Отдельно описано правило ufw для
|
||||||
|
relay-диапазона `49160:49200/udp` — без него TURN отвечает на запросы, но
|
||||||
|
релей не работает, причём в логах coturn при этом тишина.
|
||||||
|
|
||||||
|
TURN over TLS (5349/443) по-прежнему не настроен: требует монтирования
|
||||||
|
сертификата в coturn. Неработающий `turns:` намеренно не анонсируется, иначе
|
||||||
|
клиент ждал бы таймаута перед переходом к рабочему кандидату.
|
||||||
|
|
||||||
|
## [0.0.13] — 2026-07-28
|
||||||
|
|
||||||
|
Снижение нагрузки на сеть: клиент перестаёт получать полное качество всех
|
||||||
|
чужих камер независимо от того, какого размера плитка на экране.
|
||||||
|
|
||||||
|
### Изменено
|
||||||
|
- Включены `adaptiveStream` и `dynacast` в опциях комнаты. Оба флага в LiveKit
|
||||||
|
выключены по умолчанию, из-за чего каждый участник был подписан на полное
|
||||||
|
качество всех чужих треков, а каждый паблишер слал все слои симулкаста, даже
|
||||||
|
когда их никто не смотрит. Теперь качество подписки выбирается по фактическому
|
||||||
|
размеру плитки, а неотрисованные треки уходят в паузу. На локальном стенде
|
||||||
|
(9 участников, паблишеры 720p) входящий поток одного клиента упал с
|
||||||
|
11 110 до 778 кбит/с.
|
||||||
|
|
||||||
|
Вместе с этим начинает экономить уже написанный код, который до сих пор не
|
||||||
|
давал выигрыша: «скрыть остальных» не рендерит карусель (исходящий трафик
|
||||||
|
LiveKit 0.76 → 0.03 Мбит/с), пагинация сетки участников рендерит только
|
||||||
|
текущую страницу, а пауза чужого видео в свёрнутой вкладке работает лишь
|
||||||
|
при включённом `adaptiveStream`.
|
||||||
|
- Контейнеры больше не пересобирают окружение Python при запуске: во все
|
||||||
|
вызовы `uv run` в прод-путях (CMD образа, `command`/`entrypoint`/`healthcheck`
|
||||||
|
сервисов, миграции и seed в `install.sh`) добавлен `--no-sync`. Раньше
|
||||||
|
окружение, собранное на этапе build с `--no-dev`, при каждом старте
|
||||||
|
синхронизировалось заново и подтягивало dev-группу: ~26 МБ загрузок,
|
||||||
|
замедленный старт и ruff/mypy/pytest в рантайме. Healthcheck'и делали то же
|
||||||
|
самое каждые 15 секунд всю жизнь контейнера. Размер `/app/.venv` после
|
||||||
|
запуска: 574 → 456 МБ. Локальная разработка не затронута — там dev-группа
|
||||||
|
нужна и ставится как раньше.
|
||||||
|
|
||||||
## [0.0.12] — 2026-07-28
|
## [0.0.12] — 2026-07-28
|
||||||
|
|
||||||
Разблокировка backend под нагрузкой: вход в конференцию перестаёт отваливаться,
|
Разблокировка backend под нагрузкой: вход в конференцию перестаёт отваливаться,
|
||||||
|
|||||||
@@ -45,5 +45,12 @@ HEALTHCHECK --interval=10s --timeout=5s --retries=10 --start-period=15s \
|
|||||||
#
|
#
|
||||||
# `sh -c` нужен ради подстановки переменной (exec-форма её не делает),
|
# `sh -c` нужен ради подстановки переменной (exec-форма её не делает),
|
||||||
# `exec` — чтобы uvicorn получил PID 1 и корректно принимал SIGTERM.
|
# `exec` — чтобы uvicorn получил PID 1 и корректно принимал SIGTERM.
|
||||||
|
#
|
||||||
|
# `--no-sync`: окружение уже собрано выше (`uv sync --frozen --no-dev`), и
|
||||||
|
# пересобирать его в рантайме незачем. Без флага `uv run` перед каждым
|
||||||
|
# запуском заново синхронизирует venv И ПОДТЯГИВАЕТ dev-группу (ruff, mypy,
|
||||||
|
# pytest — ~30 МБ загрузок на каждый старт контейнера, dev-инструменты в
|
||||||
|
# проде и раздутый venv). То же касается healthcheck'ов в
|
||||||
|
# deploy/docker-compose.yml, которые дёргают `uv run` каждые 15 секунд.
|
||||||
CMD ["sh", "-c", \
|
CMD ["sh", "-c", \
|
||||||
"exec uv run uvicorn main:create_app --factory --host 0.0.0.0 --port 8000 --workers ${UVICORN_WORKERS:-2}"]
|
"exec uv run --no-sync uvicorn main:create_app --factory --host 0.0.0.0 --port 8000 --workers ${UVICORN_WORKERS:-2}"]
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ services:
|
|||||||
MEDIA_ROOT: ${MEDIA_ROOT:-/app/media}
|
MEDIA_ROOT: ${MEDIA_ROOT:-/app/media}
|
||||||
# Версия инстанса (релиз v0.0.1) — install.sh копирует значение
|
# Версия инстанса (релиз v0.0.1) — install.sh копирует значение
|
||||||
# из файла VERSION (корень репозитория) в .env; отдаётся в GET /api/health.
|
# из файла VERSION (корень репозитория) в .env; отдаётся в GET /api/health.
|
||||||
VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.12}
|
VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.14}
|
||||||
# Число процессов uvicorn (см. backend/Dockerfile). Дефолт 2 рассчитан
|
# Число процессов uvicorn (см. backend/Dockerfile). Дефолт 2 рассчитан
|
||||||
# на 4-ядерный сервер, где ядра делятся с LiveKit. Поднимая значение,
|
# на 4-ядерный сервер, где ядра делятся с LiveKit. Поднимая значение,
|
||||||
# проверьте бюджет соединений с БД: каждый воркер держит свой пул
|
# проверьте бюджет соединений с БД: каждый воркер держит свой пул
|
||||||
@@ -125,7 +125,13 @@ services:
|
|||||||
context: ../backend
|
context: ../backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["uv", "run", "celery", "-A", "workers.celery_app", "worker", "-B",
|
# `--no-sync` во ВСЕХ вызовах `uv run` в этом файле: окружение собрано на
|
||||||
|
# этапе build образа (`uv sync --frozen --no-dev`, backend/Dockerfile), а
|
||||||
|
# без флага `uv run` синхронизирует venv заново при каждом запуске — и
|
||||||
|
# тянет dev-группу (ruff, mypy, pytest), которой в проде делать нечего.
|
||||||
|
# Для healthcheck'ов это особенно дорого: они дёргаются каждые 15 секунд
|
||||||
|
# всю жизнь контейнера.
|
||||||
|
command: ["uv", "run", "--no-sync", "celery", "-A", "workers.celery_app", "worker", "-B",
|
||||||
"-Q", "celery,summarize,notify", "--loglevel=info"]
|
"-Q", "celery,summarize,notify", "--loglevel=info"]
|
||||||
env_file:
|
env_file:
|
||||||
- ../.env
|
- ../.env
|
||||||
@@ -152,7 +158,7 @@ services:
|
|||||||
# нет HTTP-сервера на 8000. Проверяем воркер через `celery ... inspect
|
# нет HTTP-сервера на 8000. Проверяем воркер через `celery ... inspect
|
||||||
# ping`, как рекомендует документация Celery.
|
# ping`, как рекомендует документация Celery.
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "uv", "run", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5"]
|
test: ["CMD", "uv", "run", "--no-sync", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -186,7 +192,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../backend
|
context: ../backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
entrypoint: ["uv", "run", "python", "/download-model.py"]
|
entrypoint: ["uv", "run", "--no-sync", "python", "/download-model.py"]
|
||||||
environment:
|
environment:
|
||||||
WHISPER_MODEL: ${WHISPER_MODEL:-small}
|
WHISPER_MODEL: ${WHISPER_MODEL:-small}
|
||||||
WHISPER_MODELS_ROOT: /models/whisper
|
WHISPER_MODELS_ROOT: /models/whisper
|
||||||
@@ -221,7 +227,7 @@ services:
|
|||||||
# `worker`, и `celery inspect ping` без `--destination` опросит ВЕСЬ
|
# `worker`, и `celery inspect ping` без `--destination` опросит ВЕСЬ
|
||||||
# кластер — упавший worker-transcriber остался бы "healthy", потому что
|
# кластер — упавший worker-transcriber остался бы "healthy", потому что
|
||||||
# ответил бы базовый worker.
|
# ответил бы базовый worker.
|
||||||
command: ["uv", "run", "celery", "-A", "workers.celery_app", "worker",
|
command: ["uv", "run", "--no-sync", "celery", "-A", "workers.celery_app", "worker",
|
||||||
"-Q", "transcription", "--pool=solo", "--concurrency=1",
|
"-Q", "transcription", "--pool=solo", "--concurrency=1",
|
||||||
"--hostname=worker-transcriber@localhost", "--loglevel=info"]
|
"--hostname=worker-transcriber@localhost", "--loglevel=info"]
|
||||||
env_file:
|
env_file:
|
||||||
@@ -252,7 +258,7 @@ services:
|
|||||||
# HTTP-эндпоинта нет — пинг celery, но именно этого узла (см. --hostname
|
# HTTP-эндпоинта нет — пинг celery, но именно этого узла (см. --hostname
|
||||||
# в command выше), а не первого ответившего в общем кластере.
|
# в command выше), а не первого ответившего в общем кластере.
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "uv", "run", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5", "--destination", "worker-transcriber@localhost"]
|
test: ["CMD", "uv", "run", "--no-sync", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5", "--destination", "worker-transcriber@localhost"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -277,7 +283,7 @@ services:
|
|||||||
args:
|
args:
|
||||||
WITH_GPU_EXTRA: "true"
|
WITH_GPU_EXTRA: "true"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["uv", "run", "celery", "-A", "workers.celery_app", "worker",
|
command: ["uv", "run", "--no-sync", "celery", "-A", "workers.celery_app", "worker",
|
||||||
"-Q", "transcription", "--pool=solo", "--concurrency=1",
|
"-Q", "transcription", "--pool=solo", "--concurrency=1",
|
||||||
"--hostname=worker-transcriber-gpu@localhost", "--loglevel=info"]
|
"--hostname=worker-transcriber-gpu@localhost", "--loglevel=info"]
|
||||||
env_file:
|
env_file:
|
||||||
@@ -310,7 +316,7 @@ services:
|
|||||||
whisper-model-init:
|
whisper-model-init:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "uv", "run", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5", "--destination", "worker-transcriber-gpu@localhost"]
|
test: ["CMD", "uv", "run", "--no-sync", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5", "--destination", "worker-transcriber-gpu@localhost"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -662,6 +668,12 @@ services:
|
|||||||
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
- ./monitoring/alerts.yml:/etc/prometheus/alerts.yml:ro
|
- ./monitoring/alerts.yml:/etc/prometheus/alerts.yml:ro
|
||||||
- prometheus_data:/prometheus
|
- prometheus_data:/prometheus
|
||||||
|
# node-exporter живёт в host-сети (см. комментарий у него) и по имени
|
||||||
|
# сервиса в docker-сети больше не резолвится. `host-gateway` — штатный
|
||||||
|
# способ дать контейнеру адрес хоста, не завязываясь на конкретный IP
|
||||||
|
# docker-моста.
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
# Loopback-only: админ-доступ по ssh-туннелю, наружу не публикуется.
|
# Loopback-only: админ-доступ по ssh-туннелю, наружу не публикуется.
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:9090:9090"
|
- "127.0.0.1:9090:9090"
|
||||||
@@ -711,13 +723,27 @@ services:
|
|||||||
|
|
||||||
node-exporter:
|
node-exporter:
|
||||||
# Метрики железа хоста (CPU, память, диск, сеть, load average) — то,
|
# Метрики железа хоста (CPU, память, диск, сеть, load average) — то,
|
||||||
# чего нет ни в одном из приложенческих экспортеров выше. Без
|
# чего нет ни в одном из приложенческих экспортеров выше.
|
||||||
# `network_mode: host` (не нужен: читаем /proc,/sys,/ хоста через
|
#
|
||||||
# bind-mount, а Prometheus достаёт их по имени сервиса во внутренней
|
# `network_mode: host` ОБЯЗАТЕЛЕН, и вот почему (проверено 2026-07-28,
|
||||||
# сети compose — так безопаснее, не расширяет сетевой доступ контейнера).
|
# до этого экспортер работал в bridge-сети и отдавал неверные данные).
|
||||||
|
# Bind-mount'а `/proc` достаточно для CPU, памяти и диска, но НЕ для сети:
|
||||||
|
# `/proc/net` — это симлинк на `self/net`, который резолвится в сетевом
|
||||||
|
# namespace ЧИТАЮЩЕГО процесса. В bridge-сети экспортер видел собственные
|
||||||
|
# `lo` и `eth0` (56 МБ трафика) вместо хостового `enp3s0` (39.8 ГБ), то
|
||||||
|
# есть `node_network_*` показывал трафик контейнера, а не сервера. При
|
||||||
|
# разборе нагрузочного теста 28.07 сетевых метрик хоста не оказалось
|
||||||
|
# вовсе — см. .forcc/LOAD-FINDINGS.md.
|
||||||
|
#
|
||||||
|
# Порт 9100 при этом слушается на хосте. Наружу он не торчит: ufw
|
||||||
|
# пропускает только 22/80/443/3478/7881/51820 и UDP-диапазон LiveKit
|
||||||
|
# (проверено `ufw status`). Prometheus обращается к нему через
|
||||||
|
# `host.docker.internal` (см. `extra_hosts` у сервиса prometheus и
|
||||||
|
# таргет `node` в deploy/monitoring/prometheus.yml).
|
||||||
image: prom/node-exporter:v1.8.2
|
image: prom/node-exporter:v1.8.2
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
pid: host
|
pid: host
|
||||||
|
network_mode: host
|
||||||
volumes:
|
volumes:
|
||||||
- /proc:/host/proc:ro
|
- /proc:/host/proc:ro
|
||||||
- /sys:/host/sys:ro
|
- /sys:/host/sys:ro
|
||||||
@@ -727,9 +753,8 @@ services:
|
|||||||
- '--path.sysfs=/host/sys'
|
- '--path.sysfs=/host/sys'
|
||||||
- '--path.rootfs=/rootfs'
|
- '--path.rootfs=/rootfs'
|
||||||
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
|
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
|
||||||
# Не публикуем порт наружу вообще (не 127.0.0.1:9100, а совсем без
|
# Секции `ports` нет и с host-сетью быть не может: контейнер слушает
|
||||||
# ports) — Prometheus ходит к нему по внутренней сети compose
|
# прямо на интерфейсах хоста. От внешнего мира порт закрывает ufw.
|
||||||
# (`node-exporter:9100`), публикация на хост для этого не нужна.
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9100/metrics >/dev/null || exit 1"]
|
test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9100/metrics >/dev/null || exit 1"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|||||||
@@ -32,6 +32,40 @@ rtc:
|
|||||||
use_external_ip: ${LIVEKIT_USE_EXTERNAL_IP}
|
use_external_ip: ${LIVEKIT_USE_EXTERNAL_IP}
|
||||||
node_ip: ${LIVEKIT_NODE_IP}
|
node_ip: ${LIVEKIT_NODE_IP}
|
||||||
|
|
||||||
|
# Внешний TURN (сервис coturn, профиль `media`) — АНОНС КЛИЕНТАМ.
|
||||||
|
# Сам SFU через эти серверы не ходит: LiveKit лишь отдаёт их браузеру в
|
||||||
|
# списке ICE-серверов при подключении (см. iceServersForParticipant в
|
||||||
|
# pkg/service/roommanager.go), а клиент уже решает, нужен ли ему relay.
|
||||||
|
#
|
||||||
|
# Зачем. До 28.07.2026 coturn работал, но КЛИЕНТЫ О НЁМ НЕ ЗНАЛИ: секция
|
||||||
|
# `turn` ниже выключена (встроенный TURN не поднимаем), внешний в конфиге
|
||||||
|
# объявлен не был, а фронтенд `iceServers` не задаёт. За всё время работы
|
||||||
|
# в логах coturn — ноль ALLOCATE. Итог: у клиентов из сетей с жёстким NAT
|
||||||
|
# не было relay-фолбэка вообще, только прямой UDP и TCP 7881. Именно так
|
||||||
|
# объясняются `PEER_CONNECTION_DISCONNECTED` на нагрузочном тесте — все
|
||||||
|
# у внешних участников, ни одного у офисных (.forcc/LOAD-FINDINGS.md,
|
||||||
|
# причина C).
|
||||||
|
#
|
||||||
|
# `secret` обязан совпадать с `static-auth-secret` в turnserver.conf —
|
||||||
|
# оба рендерятся из одного TURN_STATIC_AUTH_SECRET (deploy/render-templates.sh).
|
||||||
|
# Логин/пароль LiveKit генерирует сам по механизму TURN REST API.
|
||||||
|
#
|
||||||
|
# UDP и TCP на 3478 — оба порта уже открыты в ufw. TLS (5349) намеренно не
|
||||||
|
# объявляем: в turnserver.conf сертификаты не смонтированы, и анонс
|
||||||
|
# неработающего `turns:` заставил бы клиента впустую ждать таймаута,
|
||||||
|
# прежде чем перейти к рабочему кандидату.
|
||||||
|
turn_servers:
|
||||||
|
- host: ${TURN_EXTERNAL_IP}
|
||||||
|
port: 3478
|
||||||
|
protocol: udp
|
||||||
|
secret: ${TURN_STATIC_AUTH_SECRET}
|
||||||
|
ttl: 14400
|
||||||
|
- host: ${TURN_EXTERNAL_IP}
|
||||||
|
port: 3478
|
||||||
|
protocol: tcp
|
||||||
|
secret: ${TURN_STATIC_AUTH_SECRET}
|
||||||
|
ttl: 14400
|
||||||
|
|
||||||
# Redis обязателен для сервиса egress (см. deploy/egress/) — он использует
|
# Redis обязателен для сервиса egress (см. deploy/egress/) — он использует
|
||||||
# его как pub/sub и key-value хранилище состояния запущенных записей;
|
# его как pub/sub и key-value хранилище состояния запущенных записей;
|
||||||
# без него egress не может получать room/track-события от LiveKit
|
# без него egress не может получать room/track-события от LiveKit
|
||||||
|
|||||||
@@ -38,9 +38,16 @@ scrape_configs:
|
|||||||
# — сервис node-exporter). Единственный источник, который покажет
|
# — сервис node-exporter). Единственный источник, который покажет
|
||||||
# нехватку памяти/CPU на сервере, если она не проявится как рост
|
# нехватку памяти/CPU на сервере, если она не проявится как рост
|
||||||
# латентности API (см. дашборд host.json).
|
# латентности API (см. дашборд host.json).
|
||||||
|
#
|
||||||
|
# Адрес `host.docker.internal`, а не `node-exporter:9100`: с 2026-07-28
|
||||||
|
# экспортер работает в host-сети и по имени сервиса в docker-сети не
|
||||||
|
# резолвится. Причина перевода — сетевые метрики: `/proc/net` это симлинк
|
||||||
|
# на `self/net`, поэтому в bridge-сети экспортер отдавал трафик
|
||||||
|
# собственного `eth0` вместо хостового `enp3s0`. Имя резолвится через
|
||||||
|
# `extra_hosts: host-gateway` у сервиса prometheus (deploy/docker-compose.yml).
|
||||||
- job_name: node
|
- job_name: node
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["node-exporter:9100"]
|
- targets: ["host.docker.internal:9100"]
|
||||||
|
|
||||||
# Метрики по каждому контейнеру (CPU/память/сеть отдельно у backend,
|
# Метрики по каждому контейнеру (CPU/память/сеть отдельно у backend,
|
||||||
# worker, postgres и т.д. — профиль monitoring, сервис
|
# worker, postgres и т.д. — профиль monitoring, сервис
|
||||||
@@ -53,6 +60,32 @@ scrape_configs:
|
|||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["container-exporter:9419"]
|
- targets: ["container-exporter:9419"]
|
||||||
|
|
||||||
|
# LiveKit SFU (профиль `media`). Порт объявлен в самом LiveKit —
|
||||||
|
# `deploy/livekit/livekit.yaml.template`, секция `prometheus: port: 6789`;
|
||||||
|
# наружу он не публикуется, скрейп идёт по имени сервиса внутри docker-сети.
|
||||||
|
#
|
||||||
|
# Почему это важно отдельно от `container-exporter`: тот показывает CPU,
|
||||||
|
# память и суммарный трафик контейнера, но ничего не знает о том, ЧТО внутри
|
||||||
|
# этого трафика. Разбор нагрузочного теста 28.07.2026 пришлось делать по
|
||||||
|
# логам именно потому, что job'а здесь не было (см. .forcc/LOAD-FINDINGS.md).
|
||||||
|
#
|
||||||
|
# Ключевое, что отсюда появляется:
|
||||||
|
# livekit_track_subscribed_total / livekit_track_published_total —
|
||||||
|
# подписки против публикаций, то есть прямой эффект adaptiveStream;
|
||||||
|
# livekit_participant_total, livekit_room_total — нагрузка в участниках;
|
||||||
|
# livekit_quality_score, livekit_packet_loss_percent, livekit_rtt_ms,
|
||||||
|
# livekit_jitter_us, livekit_nack_total, livekit_pli_total — качество
|
||||||
|
# связи у клиентов, а не догадки по событиям congestion в логах;
|
||||||
|
# livekit_webhook_queue_length, livekit_webhook_dispatch_total — очередь
|
||||||
|
# доставки вебхуков в backend (на тесте она росла до 56 секунд).
|
||||||
|
#
|
||||||
|
# Профиль `media` входит в дефолтный набор COMPOSE_PROFILES (см. .env.example),
|
||||||
|
# поэтому job включён, а не закомментирован, как `llm`. На инсталляции без
|
||||||
|
# профиля `media` таргет не резолвится — закомментируйте секцию.
|
||||||
|
- job_name: livekit
|
||||||
|
static_configs:
|
||||||
|
- targets: ["livekit:6789"]
|
||||||
|
|
||||||
# Локальный LLM-сервер (llama.cpp, LLAMA_ARG_ENDPOINT_METRICS=1). Адрес
|
# Локальный LLM-сервер (llama.cpp, LLAMA_ARG_ENDPOINT_METRICS=1). Адрес
|
||||||
# `llm:8080` разрешается ОДНИМ из двух compose-сервисов в зависимости от
|
# `llm:8080` разрешается ОДНИМ из двух compose-сервисов в зависимости от
|
||||||
# выбранного при установке пресета — `llm` (CPU, профиль `llm`, уровни
|
# выбранного при установке пресета — `llm` (CPU, профиль `llm`, уровни
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ envsubst '${TURN_STATIC_AUTH_SECRET} ${TURN_REALM} ${TURN_EXTERNAL_IP}' \
|
|||||||
< "$SCRIPT_DIR/coturn/turnserver.conf.template" > "$SCRIPT_DIR/coturn/turnserver.conf"
|
< "$SCRIPT_DIR/coturn/turnserver.conf.template" > "$SCRIPT_DIR/coturn/turnserver.conf"
|
||||||
echo "[render] deploy/coturn/turnserver.conf готов"
|
echo "[render] deploy/coturn/turnserver.conf готов"
|
||||||
|
|
||||||
envsubst '${LIVEKIT_USE_EXTERNAL_IP} ${LIVEKIT_NODE_IP} ${LIVEKIT_API_KEY} ${REDIS_PASSWORD}' \
|
# TURN_EXTERNAL_IP и TURN_STATIC_AUTH_SECRET нужны и здесь: с 0.0.14 LiveKit
|
||||||
|
# анонсирует клиентам внешний coturn (секция `rtc.turn_servers`), и секрет
|
||||||
|
# обязан совпадать с `static-auth-secret` в turnserver.conf выше.
|
||||||
|
envsubst '${LIVEKIT_USE_EXTERNAL_IP} ${LIVEKIT_NODE_IP} ${LIVEKIT_API_KEY} ${REDIS_PASSWORD} ${TURN_EXTERNAL_IP} ${TURN_STATIC_AUTH_SECRET}' \
|
||||||
< "$SCRIPT_DIR/livekit/livekit.yaml.template" > "$SCRIPT_DIR/livekit/livekit.yaml"
|
< "$SCRIPT_DIR/livekit/livekit.yaml.template" > "$SCRIPT_DIR/livekit/livekit.yaml"
|
||||||
echo "[render] deploy/livekit/livekit.yaml готов"
|
echo "[render] deploy/livekit/livekit.yaml готов"
|
||||||
|
|
||||||
|
|||||||
@@ -82,9 +82,25 @@ ufw allow 80/tcp # HTTP (редирект на HTTPS + ACME-challenge)
|
|||||||
ufw allow 443/tcp # HTTPS
|
ufw allow 443/tcp # HTTPS
|
||||||
ufw allow 7881/tcp # LiveKit RTC TCP fallback (профиль media)
|
ufw allow 7881/tcp # LiveKit RTC TCP fallback (профиль media)
|
||||||
ufw allow 54000:54100/udp # LiveKit WebRTC media (ICE), см. docker-compose.yml
|
ufw allow 54000:54100/udp # LiveKit WebRTC media (ICE), см. docker-compose.yml
|
||||||
# TURN (coturn) — только если включаете раздел 8:
|
# TURN (coturn) — только если включаете раздел 8. Нужны ОБА пункта:
|
||||||
|
# сигнальные порты И диапазон relay-аллокаций (min-port/max-port из
|
||||||
|
# deploy/coturn/turnserver.conf). Без второго TURN отвечает на запросы, но
|
||||||
|
# сам релей не работает — клиент получает кандидата и не может им
|
||||||
|
# воспользоваться, а в логах coturn при этом тишина.
|
||||||
# ufw allow 3478/tcp
|
# ufw allow 3478/tcp
|
||||||
# ufw allow 3478/udp
|
# ufw allow 3478/udp
|
||||||
|
# ufw allow 49160:49200/udp
|
||||||
|
|
||||||
|
# Мониторинг (профиль `monitoring`): node-exporter работает в host-сети —
|
||||||
|
# иначе он отдаёт сетевые метрики собственного контейнера вместо метрик
|
||||||
|
# сервера (`/proc/net` — симлинк на `self/net`, bind-mount `/proc` этого не
|
||||||
|
# обходит; см. комментарий у сервиса в deploy/docker-compose.yml). Порт
|
||||||
|
# слушается на хосте, поэтому Prometheus в docker-сети упирается в
|
||||||
|
# политику ufw по умолчанию. Правило разрешает скрейп ТОЛЬКО из внутренних
|
||||||
|
# docker-подсетей — снаружи 9100 остаётся закрыт (172.16.0.0/12 не
|
||||||
|
# маршрутизируется в интернете):
|
||||||
|
ufw allow from 172.16.0.0/12 to any port 9100 proto tcp comment 'node-exporter: скрейп Prometheus из docker-сети'
|
||||||
|
|
||||||
ufw enable
|
ufw enable
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -309,29 +325,48 @@ firewall) хватает для подавляющего большинства
|
|||||||
(характерный симптом — конференция подключается по signaling, `connection
|
(характерный симптом — конференция подключается по signaling, `connection
|
||||||
state: connected`, но собеседник не видит видео/не слышит звук).
|
state: connected`, но собеседник не видит видео/не слышит звук).
|
||||||
|
|
||||||
По умолчанию `deploy/livekit/livekit.yaml.template` содержит
|
**С версии 0.0.14 LiveKit анонсирует coturn клиентам** — секция
|
||||||
`turn.enabled: false`, и `rtc.turn_servers` не задан — standalone coturn
|
`rtc.turn_servers` в `deploy/livekit/livekit.yaml.template` (UDP и TCP на
|
||||||
поднимается (профиль `media`), но LiveKit не раздаёт его клиентам как
|
3478, credentials по механизму TURN REST API из общего
|
||||||
ICE-фолбэк.
|
`TURN_STATIC_AUTH_SECRET`). Встроенный TURN LiveKit при этом остаётся
|
||||||
|
выключенным (`turn.enabled: false`), чтобы не поднимать два TURN-сервера.
|
||||||
|
|
||||||
Включение (правки шаблона `deploy/livekit/livekit.yaml.template` +
|
⚠️ **Чем это было до 0.0.14, если вы обновляетесь со старой версии.** coturn
|
||||||
редеплой; **код-фикс не входит в это руководство без запроса** — обсудите
|
поднимался и был healthy, но клиенты о нём не знали: в конфиге LiveKit
|
||||||
с командой перед изменением):
|
внешний TURN объявлен не был, а фронтенд `iceServers` не задаёт. За всё
|
||||||
|
время работы в логах coturn не было ни одного ALLOCATE — то есть relay не
|
||||||
|
использовался никогда, и участники из сетей с жёстким NAT просто теряли
|
||||||
|
соединение (`PEER_CONNECTION_DISCONNECTED`).
|
||||||
|
|
||||||
1. Открыть 443 для TURN/TLS (наиболее надёжный фолбэк — TURN через тот же
|
Что нужно проверить на своей инсталляции:
|
||||||
порт, что и остальной HTTPS-трафик, редко блокируется firewall'ами):
|
|
||||||
потребует отдельного TLS-сертификата для coturn (`cert-file`/`pkey-file`
|
1. **Порты в ufw — оба пункта** (см. шаг 1): `3478/tcp` + `3478/udp` для
|
||||||
в `deploy/coturn/turnserver.conf.template`) — можно переиспользовать тот
|
сигнализации и `49160:49200/udp` для relay-аллокаций. Диапазон должен
|
||||||
же Let's Encrypt сертификат, что и nginx (тот же `/etc/letsencrypt`, уже
|
совпадать с `min-port`/`max-port` в
|
||||||
смонтированный в nginx — coturn сейчас его не монтирует, потребуется
|
`deploy/coturn/turnserver.conf.template`. Без него TURN отвечает на
|
||||||
доп. volume).
|
запросы, но релей не работает — самый неприятный вариант, потому что в
|
||||||
2. В `livekit.yaml.template` включить `turn.enabled: true` и/или явно
|
логах coturn при этом тишина.
|
||||||
прописать `rtc.turn_servers` со статическими credentials
|
2. **`TURN_EXTERNAL_IP` в `.env`** — реальный внешний IP или домен сервера.
|
||||||
(`TURN_STATIC_AUTH_SECRET` уже есть в `.env`).
|
Именно это значение уезжает клиентам как адрес TURN-сервера, поэтому
|
||||||
3. `ufw allow 3478/tcp` + `ufw allow 3478/udp` (шаг 1, закомментированные
|
`127.0.0.1` из dev-дефолта сделает анонс бесполезным.
|
||||||
строки) — сейчас coturn поднят, но порт не проверялся как обязательный.
|
3. После правок — `./deploy/render-templates.sh` (перерендерит конфиги из
|
||||||
4. Передеплой (`docker compose ... up -d --force-recreate livekit coturn`)
|
шаблонов), затем `docker compose ... up -d --force-recreate livekit`.
|
||||||
и повторный кросс-сетевой тест именно с проблемной сетью.
|
⚠️ Перезапуск LiveKit **разрывает все активные конференции** — выбирайте
|
||||||
|
окно.
|
||||||
|
4. Проверка, что релей заработал: провести звонок из проблемной сети и
|
||||||
|
убедиться, что в логах появились аллокации:
|
||||||
|
`docker logs vidconf-coturn-1 --since 10m 2>&1 | grep -ci allocate`.
|
||||||
|
Ноль при живом звонке из-за NAT означает, что до coturn не дошли —
|
||||||
|
смотрите ufw и `TURN_EXTERNAL_IP`.
|
||||||
|
|
||||||
|
**TURN over TLS (порт 5349 или 443) — не настроен.** Это самый надёжный
|
||||||
|
фолбэк (проходит там, где режут UDP и нестандартные порты), но требует
|
||||||
|
смонтировать в coturn TLS-сертификат: раскомментировать `cert`/`pkey` в
|
||||||
|
`deploy/coturn/turnserver.conf.template`, добавить volume с
|
||||||
|
`/etc/letsencrypt` (nginx его уже монтирует, coturn — нет), открыть порт и
|
||||||
|
не забыть про перезапуск coturn при обновлении сертификата. Пока этого нет,
|
||||||
|
`turns:` намеренно не анонсируется: анонс неработающего адреса заставил бы
|
||||||
|
клиента ждать таймаута перед переходом к рабочему кандидату.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -389,7 +424,7 @@ docker exec vidconf-postgres-1 pg_dump -U vidconf vidconf | gzip > db-$(date +%F
|
|||||||
```bash
|
```bash
|
||||||
gunzip -c db-2026-07-25.sql.gz | docker exec -i vidconf-postgres-1 psql -U vidconf vidconf
|
gunzip -c db-2026-07-25.sql.gz | docker exec -i vidconf-postgres-1 psql -U vidconf vidconf
|
||||||
# Догнать миграции, если бэкап снят на более старой версии кода:
|
# Догнать миграции, если бэкап снят на более старой версии кода:
|
||||||
docker compose -f deploy/docker-compose.yml --env-file .env run --rm backend uv run alembic upgrade head
|
docker compose -f deploy/docker-compose.yml --env-file .env run --rm backend uv run --no-sync alembic upgrade head
|
||||||
docker compose -f deploy/docker-compose.yml --env-file .env restart backend worker
|
docker compose -f deploy/docker-compose.yml --env-file .env restart backend worker
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ CPU-only — отдельного GPU-варианта профилей для
|
|||||||
|
|
||||||
Поведение:
|
Поведение:
|
||||||
- **Дефолт (Enter):** применяет матрицу выбранного пресета к настройкам БД
|
- **Дефолт (Enter):** применяет матрицу выбранного пресета к настройкам БД
|
||||||
(defs: `docker compose exec -T backend uv run python -m scripts.apply_preset_settings --force`)
|
(defs: `docker compose exec -T backend uv run --no-sync python -m scripts.apply_preset_settings --force`)
|
||||||
- **Отказ (`n`):** сохраняет ручные правки админа; переменные `BOOTSTRAP_*` в `.env`
|
- **Отказ (`n`):** сохраняет ручные правки админа; переменные `BOOTSTRAP_*` в `.env`
|
||||||
обновляются, но скрипт применения настроек НЕ запускается
|
обновляются, но скрипт применения настроек НЕ запускается
|
||||||
- **Флаг `--yes`:** автоматически применяет пресет без вопроса (для CI/CD)
|
- **Флаг `--yes`:** автоматически применяет пресет без вопроса (для CI/CD)
|
||||||
@@ -111,7 +111,7 @@ Grafana/Prometheus не поднимаются автоматически — с
|
|||||||
собирает фронтенд-SPA и вкомпилирует статику, `frontend/Dockerfile`).
|
собирает фронтенд-SPA и вкомпилирует статику, `frontend/Dockerfile`).
|
||||||
4. Поднимает `postgres`/`redis` (`up -d --wait`) и применяет **до старта
|
4. Поднимает `postgres`/`redis` (`up -d --wait`) и применяет **до старта
|
||||||
backend** миграции и seed одноразовыми контейнерами:
|
backend** миграции и seed одноразовыми контейнерами:
|
||||||
`docker compose run --rm backend uv run alembic upgrade head` +
|
`docker compose run --rm backend uv run --no-sync alembic upgrade head` +
|
||||||
`... python -m scripts.seed`. Порядок критичен: `backend.lifespan`
|
`... python -m scripts.seed`. Порядок критичен: `backend.lifespan`
|
||||||
бутстрапит `instance_settings` при каждом старте приложения, поэтому на
|
бутстрапит `instance_settings` при каждом старте приложения, поэтому на
|
||||||
чистой БД таблицы обязаны существовать до первого запуска backend — иначе
|
чистой БД таблицы обязаны существовать до первого запуска backend — иначе
|
||||||
@@ -122,9 +122,8 @@ Grafana/Prometheus не поднимаются автоматически — с
|
|||||||
настройки инстанса не перетираются).
|
настройки инстанса не перетираются).
|
||||||
5. Поднимает остальной стек: `docker compose <--profile ...> up -d --wait`
|
5. Поднимает остальной стек: `docker compose <--profile ...> up -d --wait`
|
||||||
(backend, worker, nginx с фронтом + сервисы активных профилей). Команда
|
(backend, worker, nginx с фронтом + сервисы активных профилей). Команда
|
||||||
идемпотентна и обёрнута в ретрай (до 3 попыток): первый старт backend/worker
|
идемпотентна и обёрнута в ретрай (до 3 попыток): на слабой/загруженной
|
||||||
включает `uv run` (синхронизация окружения + компиляция байткода), и на
|
машине healthcheck может не успеть за отведённые
|
||||||
слабой/загруженной машине healthcheck может не успеть за отведённые
|
|
||||||
retries — повтор лишь дожидается уже стартующих контейнеров.
|
retries — повтор лишь дожидается уже стартующих контейнеров.
|
||||||
6. Печатает сводку: URL фронтенда/бэкенда, учётные данные администратора,
|
6. Печатает сводку: URL фронтенда/бэкенда, учётные данные администратора,
|
||||||
команда для `--profile monitoring`.
|
команда для `--profile monitoring`.
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# Без -B: beat уже запущен на базовом worker (см. правило выше).
|
# Без -B: beat уже запущен на базовом worker (см. правило выше).
|
||||||
command: ["uv", "run", "celery", "-A", "workers.celery_app", "worker",
|
command: ["uv", "run", "--no-sync", "celery", "-A", "workers.celery_app", "worker",
|
||||||
"-Q", "summarize", "--hostname=worker-summarize-%h@%h", "--loglevel=info"]
|
"-Q", "summarize", "--hostname=worker-summarize-%h@%h", "--loglevel=info"]
|
||||||
env_file:
|
env_file:
|
||||||
- ../.env
|
- ../.env
|
||||||
|
|||||||
@@ -187,6 +187,22 @@ export function RoomPage() {
|
|||||||
const { userChoices } = usePersistentUserChoices()
|
const { userChoices } = usePersistentUserChoices()
|
||||||
const roomOptions = useMemo<RoomOptions>(
|
const roomOptions = useMemo<RoomOptions>(
|
||||||
() => ({
|
() => ({
|
||||||
|
// Оба флага в LiveKit по умолчанию выключены, и без них каждый клиент
|
||||||
|
// подписан на полное качество всех чужих треков независимо от размера
|
||||||
|
// плитки, а каждый паблишер шлёт все слои симулкаста, даже если их никто
|
||||||
|
// не смотрит. На тесте 28.07.2026 (19 участников, ~8 камер) это дало
|
||||||
|
// устойчивые 140–169 Мбит/с исходящего трафика при пике 240, 662 события
|
||||||
|
// `remote bwe: channel congestion detected` и 146 переходов аллокатора
|
||||||
|
// STABLE → DEFICIENT — то есть видимый участникам лаг.
|
||||||
|
//
|
||||||
|
// adaptiveStream: подписка на слой по фактическому размеру плитки на
|
||||||
|
// экране + пауза треков, которые сейчас не отрисованы. Именно на нём
|
||||||
|
// начинает экономить уже написанный код: «скрыть остальных»
|
||||||
|
// (RoomStage) не рендерит карусель, а пагинация StageGrid рендерит
|
||||||
|
// только текущую страницу — неприаттаченные треки считаются невидимыми.
|
||||||
|
// dynacast: паблишер прекращает отдавать слои, на которые нет подписчиков.
|
||||||
|
adaptiveStream: true,
|
||||||
|
dynacast: true,
|
||||||
audioCaptureDefaults: { deviceId: userChoices.audioDeviceId || undefined },
|
audioCaptureDefaults: { deviceId: userChoices.audioDeviceId || undefined },
|
||||||
videoCaptureDefaults: { deviceId: userChoices.videoDeviceId || undefined },
|
videoCaptureDefaults: { deviceId: userChoices.videoDeviceId || undefined },
|
||||||
// Аудиовыход (колонки/наушники/bluetooth) — отдельный персист, не через
|
// Аудиовыход (колонки/наушники/bluetooth) — отдельный персист, не через
|
||||||
|
|||||||
13
install.sh
13
install.sh
@@ -526,13 +526,14 @@ docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --wait postgres r
|
|||||||
# exist" и healthcheck (--wait) никогда не проходит. `compose run` запускает
|
# exist" и healthcheck (--wait) никогда не проходит. `compose run` запускает
|
||||||
# одноразовый контейнер с нужной командой, не поднимая uvicorn/lifespan.
|
# одноразовый контейнер с нужной командой, не поднимая uvicorn/lifespan.
|
||||||
echo "[install] Применяю миграции Alembic и seed (админ/справочники) — до старта backend"
|
echo "[install] Применяю миграции Alembic и seed (админ/справочники) — до старта backend"
|
||||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" run --rm backend uv run alembic upgrade head
|
# `--no-sync`: окружение собрано в образе, повторная синхронизация в рантайме
|
||||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" run --rm backend uv run python -m scripts.seed
|
# только тянула бы dev-группу (см. комментарий в backend/Dockerfile).
|
||||||
|
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" run --rm backend uv run --no-sync alembic upgrade head
|
||||||
|
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" run --rm backend uv run --no-sync python -m scripts.seed
|
||||||
|
|
||||||
echo "[install] docker compose up -d --wait (backend/worker/nginx и остальные сервисы профиля)"
|
echo "[install] docker compose up -d --wait (backend/worker/nginx и остальные сервисы профиля)"
|
||||||
# Первый старт backend/worker включает `uv run` (синхронизация окружения +
|
# На слабой/загруженной машине healthcheck может не успеть пройти за отведённые
|
||||||
# компиляция байткода) — на слабой/загруженной машине healthcheck может не
|
# retries, и `--wait` вернёт "container is
|
||||||
# успеть пройти за отведённые retries, и `--wait` вернёт "container is
|
|
||||||
# unhealthy", хотя сервис через несколько секунд становится healthy. Команда
|
# unhealthy", хотя сервис через несколько секунд становится healthy. Команда
|
||||||
# идемпотентна, поэтому повторяем её несколько раз: повтор лишь дожидается
|
# идемпотентна, поэтому повторяем её несколько раз: повтор лишь дожидается
|
||||||
# уже стартующих контейнеров, ничего не пересоздавая.
|
# уже стартующих контейнеров, ничего не пересоздавая.
|
||||||
@@ -573,7 +574,7 @@ if [ "$FRESH_ENV" != "1" ]; then
|
|||||||
fi
|
fi
|
||||||
if [ "$APPLY_PRESET_SETTINGS" = "1" ]; then
|
if [ "$APPLY_PRESET_SETTINGS" = "1" ]; then
|
||||||
echo "[install] Применяю настройки модулей инстанса под пресет ${PRESET}"
|
echo "[install] Применяю настройки модулей инстанса под пресет ${PRESET}"
|
||||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T backend uv run python -m scripts.apply_preset_settings --force
|
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T backend uv run --no-sync python -m scripts.apply_preset_settings --force
|
||||||
else
|
else
|
||||||
echo "[install] Настройки модулей инстанса НЕ изменены — сохранены ручные правки администратора"
|
echo "[install] Настройки модулей инстанса НЕ изменены — сохранены ручные правки администратора"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user