Ticket 10 verification surfaced runtime issues that static validation
missed; all verified against running Docker stack (Vector 0.43 + OO 0.92.2):
- Rewrite VRL for Vector 0.43: no coalesce/default funcs, merge!/replace!
for guarded fallible calls, no two-value parse_json destructuring
- Quote env-interpolated sink credentials (empty env left a bare YAML null)
- Fix logs sink URI: OO needs /api/{org}/{stream}/_json (stream segment
was missing, causing 404); logs now land in contract_check stream
- Replace OPENOBSERVE_AUTH_TOKEN with root email/password Basic auth:
prometheus_remote_write ignores request.headers, so remote-write got 401
- Update .env.example, DEPLOY.md, ARCHITECTURE.md, ticket 05 accordingly
Verified live: logs with service + correlation_id searchable in
OpenObserve; contract_check_* metrics queryable via its Prometheus API;
no outbound port 4318 connections from app containers.
41 KiB
Руководство по развёртыванию «Контракт-чек»
Целевая аудитория: DevOps / разработчик, поднимающий стек на одном VPS или локально. Предполагается: Docker + Docker Compose установлены, есть SSH-доступ к VPS. Полная архитектура:
ARCHITECTURE.md.
Содержание
- Требования
- Быстрый старт локально
- Конфигурация через
.env - Docker Compose — полный стек
- Первичная инициализация
- Проверка работоспособности
- Telegram-бот: первый запуск
- B2B API: первый API-ключ
- Резервное копирование
- Обновление (zero-downtime-ish)
- Масштабирование
- Troubleshooting
- Edge proxy (Nginx + certbot)
- 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
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)
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 Миграции
# Локально (нужен 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) и бот
# 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 (не коммитить!). Ключевые переменные:
# --- 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/...
# Vector (профиль `observer`) авторизуется в OpenObserve с этими кредами
# (совпадают с root-пользователем OpenObserve).
OPENOBSERVE_ROOT_USER_EMAIL=root@example.com
OPENOBSERVE_ROOT_USER_PASSWORD=Complexpass#123
# OTEL_EXPORTER_OTLP_ENDPOINT больше не используется: приложение не шлёт OTLP.
Полный список: см.
.env.exampleиARCHITECTURE.md §11. Включение платежей (ЮKassa) — §4a ниже.
4a. ЮKassa: включение платежей
-
Получите
shopIdи секретный ключ в личном кабинете ЮKassa (Настройки → API-ключи; для старта можно «тестовый» магазин). -
Пропишите в
.env: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 -
В личном кабинете ЮKassa настройте HTTP-уведомления (webhook) на
https://<ваш-домен>/api/v1/webhooks/yookassaдля событийpayment.succeeded,payment.canceled,refund.succeeded. API аутентифицирует уведомления по Basic auth (shopId:secretKey) и всегда перезапрашивает платёж через REST — сумме из push не доверяем. -
docker compose --profile services up -d --build— изменения подхватятся без правок compose (12-factor). -
Поднимите
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 напрямую (цены — в копейках):
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, vector (logs + metrics; приложение не шлёт OTLP) |
edge |
nginx, certbot |
# Инфра:
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 (логи + метрики через Vector; приложение не шлёт OTLP):
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 Миграции
# Способ A: локально (если uv установлен)
uv run alembic upgrade head
# Способ B: через api-контейнер (если uv недоступен на хосте)
docker compose --profile services run --rm api alembic upgrade head
Проверка:
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.
# Генерируем секрет
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-tokenCLI (если реализовано).
5.3 Перезапуск бота с новым токеном
docker compose --profile bot restart bot
# Проверка логов:
docker compose --profile bot logs -f bot
6. Проверка работоспособности
6.1 Health endpoints
curl http://localhost:8000/healthz # liveness — 200
curl http://localhost:8000/readyz # readiness — 200 (PG + Rabbit + MinIO)
# /metrics доступен на loopback по умолчанию; снаружи хоста — только через
# edge/nginx или по туннелю. Для проверки внутри контейнера:
docker compose exec api curl -s http://localhost:8000/metrics
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)
# 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
- Напишите
@BotFather→/newbot - Скопируйте токен (
BOT_TOKEN) в.env - Режим доставки updates выбирается переменной
BOT_UPDATE_MODE: по умолчаниюpolling(ничего настраивать не нужно). Для webhook-режима смотрите §14.4 — вручную вызыватьsetWebhookчерез curl не нужно, бот сам регистрирует webhook на старте и снимает его при остановке.
7.2 Запуск
docker compose --profile bot up -d --build
docker compose --profile bot logs -f bot
Ожидаемый вывод при /start:
Привет! Я «Контракт-чек» — первичный скрининг рисков в договорах...
Осталось проверок: 0.
7.3 Добавление кредитов пользователю
# Найти 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) и выполните:
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
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
curl -H "X-API-Key: $API_KEY" http://localhost:8000/api/v1/b2b/usage
9. Резервное копирование
9.1 Postgres
# Ручной 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 (без остановки всего)
# 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-ов)
# docker-compose.override.yml
services:
worker-extract:
deploy:
replicas: 2
worker-analyze:
deploy:
replicas: 2
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.
# Диагностика:
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 добавьте вручную:
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).
Проверьте:
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 Бот не отвечает
# Проверка логов (если бот на этом хосте)
docker compose --profile bot logs -f bot
# Если бот на отдельном сервере — смотрите логи там:
# docker compose -f docker-compose.bot.yml logs -f
# Проверка режима доставки:
# Бот использует polling по умолчанию (BOT_UPDATE_MODE не задан или polling).
# В webhook-режиме проверьте: /healthz на ботовом сервере отвечает 200,
# edge маршрутизирует BOT_WEBHOOK_PATH на бот, секретный заголовок совпадает
# (см. §14.4.3).
13. Edge proxy (Nginx + certbot)
13.1 Профиль edge
Профиль поднимает Nginx (443/80) и certbot-renewal sidecar.
# .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".
# Запуск 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:
# 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→ проксируется наружу; настройтеMETRICS_BEARER_TOKENи ограничьте доступ файрволом/edge, чтобы метрики не утекали наружу. Worker-метрики доступны только внутри сети compose./webhook/*→/api/v1/webhooks/${BOT_WEBHOOK_PATH}→bot:8080(Telegram-бот в webhook-режиме, профильbot; см. §14.4)/grafana/*→grafana:3000(когда поднят профильobs)
Grafana под путём /grafana:
# Открыть логи через основной домен (нужны профили 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 часов.
Проверка вручную:
docker compose --profile edge exec certbot certbot renew --dry-run
docker compose --profile edge exec nginx nginx -s reload
13.4 MinIO: файлы не видны / bucket не создан
# Ручной init (если minio-init не отработал)
docker compose run --rm minio-init
# Или проверьте через mc:
docker compose run --rm minio-init mc ls local/
13.5 Миграции: alembic не видит таблицы / revision conflict
# Проверка текущей 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!)
# Удалить ВСЕ данные (тома + контейнеры):
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-вызовы api.telegram.org; в webhook-режиме — только API-вызовы |
| bot → API | HTTPS (рекомендуется) | 443 / 8000 | API_URL без trailing slash, например https://contract-check.example.com |
| bot → Redis (опц.) | Redis | 6379 | Только внутри приватной сети/VPN; иначе оставьте REDIS_URL пустым |
| edge → bot (webhook-режим) | HTTP | 8080 | Только с TLS-edge (nginx) на приватном интерфейсе/loopback; наружу порт не публикуется |
На firewall ботового сервера открывайте только outbound 443 (и 22 для SSH).
В polling-режиме входящих портов бот не требует. В webhook-режиме порт 8080
публикуется compose-файлом только на 127.0.0.1 (см. BOT_WEBHOOK_BIND_HOST)
— наружу бот доступен исключительно через TLS-edge.
14.2 Подготовка центрального API
Убедитесь, что API доступен ботовому серверу по домену/IP и что service_tokens содержит запись для бота (§5.2):
# На сервере с 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 Развёртывание бота на отдельном сервере
-
Склонируйте репозиторий на ботовый сервер (нужны только
srv/bot/Dockerfile,docker-compose.bot.yml,pyproject.toml,uv.lock,src/,.env.bot.example). -
Создайте
.envиз.env.bot.exampleс минимальным набором переменных:cp .env.bot.example .env # отредактируйте .envENV=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 BOT_UPDATE_MODE=polling # webhook-режим: см. §14.4docker-compose.bot.ymlподнимает собственный Redis-контейнер (только для rate-limit, не для очереди). Чтобы использовать in-memory бэкенд, задайтеREDIS_URL=(пустое значение). -
Запустите:
make bot-remote-up # или напрямую: docker compose -f docker-compose.bot.yml up -d --build -
Проверьте логи:
make bot-remote-logsОжидаемое сообщение:
bot_started username=<...> api_url=https://contract-check.example.com -
Проверьте healthcheck контейнера:
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 (встроенный режим)
Бот поддерживает два режима доставки updates — переключение одной переменной:
BOT_UPDATE_MODE=polling # по умолчанию: long-polling (локальная разработка)
BOT_UPDATE_MODE=webhook # продакшн: HTTP-сервер + регистрация webhook в Telegram
В webhook-режиме бот поднимает маленький aiohttp-сервер, который:
- принимает Telegram updates по секретному пути
/tg-webhook/<digest>и проверяет заголовокX-Telegram-Bot-Api-Secret-Token(сравнение в constant-time); неправильный или отсутствующий токен → 403 до диспетчеризации, поэтому подделанные updates не могут тратить кредиты пользователей; - отвечает
GET /healthz(200) — compose healthcheck и оркестратор перезапускают умерший процесс; - на старте регистрирует webhook (
set_webhookсdrop_pending_updates), при graceful shutdown снимает его (delete_webhook) — стейджинг и прод никогда не дерутся за один токен, и на ботовом сервере не остаётся устаревший webhook.
14.4.1 Переменные окружения webhook-режима
| Переменная | Обязательна | Описание |
|---|---|---|
BOT_UPDATE_MODE |
— | polling (default) / webhook |
BOT_WEBHOOK_PUBLIC_BASE_URL |
в webhook-режиме | Публичный https-базовый URL TLS-edge без trailing slash, напр. https://contract-check.example.com |
BOT_WEBHOOK_SECRET_TOKEN |
в webhook-режиме | Секрет для X-Telegram-Bot-Api-Secret-Token и деривации пути. Только A-Z a-z 0-9 _ -, до 256 символов; генерируйте openssl rand -hex 32 |
BOT_WEBHOOK_BIND_HOST |
— | Куда публиковать порт в docker-compose.bot.yml (default 127.0.0.1); используйте адрес приватного/VPN-интерфейса, не публичный |
BOT_WEBHOOK_PORT |
— | Хостовый порт webhook-сервера в docker-compose.bot.yml (default 8080; внутри контейнера всегда 8080) |
Polling-режим не требует ни одной из них и остаётся ровно тем же кодом, что и до появления webhook-режима.
14.4.2 Секретный путь и маршрутизация через edge
Путь вебхука вычисляется детерминированно из секретов — HMAC-SHA256(секрет, ключ = токен бота), первые 32 hex-символа:
make bot-webhook-path
# → /tg-webhook/81440d4113c56ead9fa711fce29bc371
Знание домена (или чтение шаблона nginx в репозитории) не раскрывает путь.
Telegram шлёт updates на BOT_WEBHOOK_PUBLIC_BASE_URL + этот путь, и даже
угадавший путь получает 403 без правильного секретного заголовка.
Настройка edge:
- Бот на том же хосте, что и стек (профили
bot+edge): задайте в.envBOT_WEBHOOK_PATH(значение изmake bot-webhook-path) — nginx уже умеет проксировать этот путь наbot:8080внутри Docker-сети. Порт наружу не публикуется: только 443 у edge. - Бот на отдельном сервере:
docker-compose.bot.ymlпубликует webhook-порт на127.0.0.1:${BOT_WEBHOOK_PORT:-8080}. Поставьте TLS-edge (nginx/caddy) на ботовом серверере, проксируйте секретный путь на127.0.0.1:8080и укажите этот домен вBOT_WEBHOOK_PUBLIC_BASE_URL. Альтернатива —BOT_WEBHOOK_BIND_HOSTс адресом интерфейса VPN.
Минимальный .env webhook-режима на ботовом сервере:
BOT_TOKEN=123456789:ABCDEF...
BOT_SERVICE_TOKEN=bot-prod-secret-xxx
API_URL=https://contract-check.example.com
BOT_UPDATE_MODE=webhook
BOT_WEBHOOK_PUBLIC_BASE_URL=https://bot.example.com
BOT_WEBHOOK_SECRET_TOKEN=<openssl rand -hex 32>
14.4.3 Проверка
# Healthcheck бота (webhook-режим): compose проверяет сам процесс бота
docker compose -f docker-compose.bot.yml ps # STATUS: Up (healthy)
# Ручная проверка /healthz
curl http://127.0.0.1:8080/healthz # → {"status": "ok"}
# Секретный путь без токена → 403
curl -i -X POST https://<edge>/tg-webhook/<digest> # → HTTP/1.1 403 Forbidden
Для HA за балансировщиком помните: webhook-эндпоинт обрабатывает update до ответа; при нескольких репликах балансировщик должен направлять запросы Telegram на активную реплику (Telegram сам ретраит неотвеченные updates).
Для большинства production-развёртываний polling на выделенном ботовом сервере достаточен; webhook-режим нужен при ограничительных сетях (где outbound long-polling нестабилен) и для healthcheck-надзора за процессом.
14.5 Управление и обновление
# Пересобрать и перезапустить бота после 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 - Если бот в webhook-режиме:
BOT_UPDATE_MODE=webhook,BOT_WEBHOOK_PUBLIC_BASE_URL/BOT_WEBHOOK_SECRET_TOKENзаданы, edge маршрутизируетBOT_WEBHOOK_PATHна бота, webhook-порт не опубликован в интернет
Ссылки
- ARCHITECTURE.md — полная архитектура, схемы БД, RabbitMQ topology, конфиг
- TICKETS.md — текущие статусы задач
- README.md — структура, quick start, API overview