diff --git a/README.md b/README.md index 8e71938..7ab1648 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ ## Быстрый старт +> 📘 **Разворачиваете на боевом сервере?** Пошаговое руководство — +> [docs/deploy/DEPLOYMENT.md](docs/deploy/DEPLOYMENT.md): от голой Ubuntu до +> `https://<домен>` (SSL, `.env`, профили, траблшутинг, мониторинг). +> Раздел ниже — быстрый старт для разработки. + ### Требования - Docker + Docker Compose v2 @@ -237,6 +242,11 @@ UI следует строгим рекомендациям дизайна, оп ## Развёртывание +**Полное руководство по боевому развёртыванию:** [docs/deploy/DEPLOYMENT.md](docs/deploy/DEPLOYMENT.md) — +воспроизводимый путь от голой Ubuntu 24 до рабочего `https://<домен>`: +предусловия, выпуск SSL, разбор `.env`, запуск, проверка после деплоя, +траблшутинг, мониторинг. + ### Пресеты инсталлятора VidConf использует единый инсталлятор `install.sh` с 5 пресетами и автодетектом железа: diff --git a/docs/deploy/DEPLOYMENT.md b/docs/deploy/DEPLOYMENT.md new file mode 100644 index 0000000..6e4c653 --- /dev/null +++ b/docs/deploy/DEPLOYMENT.md @@ -0,0 +1,641 @@ +# Развёртывание на боевом сервере — от голой 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: +# ufw allow 3478/tcp +# ufw allow 3478/udp +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 /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 стартует штатно, 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/` в приватном/инкогнито +окне без активной сессии) — должна показаться форма «Как вас зовут?», а не +редирект на логин. + +--- + +## 8. TURN — опционально, для экстремального NAT + +**Статус: НЕ обязателен.** Реальное кросс-сетевое тестирование (участники в +разных сетях/на разных устройствах) прошло успешно **без** раздачи TURN +клиентам — комбинации `LIVEKIT_USE_EXTERNAL_IP=false` + реальный +`LIVEKIT_NODE_IP` + проброшенный UDP-диапазон `54000-54100` (шаг 1, +firewall) хватает для подавляющего большинства сетей. Включайте этот +раздел только если у вас есть конкретные пользователи за CGNAT или +жёстким корпоративным firewall, которые не могут установить медиа-соединение +(характерный симптом — конференция подключается по signaling, `connection +state: connected`, но собеседник не видит видео/не слышит звук). + +По умолчанию `deploy/livekit/livekit.yaml.template` содержит +`turn.enabled: false`, и `rtc.turn_servers` не задан — standalone coturn +поднимается (профиль `media`), но LiveKit не раздаёт его клиентам как +ICE-фолбэк. + +Включение (правки шаблона `deploy/livekit/livekit.yaml.template` + +редеплой; **код-фикс не входит в это руководство без запроса** — обсудите +с командой перед изменением): + +1. Открыть 443 для TURN/TLS (наиболее надёжный фолбэк — TURN через тот же + порт, что и остальной HTTPS-трафик, редко блокируется firewall'ами): + потребует отдельного TLS-сертификата для coturn (`cert-file`/`pkey-file` + в `deploy/coturn/turnserver.conf.template`) — можно переиспользовать тот + же Let's Encrypt сертификат, что и nginx (тот же `/etc/letsencrypt`, уже + смонтированный в nginx — coturn сейчас его не монтирует, потребуется + доп. volume). +2. В `livekit.yaml.template` включить `turn.enabled: true` и/или явно + прописать `rtc.turn_servers` со статическими credentials + (`TURN_STATIC_AUTH_SECRET` уже есть в `.env`). +3. `ufw allow 3478/tcp` + `ufw allow 3478/udp` (шаг 1, закомментированные + строки) — сейчас coturn поднят, но порт не проверялся как обязательный. +4. Передеплой (`docker compose ... up -d --force-recreate livekit coturn`) + и повторный кросс-сетевой тест именно с проблемной сетью. + +--- + +## 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 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://` вместо `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 @ +``` + +Разбор синтаксиса `-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