# Руководство по развёртыванию «Контракт-чек» > Целевая аудитория: 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 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 `. Этот токен нужно положить в `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 = ;" ``` --- ## 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 # Частые причины: # - 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 = ;" ``` ### 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//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