DealDocumentScreening/docs/DEPLOY.md

785 lines
33 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.

# Руководство по развёртыванию «Контракт-чек»
> Целевая аудитория: DevOps / разработчик, поднимающий стек на одном VPS или локально.
> Предполагается: Docker + Docker Compose установлены, есть SSH-доступ к VPS.
> Полная архитектура: [`ARCHITECTURE.md`](ARCHITECTURE.md).
---
## Содержание
1. [Требования](#1-требования)
2. [Быстрый старт локально](#2-быстрый-старт-локально)
3. [Конфигурация через `.env`](#3-конфигурация-через-env)
4. [Docker Compose — полный стек](#4-docker-compose--полный-стек)
5. [Первичная инициализация](#5-первичная-инициализация)
6. [Проверка работоспособности](#6-проверка-работоспособности)
7. [Telegram-бот: первый запуск](#7-telegram-бот-первый-запуск)
8. [B2B API: первый API-ключ](#8-b2b-api-первый-api-ключ)
9. [Резервное копирование](#9-резервное-копирование)
10. [Обновление (zero-downtime-ish)](#10-обновление)
11. [Масштабирование](#11-масштабирование)
12. [Troubleshooting](#12-troubleshooting)
13. [Edge proxy (Nginx + certbot)](#13-edge-proxy-nginx--certbot)
14. [Telegram-бот на отдельном сервере](#14-telegram-бот-на-отдельном-сервере)
---
## 1. Требования
| Компонент | Минимум | Рекомендуемое |
|---|---|---|
| CPU | 2 vCPU | 4 vCPU (worker-extract CPU-bound) |
| RAM | 2 GB | 4 GB |
| Disk | 20 GB SSD | 40 GB SSD (MinIO + WAL-archive) |
| OS | Ubuntu 22.04+ / Debian 12+ | Ubuntu 24.04 LTS |
| Docker | 24.0+ | 25.0+ |
| Docker Compose | v2 | v2 |
| Интернет | нужен для Ollama Cloud | — |
> **Важно:** Ollama Cloud инферит в США. Если 152-ФЗ data-residency критичен —
> рассмотрите self-hosted Ollama (GPU) или GigaChat-адаптер (`core/llm/factory.py`).
---
## 2. Быстрый старт локально
### 2.1 Клонирование и env
```bash
git clone <repo> contract-check
cd contract-check
cp .env.example .env
# Отредактируйте .env — минимум: OLLAMA_HOST, OLLAMA_API_KEY, BOT_TOKEN,
# TELEGRAM_BOT_TOKEN, JWT_SECRET
```
### 2.2 Инфраструктура (только Postgres + Redis + RabbitMQ + MinIO)
```bash
docker compose up -d
# Ждём healthy:
docker compose ps
# Ожидаемый результат: postgres, redis, rabbitmq, minio, minio-init — Up (healthy)
```
Порты на хосте (смещены, чтобы не конфликтовать):
- Postgres: `15432`
- Redis: `17379`
- RabbitMQ AMQP: `5672`, Management UI: `http://localhost:15672`
- MinIO S3 API: `9000`, Console: `http://localhost:9001`
### 2.3 Миграции
```bash
# Локально (нужен uv + dev-группа):
uv sync --group dev
uv run alembic upgrade head
# Или через временный api-контейнер:
docker compose --profile services run --rm api alembic upgrade head
```
### 2.4 Полный стек (api + workers + bot)
```bash
docker compose --profile services up -d --build
# Ждём healthy:
docker compose --profile services ps
```
---
## 3. Конфигурация через `.env`
Все секреты — в `.env` (не коммитить!). Ключевые переменные:
```bash
# --- LLM (обязательно) ---
OLLAMA_HOST=https://api.ollama.com
OLLAMA_API_KEY=sk-xxxxxxxx
OLLAMA_MODEL=qwen2.5:14b
OLLAMA_FALLBACK_MODEL=qwen2.5:7b
# --- Telegram + auth (обязательно) ---
BOT_TOKEN=123456789:ABCDEF... # для aiogram бота
TELEGRAM_BOT_TOKEN=$BOT_TOKEN # тот же токен; API использует для проверки Login Widget / Mini App
BOT_SERVICE_TOKEN=bot-prod-secret-xxx # см. §5.3
JWT_SECRET=$(openssl rand -hex 32) # HS256 secret для подписи JWT
# --- WebUI auth + admin panel (опционально, defaults ниже) ---
WEB_AUTH_ENABLED=true # /api/v1/auth/register,login,...
WEB_APP_BASE_URL=http://localhost:5173 # SPA — ссылки для сброса пароля
WEB_ADMIN_ENABLED=true # панель управления по /admin
ADMIN_REQUIRED_ROLE=admin # users.role, которое допущено в /admin
PASSWORD_MIN_LENGTH=8
POSTGRES_USER=contract_check
POSTGRES_PASSWORD=changeme-strong-password
POSTGRES_DB=contract_check
# --- RabbitMQ ---
RABBITMQ_USER=contract_check
RABBITMQ_PASS=changeme-strong-password
# --- MinIO ---
S3_ACCESS_KEY=contract_check
S3_SECRET_KEY=changeme-strong-password
S3_BUCKET=contract-check-docs
# --- Billing ---
REFUND_POLICY=all # или infra_only
PLANS_ENABLED=false # квоты подписок (миграция 0011); false = только кредиты
YOOKASSA_ENABLED=false # выключено → checkout/refund отвечают 503
PRICE_PER_DOC_KOPECKS=19900 # 199 ₽ за документ без подписки
# Полный список биллинг-env: см. .env.example и ARCHITECTURE.md §11
# --- Observability (опционально) ---
SENTRY_DSN=https://...@sentry.io/...
# OTLP/HTTP endpoint. With profile `observer` use the local collector.
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OPENOBSERVE_AUTH_TOKEN=cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM=
```
> Полный список: см. `.env.example` и `ARCHITECTURE.md §11`. Включение
> платежей (ЮKassa) — §4a ниже.
### 4a. ЮKassa: включение платежей
1. Получите `shopId` и секретный ключ в личном кабинете ЮKassa
(Настройки → API-ключи; для старта можно «тестовый» магазин).
2. Пропишите в `.env`:
```bash
YOOKASSA_ENABLED=true
YOOKASSA_SHOP_ID=812345
YOOKASSA_SECRET_KEY=live_Ax... # или test_...
YOOKASSA_RETURN_BASE_URL=https://<ваш-домен>/pay/{invoice_id}
PLANS_ENABLED=true # включить квоты подписок
BILLING_RETURN_JWT_SECRET=$(openssl rand -hex 32) # или пусто = JWT_SECRET
```
3. В личном кабинете ЮKassa настройте HTTP-уведомления (webhook) на
`https://<ваш-домен>/api/v1/webhooks/yookassa` для событий
`payment.succeeded`, `payment.canceled`, `refund.succeeded`.
API аутентифицирует уведомления по Basic auth (`shopId:secretKey`) и
**всегда перезапрашивает платёж через REST** — сумме из push не доверяем.
4. `docker compose --profile services up -d --build` — изменения подхватятся
без правок compose (12-factor).
5. Поднимите `worker-billing` (входит в профиль `services`): продления
подписок, expiry/past_due, сверка «зависших» pending-счетов старше 15 мин.
**Degraded-режим** (`YOOKASSA_ENABLED=false`, дефолт): каталог тарифов
`GET /api/v1/billing/plans` читается, мутации checkout/refund отвечают `503`,
webhook-и игнорируются с warning-логом. Отключение платежей не требует
отката миграций.
**Тюнинг каталога**: тарифы сидируются миграцией 0011 (free/lite/pro/max).
Правьте таблицу `plans` напрямую (цены — в копейках):
```bash
make shell-db # UPDATE plans SET price_kopecks = 59000 WHERE code = 'lite';
```
---
## 4. Docker Compose — полный стек
### 4.1 Профили
| Профиль | Что поднимает |
|---|---|
| *(default)* | `postgres`, `redis`, `rabbitmq`, `minio`, `minio-init` |
| `services` | `api`, `worker-extract`, `worker-prescreen`, `worker-analyze`, `worker-billing`, `worker-notify`, `bot` |
| `obs` | `loki`, `promtail`, `grafana`, `prometheus` (logs + metrics; traces — позже) |
| `observer` | `openobserve`, `otel-collector` (logs + metrics + traces через collector) |
| `edge` | `nginx`, `certbot` |
```bash
# Инфра:
docker compose up -d
# + сервисы (сборка + запуск):
docker compose --profile services up -d --build
# + observability Grafana/Loki/Prometheus (логи + метрики):
docker compose --profile services --profile obs up -d --build
# + observability OpenObserve (логи + метрики + трейсы через otel-collector):
docker compose --profile services --profile observer up -d --build
# + edge (nginx reverse proxy + TLS; deploy/nginx готов):
docker compose --profile services --profile observer --profile edge up -d --build
```
### 4.2 depends_on и healthchecks
`api` ждёт `postgres`, `rabbitmq`, `minio-init` (healthy / completed).
`bot` ждёт только `api` (healthy) — не инфра напрямую, соблюдая hexagonal boundary.
Workers ждут `postgres`, `rabbitmq`, `minio-init`.
---
## 5. Первичная инициализация
### 5.1 Миграции
```bash
# Способ A: локально (если uv установлен)
uv run alembic upgrade head
# Способ B: через api-контейнер (если uv недоступен на хосте)
docker compose --profile services run --rm api alembic upgrade head
```
Проверка:
```bash
docker compose exec postgres psql -U contract_check -d contract_check -c "\dt"
# Должны быть: api_key_requests, api_keys, documents, invoices, jobs, reports, service_tokens, users
```
### 5.2 Seed-token для адаптеров (bot / web / cli)
Бот аутентифицируется в api через `Authorization: Bearer <BOT_SERVICE_TOKEN>`.
Этот токен нужно положить в `service_tokens`.
```bash
# Генерируем секрет
BOT_SERVICE_TOKEN=$(openssl rand -hex 32)
echo "BOT_SERVICE_TOKEN=$BOT_SERVICE_TOKEN" >> .env
# Записываем hash в БД через временный api-контейнер
docker compose --profile services run --rm api \
python -c "
import asyncio, os, sys
os.chdir('/app')
sys.path.insert(0, 'src')
from contract_check.core.config import get_settings
from contract_check.core.tokens import hash_token
from contract_check.core.db.session import create_session_factory
from sqlalchemy import text
async def seed():
factory = create_session_factory()
async with factory() as s:
h = hash_token('$BOT_SERVICE_TOKEN')
await s.execute(text('INSERT INTO service_tokens (name, token_hash, adapter) VALUES (:n, :h, :a) ON CONFLICT (name) DO UPDATE SET token_hash = EXCLUDED.token_hash'), {'n': 'bot-prod', 'h': h, 'a': 'bot'})
await s.commit()
print('seeded bot-prod')
asyncio.run(seed())
"
```
> В production используйте `python -m contract_check.api seed-token` CLI (если реализовано).
### 5.3 Перезапуск бота с новым токеном
```bash
docker compose --profile services restart bot
# Проверка логов:
docker compose --profile services logs -f bot
```
---
## 6. Проверка работоспособности
### 6.1 Health endpoints
```bash
curl http://localhost:8000/healthz # liveness — 200
curl http://localhost:8000/readyz # readiness — 200 (PG + Rabbit + MinIO)
curl http://localhost:8000/metrics # Prometheus exposition
```
### 6.2 RabbitMQ Management UI
Откройте `http://localhost:15672` (guest/guest или credentials из `.env`).
Проверьте:
- Exchange `contracts.x` (direct)
- Queues: `extract.q`, `analyze.q` (quorum), `extract.retry.q`, `analyze.retry.q`, DLQ
- Connections/consumers от worker-ов
### 6.3 MinIO Console
`http://localhost:9001` (root user = `S3_ACCESS_KEY` / `S3_SECRET_KEY`).
Бакет `contract-check-docs` должен появиться после `minio-init`.
### 6.4 End-to-end smoke-тест (через curl)
```bash
# 1. Сначала нужен JWT пользователя. В dev можно обменять telegram_id через
# service-token endpoint /auth/telegram/bot (в production этим занимается бот):
curl -X POST -H "Authorization: Bearer $BOT_SERVICE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"telegram_id": 123456}' \
http://localhost:8000/api/v1/auth/telegram/bot
# → {"access_token":"eyJ...", "user_id":"...", "telegram_id":123456, ...}
USER_JWT="eyJ..." # скопируйте access_token
# 2. Проверить /me
curl -H "Authorization: Bearer $USER_JWT" http://localhost:8000/api/v1/me
# → {"telegram_id":123456, "credits_left":0}
# 3. Загрузить документ (нужен credit — пока 0, но проверим путь)
curl -X POST -H "Authorization: Bearer $USER_JWT" \
-F "file=@contract.pdf" http://localhost:8000/api/v1/documents
# → 402 Payment Required (нормально — нет кредитов)
# 4. Добавить кредит вручную (dev):
docker compose exec postgres psql -U contract_check -d contract_check \
-c "UPDATE users SET credits_left = 10 WHERE telegram_id = 123456;"
```
---
## 7. Telegram-бот: первый запуск
### 7.1 Создание бота в BotFather
1. Напишите `@BotFather` → `/newbot`
2. Скопируйте токен (`BOT_TOKEN`) в `.env`
3. Установите webhook (опционально, polling работает по умолчанию):
```bash
curl -F "url=https://your-domain.com/webhook" \
https://api.telegram.org/bot$BOT_TOKEN/setWebhook
```
### 7.2 Запуск
```bash
docker compose --profile services up -d bot
docker compose --profile services logs -f bot
```
Ожидаемый вывод при `/start`:
```
Привет! Я «Контракт-чек» — первичный скрининг рисков в договорах...
Осталось проверок: 0.
```
### 7.3 Добавление кредитов пользователю
```bash
# Найти telegram_id пользователя (из логов бота или таблицы users)
docker compose exec postgres psql -U contract_check -d contract_check \
-c "UPDATE users SET credits_left = credits_left + 5 WHERE telegram_id = <tg_id>;"
```
---
## 8. B2B API: первый API-ключ
### 8.1 Создание ключа (через user JWT)
Управление B2B-ключами теперь требует пользовательский JWT. Получите JWT через
`/auth/telegram/bot` (как в §6.4) и выполните:
```bash
curl -X POST -H "Authorization: Bearer $USER_JWT" \
-H "Content-Type: application/json" \
-d '{"name": "integration-test", "rate_limit_rps": 3, "monthly_quota": 100}' \
http://localhost:8000/api/v1/b2b/keys
# → {"api_key": "cc_abc123...", "id": "...", ...}
# Сохраните api_key — он показывается ТОЛЬКО ОДИН РАЗ
```
### 8.2 Анализ документа через B2B
```bash
API_KEY="cc_abc123..."
curl -X POST -H "X-API-Key: $API_KEY" \
-F "file=@contract.pdf" \
http://localhost:8000/api/v1/analyze
# → 202 {"document_id": "...", "correlation_id": "..."}
```
### 8.3 Проверка usage
```bash
curl -H "X-API-Key: $API_KEY" http://localhost:8000/api/v1/b2b/usage
```
---
## 9. Резервное копирование
### 9.1 Postgres
```bash
# Ручной backup
docker compose exec postgres pg_dump -U contract_check -d contract_check \
> backup_$(date +%Y%m%d_%H%M%S).sql
# Автоматический backup (cron на хосте)
# 0 2 * * * cd /opt/contract-check && docker compose exec -T postgres pg_dump -U contract_check -d contract_check | gzip > backups/pg_$(date +\%Y\%m\%d).sql.gz
```
### 9.2 WAL-архивы (PITR-ready)
Postgres настроен с `wal_level=replica`, `archive_mode=on`.
WAL-сегменты пишутся в volume `pgwal` (mount `/walarchive`).
Для полноценного PITR настройте `pg_backrest` или репликацию (см. `ARCHITECTURE.md §10`).
### 9.3 MinIO
MinIO хранит raw-документы и extracted `.txt`. ILM-правило истекает объекты
через `DOC_RETENTION_DAYS` (по умолчанию 7 дней). **Отчёты живут в Postgres** — они
выживут после истечения raw-документов. Backup MinIO не обязателен для бизнес-логики.
---
## 10. Обновление
### 10.1 Rolling update (без остановки всего)
```bash
# 1. Pull изменений
git pull origin main
# 2. Rebuild + recreate (Compose пересоздаёт только изменённые контейнеры)
docker compose --profile services up -d --build
# 3. Миграции (если есть новые)
docker compose --profile services run --rm api alembic upgrade head
# 4. Проверка:
curl http://localhost:8000/readyz
```
### 10.2 Graceful shutdown workers
Workers получают `SIGTERM` → завершают текущее сообщение → `SIGKILL` после grace period.
Compose `stop_grace_period` по умолчанию 10s; для долгих контрактов можно увеличить.
---
## 11. Масштабирование
### 11.1 На одном VPS (вертикальное)
- Увеличьте `MQ_PREFETCH_EXTRACT` до числа CPU
- Увеличьте `MQ_PREFETCH_ANALYZE` до конкарренси Ollama (3 на Pro, 10 на Max)
- Масштабируйте RAM под размер контрактов
### 11.2 Горизонтальное (несколько worker-ов)
```yaml
# docker-compose.override.yml
services:
worker-extract:
deploy:
replicas: 2
worker-analyze:
deploy:
replicas: 2
```
```bash
docker compose --profile services up -d --scale worker-extract=2 --scale worker-analyze=2
```
> Workers stateless — горизонтальное масштабирование бесплатно. Единственное ограничение:
> конкарренси Ollama Cloud (3 на Pro). Не масштабируйте `worker-analyze` выше,
> чем позволяет квота провайдера — иначе получите 429.
### 11.3 Многонодовая HA
См. `ARCHITECTURE.md §10`. Путь к HA — только инфраструктурный (Compose → кластер
RabbitMQ 3 nodes + Patroni Postgres + distributed MinIO). **Код `core/` не меняется.**
---
## 12. Troubleshooting
### 12.1 `docker compose up` зависает — сервисы не стартуют
**Причина:** `depends_on: condition: service_healthy` — кто-то не прошёл healthcheck.
```bash
# Диагностика:
docker compose ps
docker compose logs <service>
# Частые причины:
# - Postgres ещё не готов: подождите 15-20s после первого запуска
# - minio-init не выполнился: проверьте docker compose logs minio-init
# - RabbitMQ не отвечает: docker compose logs rabbitmq
```
### 12.2 `POST /documents` → 402 Payment Required
Пользователю не хватает `credits_left`. В dev добавьте вручную:
```bash
docker compose exec postgres psql -U contract_check -d contract_check \
-c "UPDATE users SET credits_left = credits_left + 1 WHERE telegram_id = <id>;"
```
### 12.3 Worker-extract падает с `ExtractionError`
Документ — скан/PDF без текстового слоя. Worker должен автоматически перейти к OCR.
Если и OCR падает — `documents.status=failed`, credit refunded (если refundable).
Проверьте:
```bash
docker compose --profile services logs worker-extract
# Или в Postgres:
docker compose exec postgres psql -U contract_check -d contract_check \
-c "SELECT id, status, stage, last_failure_class FROM documents ORDER BY created_at DESC LIMIT 5;"
```
### 12.4 Worker-analyze: постоянные 429 / `llm_quota`
- Превышена квота Ollama Cloud. Проверьте usage в dashboard ollama.com.
- Уменьшите `MQ_PREFETCH_ANALYZE` или перейдите на Enterprise тариф.
- Проверьте fallback-model (`OLLAMA_FALLBACK_MODEL`) — он должен сработать на 429.
### 12.5 Bot: `Unauthorized` / 401
- `BOT_SERVICE_TOKEN` не совпадает с `service_tokens.token_hash` в БД.
- Бот не смог получить user JWT через `/api/v1/auth/telegram/bot` (проверьте логи бота и api).
- `TELEGRAM_BOT_TOKEN` / `JWT_SECRET` не заданы в `.env` для сервиса `api`.
- Пересоздайте токен через §5.2.
### 12.6 Бот не отвечает
```bash
# Проверка логов
docker compose --profile services logs -f bot
# Проверка polling:
# Бот использует polling по умолчанию (aiogram). Если webhook установлен —
# убедитесь, что Nginx проксирует /webhook к api:8000.
```
## 13. Edge proxy (Nginx + certbot)
### 13.1 Профиль `edge`
Профиль поднимает Nginx (443/80) и certbot-renewal sidecar.
```bash
# .env
NGINX_SERVER_NAME=contract-check.example.com
```
> **Важно:** nginx зависит от `api` (`service_healthy`), а `api` находится в
> профиле `services` — поэтому nginx/certbot всегда запускайте с обоими
> профилями: `docker compose --profile services --profile edge ...`.
> Запуск только `--profile edge` падает с `service "nginx" depends on
> undefined service "api"`.
```bash
# Запуск edge вместе со стеком (после получения первого сертификата, см. §13.2)
docker compose --profile services --profile edge up -d
```
### 13.2 Первый запуск и получение сертификата
DNS A-запись должна указывать на IP сервера **до** запуска certbot.
`NGINX_SERVER_NAME` в `.env` должен совпадать с доменом.
Nginx **не может стартовать без сертификата** — рендеренный конфиг ссылается на
`/etc/letsencrypt/live/<domain>/fullchain.pem`, и nginx падает на старте, если
файла нет. Поэтому первый сертификат получается в `--standalone` режиме
(certbot сам слушает порт 80, nginx в этот момент не запущен), и только затем
nginx стартует. Всё это делает `certbot-init.sh`:
```bash
# 1. Запускаем основной стек (nginx пока НЕ стартуем)
docker compose --profile services up -d
# 2. Получаем первый сертификат (standalone, порт 80) и стартуем nginx + renew-sidecar
chmod +x deploy/nginx/certbot-init.sh
./deploy/nginx/certbot-init.sh contract-check.example.com admin@example.com
# 3. Проверяем
curl https://contract-check.example.com/healthz
```
При повторном запуске скрипт делает renew через webroot (nginx уже отдаёт
челленджи) и `nginx -s reload`.
По умолчанию `nginx.conf` проксирует:
- `/api/v1/*`, `/admin/*`
- `/healthz`, `/readyz`
- `/metrics` (открыт наружу; закройте файрволом или уберите приватный скрейпинг)
- `/webhook/*``/api/v1/webhooks/`
- `/grafana/*``grafana:3000` (когда поднят профиль `obs`)
Grafana под путём `/grafana`:
```bash
# Открыть логи через основной домен (нужен профиль obs + edge)
docker compose --profile services --profile obs --profile edge up -d
# https://contract-check.example.com/grafana/d/contract-check-logs
```
> **Безопасность:** Grafana под `/grafana` доступна всем, у кого есть доступ к домену.
> На проде добавьте basic auth, IP whitelist или вынесите Grafana на отдельный
> поддомен с отдельным ingress/VPN.
### 13.3 Обновление сертификата
Certbot-контейнер уже запущен с cron-подобным циклом `renew` каждые 12 часов.
Проверка вручную:
```bash
docker compose --profile edge exec certbot certbot renew --dry-run
docker compose --profile edge exec nginx nginx -s reload
```
### 13.4 MinIO: файлы не видны / bucket не создан
```bash
# Ручной init (если minio-init не отработал)
docker compose run --rm minio-init
# Или проверьте через mc:
docker compose run --rm minio-init mc ls local/
```
### 13.5 Миграции: `alembic` не видит таблицы / revision conflict
```bash
# Проверка текущей HEAD:
docker compose --profile services run --rm api alembic current
# Принудительный upgrade (осторожно — только dev!):
docker compose --profile services run --rm api alembic stamp head
```
### 13.6 Полный сброс (dev only!)
```bash
# Удалить ВСЕ данные (тома + контейнеры):
docker compose --profile services down -v
docker compose down -v
# Затем пересоздать с нуля: §2 + §5
```
---
## 14. Telegram-бот на отдельном сервере
Бот изначально спроектирован как тонкий HTTP-адаптер: он не импортирует `core.db`/`core.s3`/`core.mq`/`core.llm` и не хранит состояние. Поэтому его можно вынести на отдельный хост без изменения кода — только настроив `API_URL` на центральный API.
Типичные причины вынести бота:
- **Безопасность**: на ботовом сервере нет секретов БД/MinIO/RabbitMQ/LLM.
- **Сетевая изоляция**: боту нужен только outbound HTTPS к Telegram и к API.
- **Масштабирование**: можно запустить несколько ботов за одним токеном (aiogram polling не допускает несколько активных polling-инстансов с одним токеном; для HA используйте webhook + балансировку, см. §14.4).
### 14.1 Сетевая связность
| Направление | Протокол | Порт | Комментарий |
|---|---|---|---|
| bot → Telegram | outbound HTTPS | 443 | Polling updates от `api.telegram.org` |
| bot → API | HTTPS (рекомендуется) | 443 / 8000 | `API_URL` без trailing slash, например `https://contract-check.example.com` |
| bot → Redis (опц.) | Redis | 6379 | Только внутри приватной сети/VPN; иначе оставьте `REDIS_URL` пустым |
На firewall ботового сервера открывайте только **outbound** 443 (и 22 для SSH). Входящих портов бот не требует.
### 14.2 Подготовка центрального API
Убедитесь, что API доступен ботовому серверу по домену/IP и что `service_tokens` содержит запись для бота (§5.2):
```bash
# На сервере с API — убедитесь, что токен засеян:
docker compose --profile services exec api python -m src.contract_check.api seed-token bot-prod bot
```
Если API закрыт файрволом по IP, разрешите входящие от IP ботового сервера на порт API/Nginx.
### 14.3 Развёртывание бота на отдельном сервере
1. Склонируйте репозиторий на ботовый сервер (нужны только `srv/bot/Dockerfile`, `docker-compose.bot.yml`, `pyproject.toml`, `uv.lock`, `src/`, `.env.bot.example`).
2. Создайте `.env` из `.env.bot.example` с минимальным набором переменных:
```bash
cp .env.bot.example .env
# отредактируйте .env
```
```bash
ENV=prod
LOG_LEVEL=INFO
LOG_FORMAT=json
BOT_TOKEN=123456789:ABCDEF... # тот же токен из @BotFather
BOT_SERVICE_TOKEN=bot-prod-secret-xxx # должен совпадать с service_tokens.name='bot-prod'
API_URL=https://contract-check.example.com # публичный адрес центрального API, без trailing slash
REDIS_URL= # пусто → in-memory rate limiter; достаточно для одного инстанса
```
3. Запустите:
```bash
make bot-remote-up
# или напрямую:
docker compose -f docker-compose.bot.yml up -d --build
```
4. Проверьте логи:
```bash
make bot-remote-logs
```
Ожидаемое сообщение:
```
bot_started username=<...> api_url=https://contract-check.example.com
```
5. Проверьте healthcheck контейнера:
```bash
docker compose -f docker-compose.bot.yml ps
docker compose -f docker-compose.bot.yml exec bot python -c \
"import urllib.request, os; print(urllib.request.urlopen(os.environ['API_URL'] + '/healthz').read())"
```
### 14.4 Webhook вместо polling (опционально, для HA)
По умолчанию бот использует **polling**. Два и более инстанса с одним токеном в polling-режиме будут конфликтовать (Telegram отдаёт updates только одному соединению). Для высокой доступности:
1. Переведите бота на webhook: укажите публичный URL, за который отвечает Nginx, и проксируйте запросы на ботовый сервер.
2. В боте реализуйте webhook-эндпоинт (не входит в текущую версию; требуется небольшая доработка `bot/__main__.py` и добавление HTTP-сервера, например aiohttp/uvicorn).
3. Используйте `docker-compose.bot.yml` с healthcheck и `deploy.replicas`, но за балансировщиком, который направляет Telegram-запросы на одну активную реплику (либо на все, если webhook-эндпоинт идемпотентен и дедуплицирует updates по `update_id`).
> Для большинства production-развёртываний polling на выделенном ботовом сервере достаточен.
### 14.5 Управление и обновление
```bash
# Пересобрать и перезапустить бота после git pull:
make bot-remote-up
# Остановить:
make bot-remote-down
# Посмотреть статус:
make bot-remote-ps
```
---
## Чек-лист перед production
- [ ] `.env` заполнен, `.env.example` не содержит реальных секретов
- [ ] `POSTGRES_PASSWORD`, `RABBITMQ_PASS`, `S3_SECRET_KEY` — strong random
- [ ] `.env` заполнен (включая `TELEGRAM_BOT_TOKEN`, `JWT_SECRET`)
- [ ] `BOT_SERVICE_TOKEN` засеян в `service_tokens`
- [ ] Миграции накатаны (`alembic upgrade head`)
- [ ] `docker compose --profile services ps` показывает все healthy
- [ ] `/healthz` и `/readyz` отвечают 200
- [ ] Telegram-бот отвечает на `/start`
- [ ] Тестовый PDF проходит pipeline: upload → extract → analyze → report
- [ ] B2B-ключ создаётся через `/api/v1/b2b/keys` с user JWT
- [ ] Backup cron настроен
- [ ] Firewall: открыты только 443 (nginx), 22 (ssh), 15672 (RabbitMQ mgmt, restrict IP)
- [ ] Если бот на отдельном сервере: `API_URL` указывает на центральный API, `BOT_SERVICE_TOKEN` засеян, ботовый сервер имеет outbound HTTPS
---
## Ссылки
- [ARCHITECTURE.md](ARCHITECTURE.md) — полная архитектура, схемы БД, RabbitMQ topology, конфиг
- [TICKETS.md](TICKETS.md) — текущие статусы задач
- [README.md](../README.md) — структура, quick start, API overview