coturn поднимался, был healthy и слушал 3478 — но клиенты о нём никогда не узнавали: встроенный TURN выключен (`turn.enabled: false`), внешний в конфигурации не объявлен, фронтенд `iceServers` не задаёт. За всё время работы сервера в логах coturn нет ни одной аллокации. Следствие: у участников из сетей, где прямое UDP-соединение не проходит, не было relay-фолбэка вообще — только прямой UDP и TCP 7881. На нагрузочном тесте 28.07 все разрывы `PEER_CONNECTION_DISCONNECTED` пришлись на внешних участников и ни одного — на офисных. Добавлена секция `rtc.turn_servers` (UDP и TCP на 3478). Эти серверы только анонсируются клиенту в списке ICE — сам SFU через них не ходит (см. iceServersForParticipant в LiveKit). Credentials генерируются по механизму TURN REST API из общего `TURN_STATIC_AUTH_SECRET`, поэтому `render-templates.sh` теперь подставляет его и `TURN_EXTERNAL_IP` также в конфигурацию LiveKit. TLS (5349/443) намеренно не анонсируется: сертификаты в coturn не смонтированы, а неработающий `turns:` заставил бы клиента ждать таймаута перед переходом к рабочему кандидату. Что нужно для его включения — описано в разделе 8 руководства. Там же исправлено умолчание в правилах ufw: помимо 3478 нужен диапазон relay-аллокаций `49160:49200/udp`. Без него TURN отвечает на запросы, но релей не работает, причём в логах coturn при этом тишина.
677 lines
46 KiB
Markdown
677 lines
46 KiB
Markdown
# Развёртывание на боевом сервере — от голой Ubuntu до `https://<домен>`
|
||
|
||
Пошаговое воспроизводимое руководство: чистый сервер Ubuntu 24.04 → рабочий
|
||
VidConf на реальном домене с TLS. Путь проверен вживую (снос + чистый деплой
|
||
+ полное тестирование стека), детали инцидентов и находок — в разделе
|
||
«Траблшутинг» ниже.
|
||
|
||
Для локальной разработки этот документ не нужен — см.
|
||
[docs/deploy/dev-setup.md](dev-setup.md). Здесь — только боевой сценарий.
|
||
|
||
## Содержание
|
||
|
||
1. [Предусловия](#1-предусловия)
|
||
2. [Клонирование репозитория](#2-клонирование-репозитория)
|
||
3. [Настройка `.env`](#3-настройка-env)
|
||
4. [Первый подъём стека (bootstrap с самоподписанным сертификатом)](#4-первый-подъём-стека-bootstrap-с-самоподписанным-сертификатом)
|
||
5. [Выпуск SSL и автопродление](#5-выпуск-ssl-и-автопродление)
|
||
6. [Профиль monitoring](#6-профиль-monitoring)
|
||
7. [Проверка после деплоя](#7-проверка-после-деплоя)
|
||
8. [TURN — опционально, для экстремального NAT](#8-turn--опционально-для-экстремального-nat)
|
||
9. [Обновление / редеплой](#9-обновление--редеплой)
|
||
10. [Бэкап и restore БД](#10-бэкап-и-restore-бд)
|
||
11. [Траблшутинг](#11-траблшутинг)
|
||
12. [Как мониторить сервис](#12-как-мониторить-сервис)
|
||
|
||
---
|
||
|
||
## 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](../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
|
||
|
||
```bash
|
||
curl -fsSL https://get.docker.com | sh
|
||
# Проверка (нужен именно compose PLUGIN — "docker compose", не отдельный docker-compose):
|
||
docker compose version
|
||
```
|
||
|
||
### Firewall (ufw)
|
||
|
||
Открыть строго то, что реально нужно снаружи — избыточные правила вводят
|
||
в заблуждение и расширяют поверхность атаки без пользы (см. раздел
|
||
«Траблшутинг», инцидент с открытым Redis, — конкретно эти порты он не
|
||
касался, но принцип «минимум наружу» из него и вырос):
|
||
|
||
```bash
|
||
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:54100/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. Клонирование репозитория
|
||
|
||
```bash
|
||
git clone <URL вашего репозитория> /opt/vidconf
|
||
cd /opt/vidconf
|
||
```
|
||
|
||
Дальше все команды — из `/opt/vidconf`, если не указано иное.
|
||
|
||
---
|
||
|
||
## 3. Настройка `.env`
|
||
|
||
```bash
|
||
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](env.md).
|
||
|
||
---
|
||
|
||
## 4. Первый подъём стека (bootstrap с самоподписанным сертификатом)
|
||
|
||
Реального сертификата ещё нет — это ожидаемо на первом запуске.
|
||
`deploy/nginx/docker-entrypoint-certs.sh` сгенерирует самоподписанный
|
||
сертификат, если `/etc/letsencrypt/live/<NGINX_CERT_NAME>` не смонтирован
|
||
или пуст — nginx стартует штатно, HTTPS работает (с предупреждением
|
||
браузера о недоверенном сертификате — это временно, до шага 5).
|
||
|
||
```bash
|
||
./install.sh --preset 2 --monitoring --yes
|
||
# --preset N — см. таблицу пресетов (docs/deploy/install.md);
|
||
# --monitoring — сразу поднять Grafana/Prometheus (раздел 6), опционально;
|
||
# --yes — без интерактивных подтверждений.
|
||
```
|
||
|
||
Что делает скрипт (подробности — [docs/deploy/install.md](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`, ничего дополнительно поднимать не нужно.
|
||
|
||
```bash
|
||
# 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` копирует
|
||
сертификат только при старте:
|
||
|
||
```bash
|
||
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` по умолчанию. Поднять сразу вместе со
|
||
стеком:
|
||
|
||
```bash
|
||
./install.sh --preset 2 --monitoring --yes
|
||
```
|
||
|
||
Или отдельной командой после (стек уже установлен):
|
||
|
||
```bash
|
||
docker compose -f deploy/docker-compose.yml --env-file .env --profile monitoring up -d
|
||
```
|
||
|
||
Доступ, панели дашборда, алерты — раздел 12.
|
||
|
||
---
|
||
|
||
## 7. Проверка после деплоя
|
||
|
||
```bash
|
||
# 1. Все сервисы healthy
|
||
docker compose -f deploy/docker-compose.yml --env-file .env ps
|
||
|
||
# 2. Наружу открыто только ожидаемое (80/443/7881 + udp 54000-54100,
|
||
# плюс 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-54100` (шаг 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`).
|
||
|
||
Что нужно проверить на своей инсталляции:
|
||
|
||
1. **Порты в ufw — оба пункта** (см. шаг 1): `3478/tcp` + `3478/udp` для
|
||
сигнализации и `49160:49200/udp` для relay-аллокаций. Диапазон должен
|
||
совпадать с `min-port`/`max-port` в
|
||
`deploy/coturn/turnserver.conf.template`. Без него TURN отвечает на
|
||
запросы, но релей не работает — самый неприятный вариант, потому что в
|
||
логах coturn при этом тишина.
|
||
2. **`TURN_EXTERNAL_IP` в `.env`** — реальный внешний IP или домен сервера.
|
||
Именно это значение уезжает клиентам как адрес TURN-сервера, поэтому
|
||
`127.0.0.1` из dev-дефолта сделает анонс бесполезным.
|
||
3. После правок — `./deploy/render-templates.sh` (перерендерит конфиги из
|
||
шаблонов), затем `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:` намеренно не анонсируется: анонс неработающего адреса заставил бы
|
||
клиента ждать таймаута перед переходом к рабочему кандидату.
|
||
|
||
---
|
||
|
||
## 9. Обновление / редеплой
|
||
|
||
```bash
|
||
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`, шаблоны
|
||
перерендерятся автоматически.
|
||
|
||
**Полный снос и чистый передеплой** (например, после серьёзного инцидента
|
||
или для проверки, что репозиторий — единственный источник истины):
|
||
1. Снимите бэкап БД (раздел 10) и, если нужно, `.env` (секреты).
|
||
2. `docker compose -f deploy/docker-compose.yml --env-file .env down -v`
|
||
(`-v` удаляет volumes, включая БД!) или `rm -rf /opt/vidconf` целиком.
|
||
3. `/etc/letsencrypt` при этом не трогается (живёт вне `/opt/vidconf`,
|
||
раздел 5) — сертификат переживает снос, повторно выпускать (раздел 5,
|
||
`certbot certonly`) не нужно. `docker-entrypoint-certs.sh` сам найдёт
|
||
существующий сертификат на первом же старте nginx и заберёт его вместо
|
||
самоподписанного — просто пропустите раздел 5 целиком.
|
||
4. `git clone` заново → шаги 2–4 этого документа (шаг 4 сразу поднимется
|
||
с боевым сертификатом, см. п.3 выше) → restore БД (раздел 10)
|
||
вместо/после seed.
|
||
|
||
---
|
||
|
||
## 10. Бэкап и restore БД
|
||
|
||
Значения `POSTGRES_USER`/`POSTGRES_DB` — из `.env` (по умолчанию `vidconf`/`vidconf`).
|
||
|
||
### Бэкап
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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 в АВАРИЙНОЙ ситуации (живой инстанс, данные повреждены)
|
||
|
||
```bash
|
||
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-туннель:
|
||
|
||
```bash
|
||
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](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](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. Инфраструктура — поднять контейнеры:**
|
||
|
||
```bash
|
||
# Через 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+)
|
||
|
||
```bash
|
||
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](monitoring.md), раздел «Алерты».
|
||
|
||
---
|
||
|
||
## Ссылки
|
||
|
||
- [docs/deploy/install.md](install.md) — пресеты, что делает `install.sh` пошагово
|
||
- [docs/deploy/env.md](env.md) — полный справочник переменных `.env`
|
||
- [docs/deploy/monitoring.md](monitoring.md) — компоненты, метрики, алерты подробно
|
||
- [docs/deploy/scaling.md](scaling.md) — горизонтальное масштабирование воркеров
|
||
- [docs/deploy/dev-setup.md](dev-setup.md) — локальная разработка (НЕ этот документ)
|
||
- [docs/architecture/adr/004-ai-tier-matrix.md](../architecture/adr/004-ai-tier-matrix.md) — модели, кванты, требования железа по уровням AI
|