817 lines
35 KiB
Markdown
817 lines
35 KiB
Markdown
# Руководство по развёртыванию «Контракт-чек»
|
||
|
||
> Целевая аудитория: 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) и бот
|
||
|
||
```bash
|
||
# api + workers (без бота):
|
||
docker compose --profile services up -d --build
|
||
|
||
# Telegram-бот отдельно (можно на этом же или на другом хосте):
|
||
docker compose --profile bot up -d --build
|
||
|
||
# Или сразу весь стек:
|
||
docker compose --profile services --profile bot up -d --build
|
||
|
||
# Ждём healthy:
|
||
docker compose --profile services --profile bot 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` | `bot` — Telegram-адаптер; можно поднять на этом же хосте или на отдельном сервере |
|
||
| `obs` | `loki`, `promtail`, `grafana`, `prometheus` (logs + metrics; traces — позже) |
|
||
| `observer` | `openobserve`, `otel-collector` (logs + metrics + traces через collector) |
|
||
| `edge` | `nginx`, `certbot` |
|
||
|
||
```bash
|
||
# Инфра:
|
||
docker compose up -d
|
||
|
||
# api + workers (без бота):
|
||
docker compose --profile services up -d --build
|
||
|
||
# Только бот (например, на отдельном сервере или после api + workers):
|
||
docker compose --profile bot up -d --build
|
||
|
||
# Полный стек на одном хосте:
|
||
docker compose --profile services --profile bot up -d --build
|
||
|
||
# + observability Grafana/Loki/Prometheus (логи + метрики):
|
||
docker compose --profile services --profile bot --profile obs up -d --build
|
||
|
||
# + observability OpenObserve (логи + метрики + трейсы через otel-collector):
|
||
docker compose --profile services --profile bot --profile observer up -d --build
|
||
|
||
# + edge (nginx reverse proxy + TLS; deploy/nginx готов):
|
||
docker compose --profile services --profile bot --profile observer --profile edge up -d --build
|
||
```
|
||
|
||
### 4.2 depends_on и healthchecks
|
||
|
||
`api` ждёт `postgres`, `rabbitmq`, `minio-init` (healthy / completed).
|
||
Workers ждут `postgres`, `rabbitmq`, `minio-init`.
|
||
`bot` не имеет `depends_on` — он находится в отдельном профиле `bot` и может
|
||
запускаться на другом хосте. На старте бот проверяет доступность API через
|
||
встроенный healthcheck и перезапускается при необходимости (`restart: unless-stopped`).
|
||
|
||
---
|
||
|
||
## 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 bot restart bot
|
||
# Проверка логов:
|
||
docker compose --profile bot 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 bot up -d --build
|
||
docker compose --profile bot 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 api + workers (Compose пересоздаёт только изменённые контейнеры)
|
||
docker compose --profile services up -d --build
|
||
|
||
# 3. Пересоздать бота (если он запущен на этом хосте)
|
||
docker compose --profile bot up -d --build
|
||
|
||
# 4. Миграции (если есть новые)
|
||
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 bot logs -f bot
|
||
|
||
# Если бот на отдельном сервере — смотрите логи там:
|
||
# docker compose -f docker-compose.bot.yml logs -f
|
||
|
||
# Проверка 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 всегда запускайте хотя бы с
|
||
> профилями `services` и `edge`. Бот (`profile: bot`) можно добавить по желанию.
|
||
> Запуск только `--profile edge` падает с `service "nginx" depends on
|
||
> undefined service "api"`.
|
||
|
||
```bash
|
||
# Запуск edge вместе со стеком api + workers (без бота):
|
||
docker compose --profile services --profile edge up -d
|
||
|
||
# Запуск edge вместе с полным стеком (api + workers + бот на этом хосте):
|
||
docker compose --profile services --profile bot --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 пока НЕ стартуем).
|
||
# Добавьте --profile bot, если бот должен работать на этом же хосте.
|
||
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
|
||
# Открыть логи через основной домен (нужны профили services/bot/obs/edge)
|
||
docker compose --profile services --profile bot --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 --profile bot 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=redis://redis:6379/0 # локальный Redis из docker-compose.bot.yml
|
||
```
|
||
|
||
`docker-compose.bot.yml` поднимает собственный Redis-контейнер (только для
|
||
rate-limit, не для очереди). Чтобы использовать in-memory бэкенд, задайте
|
||
`REDIS_URL=` (пустое значение).
|
||
|
||
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 --profile bot ps` показывает все healthy (или `services` — если бот не на этом хосте)
|
||
- [ ] `/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
|