# Руководство по развёртыванию «Контракт-чек» > Целевая аудитория: 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) --- ## 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 + bot) ```bash docker compose --profile services up -d --build # Ждём healthy: docker compose --profile services ps ``` --- ## 3. Конфигурация через `.env` Все секреты — в `.env` (не коммитить!). Ключевые переменные: ```bash # --- LLM (обязательно) --- OLLAMA_HOST=https://api.ollama.com OLLAMA_API_KEY=sk-xxxxxxxx OLLAMA_MODEL=qwen2.5:14b OLLAMA_FALLBACK_MODEL=qwen2.5:7b # --- Telegram + auth (обязательно) --- BOT_TOKEN=123456789:ABCDEF... # для aiogram бота TELEGRAM_BOT_TOKEN=$BOT_TOKEN # тот же токен; API использует для проверки Login Widget / Mini App BOT_SERVICE_TOKEN=bot-prod-secret-xxx # см. §5.3 JWT_SECRET=$(openssl rand -hex 32) # HS256 secret для подписи JWT # --- Postgres (можно оставить defaults для dev) --- 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 # --- Observability (опционально) --- SENTRY_DSN=https://...@sentry.io/... OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 ``` > Полный список: см. `.env.example` и `ARCHITECTURE.md §11`. --- ## 4. Docker Compose — полный стек ### 4.1 Профили | Профиль | Что поднимает | |---|---| | *(default)* | `postgres`, `redis`, `rabbitmq`, `minio`, `minio-init` | | `services` | `api`, `worker-extract`, `worker-analyze`, `bot` | | `obs` *(планируется)* | `prometheus`, `grafana`, `otel-collector`, `tempo` | | `edge` *(планируется)* | `nginx`, `certbot` | ```bash # Инфра: docker compose up -d # + сервисы (сборка + запуск): docker compose --profile services up -d --build # + observability + edge (когда будут готовы конфиги в deploy/): # docker compose --profile services --profile obs --profile edge up -d --build ``` ### 4.2 depends_on и healthchecks `api` ждёт `postgres`, `rabbitmq`, `minio-init` (healthy / completed). `bot` ждёт только `api` (healthy) — не инфра напрямую, соблюдая hexagonal boundary. Workers ждут `postgres`, `rabbitmq`, `minio-init`. --- ## 5. Первичная инициализация ### 5.1 Миграции ```bash # Способ A: локально (если uv установлен) uv run alembic upgrade head # Способ B: через api-контейнер (если uv недоступен на хосте) docker compose --profile services run --rm api alembic upgrade head ``` Проверка: ```bash docker compose exec postgres psql -U contract_check -d contract_check -c "\dt" # Должны быть: api_key_requests, api_keys, documents, invoices, jobs, reports, service_tokens, users ``` ### 5.2 Seed-token для адаптеров (bot / web / cli) Бот аутентифицируется в api через `Authorization: Bearer `. Этот токен нужно положить в `service_tokens`. ```bash # Генерируем секрет BOT_SERVICE_TOKEN=$(openssl rand -hex 32) echo "BOT_SERVICE_TOKEN=$BOT_SERVICE_TOKEN" >> .env # Записываем hash в БД через временный api-контейнер docker compose --profile services run --rm api \ python -c " import asyncio, os, sys os.chdir('/app') sys.path.insert(0, 'src') from contract_check.core.config import get_settings from contract_check.core.tokens import hash_token from contract_check.core.db.session import create_session_factory from sqlalchemy import text async def seed(): factory = create_session_factory() async with factory() as s: h = hash_token('$BOT_SERVICE_TOKEN') await s.execute(text('INSERT INTO service_tokens (name, token_hash, adapter) VALUES (:n, :h, :a) ON CONFLICT (name) DO UPDATE SET token_hash = EXCLUDED.token_hash'), {'n': 'bot-prod', 'h': h, 'a': 'bot'}) await s.commit() print('seeded bot-prod') asyncio.run(seed()) " ``` > В production используйте `python -m contract_check.api seed-token` CLI (если реализовано). ### 5.3 Перезапуск бота с новым токеном ```bash docker compose --profile services restart bot # Проверка логов: docker compose --profile services logs -f bot ``` --- ## 6. Проверка работоспособности ### 6.1 Health endpoints ```bash curl http://localhost:8000/healthz # liveness — 200 curl http://localhost:8000/readyz # readiness — 200 (PG + Rabbit + MinIO) curl http://localhost:8000/metrics # Prometheus exposition ``` ### 6.2 RabbitMQ Management UI Откройте `http://localhost:15672` (guest/guest или credentials из `.env`). Проверьте: - Exchange `contracts.x` (direct) - Queues: `extract.q`, `analyze.q` (quorum), `extract.retry.q`, `analyze.retry.q`, DLQ - Connections/consumers от worker-ов ### 6.3 MinIO Console `http://localhost:9001` (root user = `S3_ACCESS_KEY` / `S3_SECRET_KEY`). Бакет `contract-check-docs` должен появиться после `minio-init`. ### 6.4 End-to-end smoke-тест (через curl) ```bash # 1. Сначала нужен JWT пользователя. В dev можно обменять telegram_id через # service-token endpoint /auth/telegram/bot (в production этим занимается бот): curl -X POST -H "Authorization: Bearer $BOT_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"telegram_id": 123456}' \ http://localhost:8000/api/v1/auth/telegram/bot # → {"access_token":"eyJ...", "user_id":"...", "telegram_id":123456, ...} USER_JWT="eyJ..." # скопируйте access_token # 2. Проверить /me curl -H "Authorization: Bearer $USER_JWT" http://localhost:8000/api/v1/me # → {"telegram_id":123456, "credits_left":0} # 3. Загрузить документ (нужен credit — пока 0, но проверим путь) curl -X POST -H "Authorization: Bearer $USER_JWT" \ -F "file=@contract.pdf" http://localhost:8000/api/v1/documents # → 402 Payment Required (нормально — нет кредитов) # 4. Добавить кредит вручную (dev): docker compose exec postgres psql -U contract_check -d contract_check \ -c "UPDATE users SET credits_left = 10 WHERE telegram_id = 123456;" ``` --- ## 7. Telegram-бот: первый запуск ### 7.1 Создание бота в BotFather 1. Напишите `@BotFather` → `/newbot` 2. Скопируйте токен (`BOT_TOKEN`) в `.env` 3. Установите webhook (опционально, polling работает по умолчанию): ```bash curl -F "url=https://your-domain.com/webhook" \ https://api.telegram.org/bot$BOT_TOKEN/setWebhook ``` ### 7.2 Запуск ```bash docker compose --profile services up -d bot docker compose --profile services logs -f bot ``` Ожидаемый вывод при `/start`: ``` Привет! Я «Контракт-чек» — первичный скрининг рисков в договорах... Осталось проверок: 0. ``` ### 7.3 Добавление кредитов пользователю ```bash # Найти telegram_id пользователя (из логов бота или таблицы users) docker compose exec postgres psql -U contract_check -d contract_check \ -c "UPDATE users SET credits_left = credits_left + 5 WHERE telegram_id = ;" ``` --- ## 8. B2B API: первый API-ключ ### 8.1 Создание ключа (через user JWT) Управление B2B-ключами теперь требует пользовательский JWT. Получите JWT через `/auth/telegram/bot` (как в §6.4) и выполните: ```bash curl -X POST -H "Authorization: Bearer $USER_JWT" \ -H "Content-Type: application/json" \ -d '{"name": "integration-test", "rate_limit_rps": 3, "monthly_quota": 100}' \ http://localhost:8000/api/v1/b2b/keys # → {"api_key": "cc_abc123...", "id": "...", ...} # Сохраните api_key — он показывается ТОЛЬКО ОДИН РАЗ ``` ### 8.2 Анализ документа через B2B ```bash API_KEY="cc_abc123..." curl -X POST -H "X-API-Key: $API_KEY" \ -F "file=@contract.pdf" \ http://localhost:8000/api/v1/analyze # → 202 {"document_id": "...", "correlation_id": "..."} ``` ### 8.3 Проверка usage ```bash curl -H "X-API-Key: $API_KEY" http://localhost:8000/api/v1/b2b/usage ``` --- ## 9. Резервное копирование ### 9.1 Postgres ```bash # Ручной backup docker compose exec postgres pg_dump -U contract_check -d contract_check \ > backup_$(date +%Y%m%d_%H%M%S).sql # Автоматический backup (cron на хосте) # 0 2 * * * cd /opt/contract-check && docker compose exec -T postgres pg_dump -U contract_check -d contract_check | gzip > backups/pg_$(date +\%Y\%m\%d).sql.gz ``` ### 9.2 WAL-архивы (PITR-ready) Postgres настроен с `wal_level=replica`, `archive_mode=on`. WAL-сегменты пишутся в volume `pgwal` (mount `/walarchive`). Для полноценного PITR настройте `pg_backrest` или репликацию (см. `ARCHITECTURE.md §10`). ### 9.3 MinIO MinIO хранит raw-документы и extracted `.txt`. ILM-правило истекает объекты через `DOC_RETENTION_DAYS` (по умолчанию 7 дней). **Отчёты живут в Postgres** — они выживут после истечения raw-документов. Backup MinIO не обязателен для бизнес-логики. --- ## 10. Обновление ### 10.1 Rolling update (без остановки всего) ```bash # 1. Pull изменений git pull origin main # 2. Rebuild + recreate (Compose пересоздаёт только изменённые контейнеры) docker compose --profile services up -d --build # 3. Миграции (если есть новые) docker compose --profile services run --rm api alembic upgrade head # 4. Проверка: curl http://localhost:8000/readyz ``` ### 10.2 Graceful shutdown workers Workers получают `SIGTERM` → завершают текущее сообщение → `SIGKILL` после grace period. Compose `stop_grace_period` по умолчанию 10s; для долгих контрактов можно увеличить. --- ## 11. Масштабирование ### 11.1 На одном VPS (вертикальное) - Увеличьте `MQ_PREFETCH_EXTRACT` до числа CPU - Увеличьте `MQ_PREFETCH_ANALYZE` до конкарренси Ollama (3 на Pro, 10 на Max) - Масштабируйте RAM под размер контрактов ### 11.2 Горизонтальное (несколько worker-ов) ```yaml # docker-compose.override.yml services: worker-extract: deploy: replicas: 2 worker-analyze: deploy: replicas: 2 ``` ```bash docker compose --profile services up -d --scale worker-extract=2 --scale worker-analyze=2 ``` > Workers stateless — горизонтальное масштабирование бесплатно. Единственное ограничение: > конкарренси Ollama Cloud (3 на Pro). Не масштабируйте `worker-analyze` выше, > чем позволяет квота провайдера — иначе получите 429. ### 11.3 Многонодовая HA См. `ARCHITECTURE.md §10`. Путь к HA — только инфраструктурный (Compose → кластер RabbitMQ 3 nodes + Patroni Postgres + distributed MinIO). **Код `core/` не меняется.** --- ## 12. Troubleshooting ### 12.1 `docker compose up` зависает — сервисы не стартуют **Причина:** `depends_on: condition: service_healthy` — кто-то не прошёл healthcheck. ```bash # Диагностика: docker compose ps docker compose logs # Частые причины: # - 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 services logs -f bot # Проверка polling: # Бот использует polling по умолчанию (aiogram). Если webhook установлен — # убедитесь, что Nginx проксирует /webhook к api:8000. ``` ### 12.7 MinIO: файлы не видны / bucket не создан ```bash # Ручной init (если minio-init не отработал) docker compose run --rm minio-init # Или проверьте через mc: docker compose run --rm minio-init mc ls local/ ``` ### 12.8 Миграции: `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 ``` ### 12.9 Полный сброс (dev only!) ```bash # Удалить ВСЕ данные (тома + контейнеры): docker compose --profile services down -v docker compose down -v # Затем пересоздать с нуля: §2 + §5 ``` --- ## Чек-лист перед production - [ ] `.env` заполнен, `.env.example` не содержит реальных секретов - [ ] `POSTGRES_PASSWORD`, `RABBITMQ_PASS`, `S3_SECRET_KEY` — strong random - [ ] `.env` заполнен (включая `TELEGRAM_BOT_TOKEN`, `JWT_SECRET`) - [ ] `BOT_SERVICE_TOKEN` засеян в `service_tokens` - [ ] Миграции накатаны (`alembic upgrade head`) - [ ] `docker compose --profile services ps` показывает все healthy - [ ] `/healthz` и `/readyz` отвечают 200 - [ ] Telegram-бот отвечает на `/start` - [ ] Тестовый PDF проходит pipeline: upload → extract → analyze → report - [ ] B2B-ключ создаётся через `/api/v1/b2b/keys` с user JWT - [ ] Backup cron настроен - [ ] Firewall: открыты только 443 (nginx), 22 (ssh), 15672 (RabbitMQ mgmt, restrict IP) --- ## Ссылки - [ARCHITECTURE.md](ARCHITECTURE.md) — полная архитектура, схемы БД, RabbitMQ topology, конфиг - [TICKETS.md](TICKETS.md) — текущие статусы задач - [README.md](../README.md) — структура, quick start, API overview