# Инфраструктура разработки АРМ КА: PostgreSQL 16, NATS 2.10 (без JetStream, как на платформе), MinIO + бакет. # Запуск: cp .env.example .env && docker compose up -d (из этого каталога) # Ожидание готовности: docker compose up -d --wait postgres nats minio. Голый `up -d --wait` возвращает код 1, потому что # одноразовый minio-init завершается (хотя и с кодом 0) — для скриптов указывайте сервисы явно. # Все учётные данные, порты хоста и имя бакета берутся из .env (значения по умолчанию — dev). # Порты публикуются только на 127.0.0.1; адрес привязки меняется через BIND_ADDR (см. .env.example). # Профиль `services` — сервисы системы, собранные из соседних репозиториев workspace (../../, Dockerfile в корне): # docker compose --profile services up -d --build # docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator # Одноразовые minio-init и arm-core-migrate (миграции + seed) в список --wait не входят: они завершаются с кодом 0. # Внутри контейнера каждый сервис слушает 8000; на хост публикуются 8000 (arm-core), 8001 (tabular-parser), 8002 (scan-gateway). # Фиксированное имя проекта: одинаковые тома и сети из любого worktree (.wt/<срез>/docs/dev). name: arm-ka # Общие переменные сервисов профиля `services` (формат шаблона команды; адреса — внутри сети compose, не localhost). x-service-env: &service-env DEBUG: "true" OPENBAO__MINIO__MINIO_URL: minio:9000 OPENBAO__MINIO__MINIO_ADMIN_LOGIN: ${MINIO_ROOT_USER:-minioadmin} OPENBAO__MINIO__MINIO_ADMIN_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} OPENBAO__MINIO__MINIO_BUCKET: ${MINIO_BUCKET:-arm-ka} OPENBAO__MINIO__MINIO_SECURE: "false" OPENBAO__NATS__NATS_URL: nats://nats:4222 NATS_ENABLED: "true" # arm-core и его одноразовые миграции: те же переменные плюс БД и dev-эмитент токенов. x-arm-core-env: &arm-core-env <<: *service-env OPENBAO__DATABASE__DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-armka}:${POSTGRES_PASSWORD:-armka}@postgres:5432/${POSTGRES_DB:-armka} # Dev-эмитент JWT (POST /api/v1/dev/token) — только при DEBUG=true; секрет публичный, только для dev. # Порт хоста по умолчанию на 127.0.0.1 (BIND_ADDR): при BIND_ADDR=0.0.0.0 токен может получить любой в сети. AUTH_DEV_ISSUER: "true" AUTH_DEV_SECRET: dev-only-secret-do-not-use-in-production-0123456789 services: postgres: image: postgres:16.10-alpine environment: POSTGRES_DB: ${POSTGRES_DB:-armka} POSTGRES_USER: ${POSTGRES_USER:-armka} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-armka} ports: - "${BIND_ADDR:-127.0.0.1}:${POSTGRES_PORT:-5432}:5432" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 3s timeout: 3s retries: 20 start_period: 5s # Core NATS без JetStream: флаг --jetstream не передаётся. Мониторинг — порт 8222 (/varz, /healthz). nats: image: nats:2.10.29-alpine command: ["-m", "8222"] ports: - "${BIND_ADDR:-127.0.0.1}:${NATS_PORT:-4222}:4222" - "${BIND_ADDR:-127.0.0.1}:${NATS_MONITOR_PORT:-8222}:8222" healthcheck: test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8222/healthz || exit 1"] interval: 3s timeout: 3s retries: 20 start_period: 2s # Образы minio/minio и minio/mc сняты с Docker Hub и quay.io; используется сборка alpine/minio с RELEASE-тегом. minio: image: alpine/minio:RELEASE.2025-10-15T17-29-55Z # Образ по умолчанию работает от пользователя minio и не может писать в свежий именованный том — в dev запускаем от root. user: "0:0" command: ["server", "/data", "--console-address", ":9001"] environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} ports: - "${BIND_ADDR:-127.0.0.1}:${MINIO_API_PORT:-9000}:9000" - "${BIND_ADDR:-127.0.0.1}:${MINIO_CONSOLE_PORT:-9001}:9001" volumes: - minio-data:/data healthcheck: test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:9000/minio/health/live || exit 1"] interval: 3s timeout: 3s retries: 20 start_period: 3s # Одноразовое создание бакета; идемпотентно (--ignore-existing), при повторном up снова завершается с кодом 0. minio-init: image: bitnamilegacy/minio-client:2025.7.21 depends_on: minio: condition: service_healthy restart: "no" environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} MINIO_BUCKET: ${MINIO_BUCKET:-arm-ka} entrypoint: ["/bin/sh", "-c"] command: - >- mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" && mc mb --ignore-existing "local/$$MINIO_BUCKET" # --- Профиль `services` ------------------------------------------------------------------------------------------- # Одноразово: миграции arm-core (alembic upgrade head) и справочники с dev-пользователями (seed --dev). # Оба шага идемпотентны; при каждом `up` профиля контейнер снова выполняется и завершается с кодом 0. # Образ собирает сервис arm-core (build только у него): две сборки одного тега дают разные ID образа, и каждый # следующий `up` пересоздавал бы arm-core-migrate и arm-core. pull_policy: never — образ локальный, не из реестра. arm-core-migrate: profiles: ["services"] image: arm-core:dev pull_policy: never restart: "no" depends_on: postgres: condition: service_healthy environment: *arm-core-env command: ["sh", "-c", "alembic upgrade head && python -m scripts.seed --dev"] arm-core: profiles: ["services"] build: context: ../../arm-core image: arm-core:dev restart: unless-stopped depends_on: postgres: condition: service_healthy nats: condition: service_healthy minio: condition: service_healthy minio-init: condition: service_completed_successfully arm-core-migrate: condition: service_completed_successfully environment: *arm-core-env ports: - "${BIND_ADDR:-127.0.0.1}:${ARM_CORE_PORT:-8000}:8000" healthcheck: # curl в python:3.11-slim нет; /healthcheck всегда отвечает 200, поэтому читаем поле nats test: - CMD - python - -c - "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' else 1)" interval: 5s timeout: 5s retries: 12 start_period: 10s tabular-parser: profiles: ["services"] build: context: ../../tabular-parser image: tabular-parser:dev restart: unless-stopped depends_on: nats: condition: service_healthy minio: condition: service_healthy minio-init: condition: service_completed_successfully environment: *service-env ports: - "${BIND_ADDR:-127.0.0.1}:${TABULAR_PARSER_PORT:-8001}:8000" healthcheck: test: - CMD - python - -c - "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' else 1)" interval: 5s timeout: 5s retries: 12 start_period: 10s # Эмулятор ИС Сканирование: тот же образ, что у шлюза (собирает сервис scan-gateway, см. arm-core-migrate); общий # том hotfolder с шлюзом. Переменные MinIO/NATS не нужны. # Фикстуры ответов ИС — ../../fixtures/ocr (только чтение). Входной файл шлюз называет . (имя файла # фикстуры не несёт); эмулятор считает sha256 содержимого и ищет пару PDF + набор <имя>.json / <имя>_*.json по хешу # (aep_3.pdf, aep_6.pdf, ...). Файл без пары по содержимому — ответ-ошибка INVALID_DOCUMENT, пока SCAN_EMULATOR_DEFAULT # пуст; чтобы такой файл получал набор по умолчанию, задайте SCAN_EMULATOR_DEFAULT=<имя набора>, например aep_6. # PDF из fixtures/ocr без JSON-набора (aep_4/7/9/30–34) остаются INVALID_DOCUMENT и при заданном default (OQ-151). scan-emulator: profiles: ["services"] image: scan-gateway:dev pull_policy: never command: ["python", "-m", "src.scan_emulator"] restart: unless-stopped environment: SCAN_HOTFOLDER_PATH: /data/hotfolder SCAN_FIXTURES_DIR: /fixtures/ocr SCAN_EMULATOR_DELAY: "2" # Набор по умолчанию для файлов без пары по содержимому. Пусто — такой файл получает INVALID_DOCUMENT. # Файл, совпавший по хешу с PDF без JSON-набора, default не получает (OQ-151). SCAN_EMULATOR_DEFAULT: "" volumes: - hotfolder:/data/hotfolder - ../../fixtures/ocr:/fixtures/ocr:ro scan-gateway: profiles: ["services"] build: context: ../../scan-gateway image: scan-gateway:dev restart: unless-stopped depends_on: nats: condition: service_healthy minio: condition: service_healthy minio-init: condition: service_completed_successfully environment: <<: *service-env SCAN_HOTFOLDER_MODE: local SCAN_HOTFOLDER_PATH: /data/hotfolder # Очистка Output// и входящего файла после чтения ответа (dev-том принадлежит нам). На шаре контура по # умолчанию выключена: права на удаление не подтверждены (OQ-144). SCAN_CLEANUP: "true" volumes: - hotfolder:/data/hotfolder ports: - "${BIND_ADDR:-127.0.0.1}:${SCAN_GATEWAY_PORT:-8002}:8000" healthcheck: # /healthcheck всегда отвечает 200, поэтому читаем поля nats и hotfolder test: - CMD - python - -c - "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' and d['hotfolder']=='ok' else 1)" interval: 5s timeout: 5s retries: 12 start_period: 10s volumes: postgres-data: minio-data: # Общая горячая папка scan-gateway и scan-emulator (Input/, Output/) hotfolder: