Диапазон 54000-54100/udp заставлял Docker поднимать по отдельному docker-proxy на каждый порт — весь медиатрафик шёл лишним userland-хопом. rtc.udp_port переключает LiveKit на мультиплексирование ICE-сессий через один порт; TURN и остальные связи (redis/nginx/webhook/prometheus) не задеты. Проверено локально lk load-test — 0% потерь пакетов, ICE во всех сессиях выбирает новый порт.
46 KiB
Развёртывание на боевом сервере — от голой Ubuntu до https://<домен>
Пошаговое воспроизводимое руководство: чистый сервер Ubuntu 24.04 → рабочий VidConf на реальном домене с TLS. Путь проверен вживую (снос + чистый деплой
- полное тестирование стека), детали инцидентов и находок — в разделе «Траблшутинг» ниже.
Для локальной разработки этот документ не нужен — см. docs/deploy/dev-setup.md. Здесь — только боевой сценарий.
Содержание
- Предусловия
- Клонирование репозитория
- Настройка
.env - Первый подъём стека (bootstrap с самоподписанным сертификатом)
- Выпуск SSL и автопродление
- Профиль monitoring
- Проверка после деплоя
- TURN — опционально, для экстремального NAT
- Обновление / редеплой
- Бэкап и restore БД
- Траблшутинг
- Как мониторить сервис
1. Предусловия
DNS
Для КАЖДОГО домена, который вы впишете в NGINX_SERVER_NAMES (см. шаг 3),
должна существовать A-запись, указывающая на публичный IP сервера, ДО выпуска
SSL (шаг 5) — certbot проверяет владение доменом HTTP-запросом на этот IP.
Сервер
- Ubuntu 24.04 (проверено; другие современные дистрибутивы с Docker, вероятно, тоже подойдут, но не проверялись).
- Публичный IP на сетевом интерфейсе (без NAT) — так проверялось
ICE(см.LIVEKIT_NODE_IPв шаге 3). Если сервер за NAT, читайте раздел 8. - Ресурсы — по выбранному пресету инсталлятора (
./install.sh --helpили docs/architecture/adr/004-ai-tier-matrix.md): пресеты 1–2 (без AI) нетребовательны (2 vCPU / 4 ГБ RAM с запасом хватает), пресеты 3–5 — см. таблицу ADR-004 (до 16+ vCPU / 64 ГБ RAM / GPU для max).
Место на диске
Критично проверить заранее — на боевом инстансе с профилями
transcribe+llm (пресеты 3–5) стек реально упирался в 99% занятого диска.
Ориентиры:
| Набор профилей | Место под образы/модели |
|---|---|
media,monitoring (пресеты 1–2, без AI) |
~5 ГБ |
+ transcribe,llm (пресет 3, уровень min) |
ещё ~5–8 ГБ (веса Qwen3.5-4B ~2,8 ГБ + faster-whisper small) |
+ transcribe,llm (пресет 4, medium) |
ещё ~10–15 ГБ (Qwen3.5-9B ~6,2 ГБ) |
+ transcribe-gpu,llm-gpu (пресет 5, max) |
ещё ~25+ ГБ (Qwen3.5-35B-A3B ~20–22 ГБ) |
Плюс место под записи (recordings, если включена транскрибация) и рост БД
со временем. Для пресетов 1–2 достаточно диска от 20 ГБ, для 3–5 — 100/150/250 ГБ
(см. install.sh, таблица пресетов).
Docker + Compose plugin
curl -fsSL https://get.docker.com | sh
# Проверка (нужен именно compose PLUGIN — "docker compose", не отдельный docker-compose):
docker compose version
Firewall (ufw)
Открыть строго то, что реально нужно снаружи — избыточные правила вводят в заблуждение и расширяют поверхность атаки без пользы (см. раздел «Траблшутинг», инцидент с открытым Redis, — конкретно эти порты он не касался, но принцип «минимум наружу» из него и вырос):
ufw allow 22/tcp # SSH — сузьте до вашей сети, если возможно
ufw allow 80/tcp # HTTP (редирект на HTTPS + ACME-challenge)
ufw allow 443/tcp # HTTPS
ufw allow 7881/tcp # LiveKit RTC TCP fallback (профиль media)
ufw allow 54000/udp # LiveKit WebRTC media (ICE, мультиплекс), см. docker-compose.yml
# TURN (coturn) — только если включаете раздел 8. Нужны ОБА пункта:
# сигнальные порты И диапазон relay-аллокаций (min-port/max-port из
# deploy/coturn/turnserver.conf). Без второго TURN отвечает на запросы, но
# сам релей не работает — клиент получает кандидата и не может им
# воспользоваться, а в логах coturn при этом тишина.
# ufw allow 3478/tcp
# 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
НЕ открывайте 7880/tcp (LiveKit signaling — публикуется только на
127.0.0.1, клиенты идут через wss://<домен>/livekit/, т.е. через
80/443), 5432/6379 (Postgres/Redis — тоже только 127.0.0.1), 9090/
3001/8080/8081 (мониторинг/LLM — тоже 127.0.0.1, доступ через
SSH-туннель, см. раздел 12). Все эти сервисы уже публикуются
docker-compose.yml только на loopback — открывать их портом наружу не
нужно и не следует (см. .forcc-заметки внутренних сессий деплоя — именно
так на этом проекте был скомпрометирован открытый в интернет Redis).
2. Клонирование репозитория
git clone <URL вашего репозитория> /opt/vidconf
cd /opt/vidconf
Дальше все команды — из /opt/vidconf, если не указано иное.
3. Настройка .env
cp .env.example .env
chmod 600 .env # секреты внутри — только root может читать
Что install.sh сделает САМ (не трогайте руками)
При первой установке install.sh (шаг 4) сам генерирует случайные секреты
для этих ключей — не прописывайте их вручную, они всё равно будут
перезаписаны при первом запуске (генерация — только если значение пусто,
см. ensure_secret в install.sh):
JWT_SECRET, POSTGRES_PASSWORD, REDIS_PASSWORD, TURN_STATIC_AUTH_SECRET,
LIVEKIT_API_SECRET, GRAFANA_ADMIN_PASSWORD, SEED_ADMIN_PASSWORD
(человекочитаемый — сообщается в конце установки, для первого входа).
Что ОБЯЗАТЕЛЬНО прописать руками ДО ./install.sh
.env.example в git хранит непустые dev-плейсхолдеры (example.com,
ws://localhost:7880, devkey и т.п.) — install.sh их НЕ трогает
(генерация секретов срабатывает только на пустых значениях). Начиная с
этой версии install.sh сам остановится с понятной ошибкой, если
что-то из списка ниже осталось плейсхолдером на реальном домене
(validate_prod_env в install.sh) — но проще сразу заполнить верно:
| Переменная | Значение для прода | Почему |
|---|---|---|
NGINX_SERVER_NAMES |
<домен> www.<домен> (через пробел, все домены из DNS) |
nginx server_name; плейсхолдер example.com не обслужит реальный трафик |
NGINX_CERT_NAME |
<домен> (основной домен из certbot-лайнеджа) |
каталог сертификата в /etc/letsencrypt/live/ |
LIVEKIT_PUBLIC_URL |
wss://<домен>/livekit/ |
критично: любой ws:// на HTTPS-странице = mixed-content, браузер молча режет соединение — конференции не стартуют. Safari/Yandex ловят это жёстче Chrome |
LIVEKIT_NODE_IP |
реальный внешний IP сервера | без него ICE-кандидаты недостижимы извне — работает только «у меня», не у remote-участников |
LIVEKIT_USE_EXTERNAL_IP |
false (дефолт, не трогать) |
связка false + реальный LIVEKIT_NODE_IP — рабочая комбинация, true вместе с node_ip избыточна и шумит в логах |
LIVEKIT_API_KEY |
сгенерируйте свой (НЕ devkey) |
devkey — публично известный идентификатор из документации LiveKit |
FRONTEND_URL |
https://<домен> |
ссылки подтверждения email иначе ведут на localhost |
AUTH_COOKIE_SECURE |
true |
без Secure на HTTPS — небезопасно, а часть браузеров такие cookie просто не сохранит |
TURN_EXTERNAL_IP |
реальный внешний IP (или оставьте 127.0.0.1, если не включаете TURN) |
нужен только разделу 8 (TURN опционален) |
Остальные переменные (email/SMTP, пресет AI, RECORDINGS_DIR и т.д.) — справочник по каждой: docs/deploy/env.md.
4. Первый подъём стека (bootstrap с самоподписанным сертификатом)
Реального сертификата ещё нет — это ожидаемо на первом запуске.
deploy/nginx/docker-entrypoint-certs.sh сгенерирует самоподписанный
сертификат, если /etc/letsencrypt/live/<NGINX_CERT_NAME> не смонтирован
или пуст — nginx стартует штатно, HTTPS работает (с предупреждением
браузера о недоверенном сертификате — это временно, до шага 5).
./install.sh --preset 2 --monitoring --yes
# --preset N — см. таблицу пресетов (docs/deploy/install.md);
# --monitoring — сразу поднять Grafana/Prometheus (раздел 6), опционально;
# --yes — без интерактивных подтверждений.
Что делает скрипт (подробности — docs/deploy/install.md):
проверяет .env на dev-плейсхолдеры (шаг 3), рендерит deploy/coturn/,
deploy/livekit/, deploy/egress/ из *.template (deploy/render-templates.sh),
собирает образы, поднимает postgres/redis, применяет миграции Alembic +
seed, поднимает остальной стек (up -d --wait).
Не запускайте docker compose вручную без --env-file .env — без него
compose не подхватывает корневой .env (файл на уровень выше
deploy/docker-compose.yml) и подставляет небезопасные дефолты из самого
docker-compose.yml (ws://localhost:7880 и т.п.) — это и было
первопричиной mixed-content на этом проекте (см. раздел 11).
install.sh передаёт --env-file сам во всех вызовах — используйте его,
а не голый docker compose up.
5. Выпуск SSL и автопродление
Стек уже поднят (шаг 4) и слушает 80/443 с самоподписанным сертификатом —
именно поэтому webroot-challenge сработает: location ^~ /.well-known/acme-challenge/ в nginx уже проксирует на volume
deploy/certbot-webroot, ничего дополнительно поднимать не нужно.
# certbot (snap — стандартный путь на Ubuntu 24.04):
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot
# -d — по одному флагу на КАЖДЫЙ домен из NGINX_SERVER_NAMES:
certbot certonly --webroot -w /opt/vidconf/deploy/certbot-webroot \
-d <домен> -d www.<домен>
# Подхватить новый сертификат (docker-entrypoint-certs.sh копирует его
# в контейнер только при СТАРТЕ — reload конфига недостаточно):
docker compose -f deploy/docker-compose.yml --env-file .env restart nginx
Откройте https://<домен> — предупреждение браузера должно исчезнуть.
Автопродление
snap install certbot сам ставит systemd-таймер (snap.certbot.renew.timer,
дважды в сутки) — руками ничего планировать не нужно. Но обязательно
добавьте deploy-hook, который перезапускает nginx-контейнер после
продления — без него certbot renew обновит файлы в /etc/letsencrypt,
а nginx продолжит отдавать старый (истекающий) сертификат до следующего
пересоздания контейнера, потому что docker-entrypoint-certs.sh копирует
сертификат только при старте:
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh << 'EOF'
#!/bin/bash
docker restart vidconf-nginx-1
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
# Проверка без реального продления:
certbot renew --dry-run
/etc/letsencrypt живёт вне /opt/vidconf — полный снос каталога
проекта (rm -rf /opt/vidconf, например, перед чистым передеплоем) сертификаты
не затрагивает, повторно выпускать их не придётся.
6. Профиль monitoring
Не входит в пресеты install.sh по умолчанию. Поднять сразу вместе со
стеком:
./install.sh --preset 2 --monitoring --yes
Или отдельной командой после (стек уже установлен):
docker compose -f deploy/docker-compose.yml --env-file .env --profile monitoring up -d
Доступ, панели дашборда, алерты — раздел 12.
7. Проверка после деплоя
# 1. Все сервисы healthy
docker compose -f deploy/docker-compose.yml --env-file .env ps
# 2. Наружу открыто только ожидаемое (80/443/7881 + udp 54000,
# плюс 3478 tcp+udp, если включили TURN — раздел 8)
ss -ltnp
# 3. Redis требует пароль (НЕ должен пускать без него)
docker exec vidconf-redis-1 redis-cli PING
# Ожидается: NOAUTH Authentication required.
# 4. Backend получил ПРАВИЛЬНЫЙ LIVEKIT_PUBLIC_URL (не localhost, не ws://)
docker exec vidconf-backend-1 env | grep LIVEKIT_PUBLIC_URL
# Ожидается: LIVEKIT_PUBLIC_URL=wss://<домен>/livekit/
docker exec vidconf-backend-1 env | grep AUTH_COOKIE_SECURE
# Ожидается: AUTH_COOKIE_SECURE=true
# 5. HTTP редиректит на HTTPS, HTTPS отвечает
curl -sI http://<домен> # 301
curl -sI https://<домен> # 200
curl -s https://<домен>/api/health
# Ожидается: {"status":"ok","db":true,"redis":true,...}
# 6. LiveKit signaling проксируется и апгрейдит WebSocket
curl -sI https://<домен>/livekit/ # 200
docker logs vidconf-nginx-1 | grep "livekit/rtc"
# Ожидается: строки со статусом 101 (WS upgrade) при реальных заходах
Ручная проверка в браузере (обязательна, автотесты этого не ловят):
войдите, создайте мгновенную конференцию, откройте DevTools → Console —
не должно быть [blocked] insecure content / ws://localhost; в Network
запрос wss://<домен>/livekit/rtc/... должен получить 101 Switching Protocols. Проверьте гостевой вход (/j/<slug> в приватном/инкогнито
окне без активной сессии) — должна показаться форма «Как вас зовут?», а не
редирект на логин.
8. TURN — опционально, для экстремального NAT
Статус: НЕ обязателен. Реальное кросс-сетевое тестирование (участники в
разных сетях/на разных устройствах) прошло успешно без раздачи TURN
клиентам — комбинации LIVEKIT_USE_EXTERNAL_IP=false + реальный
LIVEKIT_NODE_IP + проброшенный UDP-порт 54000 (шаг 1,
firewall) хватает для подавляющего большинства сетей. Включайте этот
раздел только если у вас есть конкретные пользователи за CGNAT или
жёстким корпоративным firewall, которые не могут установить медиа-соединение
(характерный симптом — конференция подключается по signaling, connection state: connected, но собеседник не видит видео/не слышит звук).
С версии 0.0.14 LiveKit анонсирует coturn клиентам — секция
rtc.turn_servers в deploy/livekit/livekit.yaml.template (UDP и TCP на
3478, credentials по механизму TURN REST API из общего
TURN_STATIC_AUTH_SECRET). Встроенный TURN LiveKit при этом остаётся
выключенным (turn.enabled: false), чтобы не поднимать два TURN-сервера.
⚠️ Чем это было до 0.0.14, если вы обновляетесь со старой версии. coturn
поднимался и был healthy, но клиенты о нём не знали: в конфиге LiveKit
внешний TURN объявлен не был, а фронтенд iceServers не задаёт. За всё
время работы в логах coturn не было ни одного ALLOCATE — то есть relay не
использовался никогда, и участники из сетей с жёстким NAT просто теряли
соединение (PEER_CONNECTION_DISCONNECTED).
Что нужно проверить на своей инсталляции:
- Порты в ufw — оба пункта (см. шаг 1):
3478/tcp+3478/udpдля сигнализации и49160:49200/udpдля relay-аллокаций. Диапазон должен совпадать сmin-port/max-portвdeploy/coturn/turnserver.conf.template. Без него TURN отвечает на запросы, но релей не работает — самый неприятный вариант, потому что в логах coturn при этом тишина. TURN_EXTERNAL_IPв.env— реальный внешний IP или домен сервера. Именно это значение уезжает клиентам как адрес TURN-сервера, поэтому127.0.0.1из dev-дефолта сделает анонс бесполезным.- После правок —
./deploy/render-templates.sh(перерендерит конфиги из шаблонов), затемdocker compose ... up -d --force-recreate livekit. ⚠️ Перезапуск LiveKit разрывает все активные конференции — выбирайте окно. - Проверка, что релей заработал: провести звонок из проблемной сети и
убедиться, что в логах появились аллокации:
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: намеренно не анонсируется: анонс неработающего адреса заставил бы
клиента ждать таймаута перед переходом к рабочему кандидату.
9. Обновление / редеплой
cd /opt/vidconf
git pull
./install.sh --preset N --yes # тот же пресет, что уже установлен —
# см. COMPOSE_PROFILES в .env, если забыли
install.sh идемпотентен: секреты и ручные правки .env сохраняются,
конфиги coturn/LiveKit/egress перерендериваются из шаблонов, образы
пересобираются, миграции применяются, стек поднимается с --wait. Если
меняли .env вручную (например, домен) без смены пресета — тоже
достаточно повторного ./install.sh --preset N --yes, шаблоны
перерендерятся автоматически.
Полный снос и чистый передеплой (например, после серьёзного инцидента или для проверки, что репозиторий — единственный источник истины):
- Снимите бэкап БД (раздел 10) и, если нужно,
.env(секреты). docker compose -f deploy/docker-compose.yml --env-file .env down -v(-vудаляет volumes, включая БД!) илиrm -rf /opt/vidconfцеликом./etc/letsencryptпри этом не трогается (живёт вне/opt/vidconf, раздел 5) — сертификат переживает снос, повторно выпускать (раздел 5,certbot certonly) не нужно.docker-entrypoint-certs.shсам найдёт существующий сертификат на первом же старте nginx и заберёт его вместо самоподписанного — просто пропустите раздел 5 целиком.git cloneзаново → шаги 2–4 этого документа (шаг 4 сразу поднимется с боевым сертификатом, см. п.3 выше) → restore БД (раздел 10) вместо/после seed.
10. Бэкап и restore БД
Значения POSTGRES_USER/POSTGRES_DB — из .env (по умолчанию vidconf/vidconf).
Бэкап
docker exec vidconf-postgres-1 pg_dump -U vidconf vidconf | gzip > db-$(date +%F).sql.gz
Снимайте перед любым рискованным действием (передеплой, снос, крупное
обновление схемы) — держите вне /opt/vidconf (например, /root/backups/),
чтобы полный снос каталога проекта не унёс с собой и бэкап.
Restore на СВЕЖЕЙ установке
Стек только что поднят install.sh — миграции и seed уже применились.
Восстанавливаем поверх, не через seed:
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 --no-sync alembic upgrade head
docker compose -f deploy/docker-compose.yml --env-file .env restart backend worker
Restore в АВАРИЙНОЙ ситуации (живой инстанс, данные повреждены)
docker compose -f deploy/docker-compose.yml --env-file .env stop backend worker worker-transcriber nginx
docker exec vidconf-postgres-1 psql -U vidconf -c "DROP DATABASE vidconf;"
docker exec vidconf-postgres-1 psql -U vidconf -c "CREATE DATABASE vidconf OWNER 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 up -d --wait
11. Траблшутинг
Формат: симптом → причина → фикс. Все пункты — реальные находки/инциденты этого проекта при первом воспроизводимом деплое.
[blocked] insecure content ws://… в консоли браузера / конференция не стартует
Причина: LIVEKIT_PUBLIC_URL содержит ws:// (не wss://) — на
HTTPS-странице браузер режет небезопасное WebSocket-соединение (Safari и
Yandex — строже Chrome). Часто это не значение из .env, а ДЕФОЛТ
docker-compose.yml (${LIVEKIT_PUBLIC_URL:-ws://localhost:7880}),
подставляемый, когда compose запущен БЕЗ --env-file — тогда даже
корректное значение в .env не подхватывается.
Фикс: LIVEKIT_PUBLIC_URL=wss://<домен>/livekit/ в .env (шаг 3) +
всегда запускайте docker compose с --env-file .env (шаг 4). Проверка:
docker exec vidconf-backend-1 env | grep LIVEKIT_PUBLIC_URL.
WARN: LIVEKIT_API_KEY not set при docker compose up
Причина: ровно та же, что и выше — команда запущена без --env-file,
compose не видит корневой .env (он на уровень выше
deploy/docker-compose.yml, автоматический поиск .env compose ищет
только рядом с самим compose-файлом/в текущей директории).
Фикс: docker compose -f deploy/docker-compose.yml --env-file /opt/vidconf/.env ... — либо используйте ./install.sh, он передаёт флаг
сам.
READONLY You can't write against a read only replica в логах LiveKit
Причина: на этом проекте был реальный инцидент — Redis публиковался
на 0.0.0.0 без пароля (ports: "6379:6379" вместо "127.0.0.1:6379:6379"),
Docker пишет DNAT-правила в обход ufw (правило в ufw status могло
отсутствовать, порт всё равно был доступен из интернета). Redis был
скомпрометирован — злоумышленник периодически переводил его в SLAVEOF
(read-only реплика) через открытый порт, что ломало регистрацию LiveKit-ноды
именно в эти окна (криптомайнер-пейлоад через cron-инъекцию — контейнерная
изоляция спасла хост, но не сам Redis).
Фикс (уже в репозитории, не требует действий): deploy/docker-compose.yml
публикует redis/postgres ТОЛЬКО на 127.0.0.1, redis запускается с
обязательным --requirepass (${REDIS_PASSWORD:?} — без пароля контейнер
не стартует). Если видите эту ошибку на инстансе, развёрнутом из текущего
репозитория, — проверьте, что ports: в docker-compose.yml не были
отредактированы вручную обратно на 0.0.0.0.
nginx-контейнер unhealthy
Причина: healthcheck обращается к http://127.0.0.1:80/, а после
добавления HTTPS-редиректа (server { listen 80; ... return 301 https://...; }) корень / на порту 80 отдаёт 301, а не 200 —
healthcheck интерпретирует это как отказ.
Фикс (уже в репозитории): healthcheck ходит на /healthz (не /) —
отдельный location, отвечающий 200 без редиректа. Если пишете свой
healthcheck поверх — используйте тот же путь.
«У меня работает, у других — нет»
Причина: чаще всего — заход по http://<IP> вместо https://<домен>.
На голом IP по HTTP браузер не применяет те же строгие проверки
mixed-content, что и на HTTPS-домене — локально «работает», а у всех
остальных, кто честно заходит на https://<домен>, ловит блокировку.
Фикс: тестируйте строго на https://<домен> — только это отражает
реальный опыт пользователей.
Прочие грабли, закрытые кодом текущего репозитория
- install.sh молча принимал плейсхолдеры
.env.exampleна боевом домене (эта самая ошибка выше, только не в логах, а в.env) — теперьinstall.shсам останавливается с понятной ошибкой (validate_prod_env), см. шаг 3. install.shне поднималmonitoringни в одном пресете (пресет перезаписываетCOMPOSE_PROFILESцеликом) — флаг--monitoring(раздел 6)./openapi.jsonотдавал HTML фронта вместо схемы API — SPA-фолбэк nginx (location /) перехватывал путь раньше, чем он доходил до backend. Добавлен точныйlocation = /openapi.json, проксирующий наbackend:8000.- Правило ufw
7880:7881/tcpбыло шире необходимого (7880 давно публикуется только на127.0.0.1) — сузьте до7881/tcp(шаг 1).
12. Как мониторить сервис
12a. Доступ к Grafana и Prometheus
Оба слушают только 127.0.0.1 на сервере (Grafana 127.0.0.1:3001 →
порт контейнера 3000, Prometheus 127.0.0.1:9090) — наружу закрыты
намеренно (харденинг после инцидента с открытым Redis, раздел 11). Доступ
— только через SSH-туннель:
ssh -L 3001:127.0.0.1:3001 -L 9090:127.0.0.1:9090 <user>@<host>
Разбор синтаксиса -L: -L <локальный_порт>:<адрес_с_точки_зрения_сервера>:<порт>.
То есть -L 3001:127.0.0.1:3001 говорит вашему ssh-клиенту: «слушай на
МОЁМ localhost:3001, всё, что туда придёт, перешли на удалённый сервер
и там доставь на 127.0.0.1:3001 — то есть на порт, который слушает
Grafana-контейнер С ТОЧКИ ЗРЕНИЯ САМОГО СЕРВЕРА». Именно поэтому в
команде указан 127.0.0.1, а не IP вашего компьютера — этот адрес
резолвится УЖЕ НА СЕРВЕРЕ, после того как ssh доставит туда трафик.
Оставьте эту сессию открытой (или добавьте -N, если оболочка не нужна —
только проброс портов, без интерактивного shell).
Дальше в браузере у себя, локально: http://localhost:3001 (Grafana),
http://localhost:9090 (Prometheus) — именно http, не https: на
localhost нет и не нужно TLS-сертификата, это туннель, а не публичный
хост.
Логин Grafana: GRAFANA_ADMIN_USER / GRAFANA_ADMIN_PASSWORD из
/opt/vidconf/.env на сервере.
Заминка: bind: Address already in use при запуске туннеля — порт
уже занят на ВАШЕМ компьютере (например, у вас локально что-то своё
слушает 3001). Смените ЛЕВУЮ цифру: -L 3300:127.0.0.1:3001, откройте
http://localhost:3300.
Закрыть доступ — exit или закрыть окно/сессию туннеля.
12b. Панели дашборда «Пайплайны пост-обработки»
Провижинится автоматически из deploy/monitoring/grafana/dashboards/pipelines.json
— появляется в Grafana сразу после подъёма профиля monitoring, без
ручной настройки. Шесть панелей:
| Панель | Метрика | Норма | Когда бить тревогу |
|---|---|---|---|
| Латентность API (p50/p95/p99 по маршрутам) | vidconf_http_request_duration_seconds (histogram) |
p50/p95 — низкие и стабильные (десятки–сотни мс для большинства маршрутов) | Главный индикатор здоровья. Устойчивый рост p95/p99 (не разовый всплеск) — проблема на бэкенде или БД. Смотрите, растёт ли ВЕЗДЕ или на конкретном маршруте ({{path}} в легенде) — второе указывает на конкретный тяжёлый эндпоинт/запрос |
| Глубина очередей Celery | vidconf_celery_queue_depth{queue=...} (transcription/summarize/notify/celery, redis LLEN) |
около нуля, кратковременные всплески при пиковой нагрузке — нормально | Устойчивый РОСТ 15 минут подряд — воркеры не успевают за потоком задач. Соответствует алерту QueueGrowing (порог >10 задач). Решение — docs/deploy/scaling.md (вынос очереди в отдельную реплику) |
| Сеансы по статусу пайплайна | vidconf_pipeline_sessions{status=...} (gauge) |
распределение по recording→transcribing→summarizing→notified, failed — единицы или ноль |
Заметный/растущий failed — сбой в пайплайне пост-обработки (транскрибация/суммаризация), смотрите логи worker/worker-transcriber |
| Сеансы failed (текущее число) | vidconf_pipeline_sessions{status="failed"} (stat, снимок) |
0 |
Любое значение >0, не обнуляющееся между проверками — соответствует алерту PipelineFailed (срабатывает на РОСТЕ за 15 минут, не на самом факте наличия) |
| Длительность шагов пайплайна (p95) | vidconf_pipeline_step_duration_seconds |
— | Заготовка: метрика ещё не инструментирована в коде (backend/api/metrics.py пока не пишет её) — панель осознанно показывает «No data» до появления соответствующей инструментации в будущей версии |
LLM-сервер доступен (up{job="llm"}) |
up{job="llm"} (stat: UP/DOWN) |
UP (1) — на инсталляциях с профилем llm/llm-gpu |
DOWN дольше 2 минут = алерт LlmDown, суммаризация встанет. На пресетах 1–2 (без AI) эта панель ВСЕГДА «No data» — это ожидаемо, см. 12c |
Алерты (deploy/monitoring/alerts.yml): PipelineFailed (critical),
QueueGrowing (warning), LlmDown (critical, закомментирован по
умолчанию — см. 12c/12d). Полный список условий и проверка искусственным
падением сервиса — docs/deploy/monitoring.md.
12c. Что активно на профилях без LLM/транскрипции
Панели латентности API, очередей Celery и статусов сеансов питаются от
job backend, который скрейпится всегда (не зависит от профиля) —
они наполнятся сами, как только пайплайн реально обработает хотя бы один
сеанс. На пресетах 1–2 (без транскрибации/суммаризации) панели статусов/
очередей пайплайна будут пустыми не потому что что-то сломано, а потому
что пайплайн просто не запускается — событий нет.
Job llm в prometheus.yml и алерт LlmDown в alerts.yml
закомментированы по умолчанию — целевой набор профилей
(media,monitoring из .env.example) не включает llm/llm-gpu, и
адрес llm:8080 не резолвится вовсе; активный scrape на несуществующий
таргет означал бы постоянные ошибки в логах Prometheus и вечно firing
алерт. Это ожидаемое, документированное в самих файлах поведение — не
баг.
12d. Runbook: включить транскрибацию/суммаризацию и вернуть их мониторинг
Включение — на ДВУХ независимых уровнях: инфраструктура (контейнеры) и приложение (настройки инстанса). Плюс отдельно — ручной шаг возврата LLM-мониторинга.
1. Инфраструктура — поднять контейнеры:
# Через install.sh (пересчитает и COMPOSE_PROFILES, и модели уровня AI):
./install.sh --preset 3 --yes # min: faster-whisper small + Qwen3.5-4B
./install.sh --preset 4 --yes # medium
./install.sh --preset 5 --yes # max, GPU обязателен
# Или вручную поверх уже поднятого стека — добавить профили в .env
# (COMPOSE_PROFILES=media,monitoring,transcribe,llm) и:
docker compose -f deploy/docker-compose.yml --env-file .env --profile transcribe --profile llm up -d
Проверьте место на диске заранее (раздел 1) — веса моделей transcribe+llm
занимают ощутимо больше, чем базовый стек, боевой инстанс с этими
профилями упирался в 99% занятого диска на недооценённом объёме.
2. Приложение — настройки инстанса:
install.sh синхронизирует их сам по выбранному пресету
(BOOTSTRAP_TRANSCRIPTION_ENABLED=true, BOOTSTRAP_AI_LEVEL=min|medium|max
в .env — применяются при первом старте backend; на уже работающей
инсталляции install.sh спросит «обновить настройки модулей под
пресет?» и применит через scripts.apply_preset_settings). Вручную —
админка → Настройки, либо PUT /api/v1/admin/settings.
3. Вернуть мониторинг LLM (иначе панель «LLM-сервер доступен» и
LlmDown молчат навсегда, даже когда LLM реально поднят):
Раскомментировать ОБА файла ВМЕСТЕ (порознь — рассинхрон: активный алерт на несуществующую метрику или наоборот):
deploy/monitoring/prometheus.yml, блокjob_name: llm(~строки 51–53)deploy/monitoring/alerts.yml, правилоLlmDown(~строки 51+)
docker restart vidconf-prometheus-1
# или: перезагрузить конфиг без рестарта контейнера (если включён
# --web.enable-lifecycle у Prometheus — по умолчанию в этом проекте НЕ
# включён, поэтому restart — рабочий путь по умолчанию)
Это осознанно ручной шаг: Prometheus использует статический
scrape-конфиг (файл), он не видит текущие профили docker compose —
раскомментировать нужно именно тогда, когда профиль llm/llm-gpu
реально поднят, иначе см. 12c (постоянные ошибки скрейпа + вечный alert).
4. Проверка:
- Проведите тестовый сеанс конференции (запись → транскрибация → суммаризация) — панели «Сеансы по статусу», «Глубина очередей» наполнятся реальными данными.
up{job="llm"}=1в Prometheus (http://localhost:9090через туннель, вкладка Graph).- Целевой тест алерта (искусственно остановить LLM и убедиться, что
LlmDown/QueueGrowing/PipelineFailedсрабатывают по цепочке) — docs/deploy/monitoring.md, раздел «Алерты».
Ссылки
- docs/deploy/install.md — пресеты, что делает
install.shпошагово - docs/deploy/env.md — полный справочник переменных
.env - docs/deploy/monitoring.md — компоненты, метрики, алерты подробно
- docs/deploy/scaling.md — горизонтальное масштабирование воркеров
- docs/deploy/dev-setup.md — локальная разработка (НЕ этот документ)
- docs/architecture/adr/004-ai-tier-matrix.md — модели, кванты, требования железа по уровням AI