Files
vidconf/docs/deploy/DEPLOYMENT.md
Max Ronzhin ce00ecf5fc docs: добавить docs/deploy/DEPLOYMENT.md — руководство по боевому развёртыванию
Воспроизводимый путь от голой Ubuntu 24 до рабочего https://<домен>:
предусловия (DNS/firewall/место на диске), выпуск SSL (bootstrap
самоподписанным сертификатом → install.sh → certbot --webroot +
deploy-hook на автопродление), полный разбор обязательных значений .env,
профиль monitoring, чек-лист проверки после деплоя, TURN как опциональный
раздел для экстремального NAT (по итогам реального кросс-сетевого
тестирования — не обязателен), обновление/редеплой, бэкап и restore БД,
траблшутинг по реальным инцидентам проекта, полный разбор мониторинга
(доступ через SSH-туннель, панели дашборда с порогами тревоги, runbook
включения транскрибации/суммаризации).

Ссылки на документ добавлены в README.md: врезка под «Быстрый старт» и
приоритетная ссылка в разделе «Развёртывание».
2026-07-26 02:58:06 +03:00

642 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Развёртывание на боевом сервере — от голой 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)):
пресеты 12 (без AI) нетребовательны (2 vCPU / 4 ГБ RAM с запасом хватает),
пресеты 35 — см. таблицу ADR-004 (до 16+ vCPU / 64 ГБ RAM / GPU для max).
### Место на диске
**Критично проверить заранее** — на боевом инстансе с профилями
`transcribe`+`llm` (пресеты 35) стек реально упирался в 99% занятого диска.
Ориентиры:
| Набор профилей | Место под образы/модели |
|---|---|
| `media,monitoring` (пресеты 12, без AI) | ~5 ГБ |
| + `transcribe,llm` (пресет 3, уровень min) | ещё ~58 ГБ (веса Qwen3.5-4B ~2,8 ГБ + faster-whisper small) |
| + `transcribe,llm` (пресет 4, medium) | ещё ~1015 ГБ (Qwen3.5-9B ~6,2 ГБ) |
| + `transcribe-gpu,llm-gpu` (пресет 5, max) | ещё ~25+ ГБ (Qwen3.5-35B-A3B ~2022 ГБ) |
Плюс место под записи (`recordings`, если включена транскрибация) и рост БД
со временем. Для пресетов 12 достаточно диска от 20 ГБ, для 35 — 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 <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`, но собеседник не видит видео/не слышит звук).
По умолчанию `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` заново → шаги 24 этого документа (шаг 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://<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`, суммаризация встанет. **На пресетах 12 (без 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`, который скрейпится **всегда** (не зависит от профиля) —
они наполнятся сами, как только пайплайн реально обработает хотя бы один
сеанс. На пресетах 12 (без транскрибации/суммаризации) панели статусов/
очередей пайплайна будут пустыми не потому что что-то сломано, а потому
что пайплайн просто не запускается — событий нет.
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` (~строки 5153)
- `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