DealDocumentScreening/docs/DEPLOY.md

35 KiB
Raw Blame History

Руководство по развёртыванию «Контракт-чек»

Целевая аудитория: DevOps / разработчик, поднимающий стек на одном VPS или локально. Предполагается: Docker + Docker Compose установлены, есть SSH-доступ к VPS. Полная архитектура: ARCHITECTURE.md.


Содержание

  1. Требования
  2. Быстрый старт локально
  3. Конфигурация через .env
  4. Docker Compose — полный стек
  5. Первичная инициализация
  6. Проверка работоспособности
  7. Telegram-бот: первый запуск
  8. B2B API: первый API-ключ
  9. Резервное копирование
  10. Обновление (zero-downtime-ish)
  11. Масштабирование
  12. Troubleshooting
  13. Edge proxy (Nginx + certbot)
  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

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/...
# 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:

    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 напрямую (цены — в копейках):

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
# Инфра:
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 Миграции

# Способ 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-token CLI (если реализовано).

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)
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)

# 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 работает по умолчанию):
    curl -F "url=https://your-domain.com/webhook" \
         https://api.telegram.org/bot$BOT_TOKEN/setWebhook
    

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:
# Бот использует polling по умолчанию (aiogram). Если webhook установлен —
# убедитесь, что Nginx проксирует /webhook к api:8000.

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 (открыт наружу; закройте файрволом или уберите приватный скрейпинг)
  • /webhook/*/api/v1/webhooks/
  • /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.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):

# На сервере с 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 с минимальным набором переменных:

    cp .env.bot.example .env
    # отредактируйте .env
    
    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. Запустите:

    make bot-remote-up
    # или напрямую:
    docker compose -f docker-compose.bot.yml up -d --build
    
  4. Проверьте логи:

    make bot-remote-logs
    

    Ожидаемое сообщение:

    bot_started username=<...> api_url=https://contract-check.example.com
    
  5. Проверьте 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 (опционально, для 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 Управление и обновление

# Пересобрать и перезапустить бота после 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 — полная архитектура, схемы БД, RabbitMQ topology, конфиг
  • TICKETS.md — текущие статусы задач
  • README.md — структура, quick start, API overview