diff --git a/deploy/docker-compose.stage.yml b/deploy/docker-compose.stage.yml new file mode 100644 index 0000000..518710e --- /dev/null +++ b/deploy/docker-compose.stage.yml @@ -0,0 +1,92 @@ +# Stage-stack compose (test.admin.food-market.kz). +# +# Деплоится на prod-vm в ~/food-market-stage/deploy/docker-compose.yml. +# Запускается через ~/deploy-stage.sh на dev-vm (билдит из main → push :stage → +# ssh prod-vm `docker compose -p food-market-stage pull/up`). +# +# ВАЖНО: все stage-ресурсы НАЗВАНЫ отдельно от прод-стека (`food-market-stage-*` +# volume'ы, `/opt/food-market-stage-data` для uploads). Guard в ~/deploy-stage.sh +# и в quality-watchdog падает, если здесь появится прод-путь или прод-том — это +# третья утечка stage↔prod и мы её закрыли (см. docs/stage-prod-isolation-progress.md). +# +# Если нужно добавить сервис — делайте это тут и в прод-compose СИММЕТРИЧНО, +# но тома/бинды называйте RAW отдельно, без автодефолта от compose-project'а. + +services: + postgres: + image: ${REGISTRY:-127.0.0.1:5001}/mirror/postgres:16-alpine + container_name: food-market-stage-postgres + restart: unless-stopped + environment: + POSTGRES_DB: food_market + POSTGRES_USER: food_market + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-food_market_dev} + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "127.0.0.1:5435:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U food_market -d food_market"] + interval: 10s + timeout: 5s + retries: 5 + + api: + image: ${REGISTRY:-127.0.0.1:5001}/food-market-api:${API_TAG:-stage} + container_name: food-market-stage-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__Default: Host=postgres;Port=5432;Database=food_market;Username=food_market;Password=${POSTGRES_PASSWORD:-food_market_dev} + # На стенде issuer должен указывать на тестовый домен. Если здесь + # внезапно появится admin.food-market.kz — guard в deploy-stage.sh упадёт. + OpenIddict__Issuer: ${OPENIDDICT_ISSUER:-https://test.admin.food-market.kz/} + OpenIddict__CertPassword: ${OPENIDDICT_CERT_PASSWORD:-} + # Rate-limits на стенде всегда выше (e2e и smoke генерят много запросов). + RateLimiting__SignupPerIpPerHour: ${RATE_SIGNUP_HOUR:-5000} + RateLimiting__SignupPerIpPerDay: ${RATE_SIGNUP_DAY:-50000} + RateLimiting__PerIpPerMinute: ${RATE_PER_IP_MIN:-5000} + RateLimiting__PerIpPerHour: ${RATE_PER_IP_HOUR:-20000} + # Quick-login доступен ТОЛЬКО на стенде; на prod — False. + QuickLogin__Enabled: ${QUICK_LOGIN_ENABLED:-false} + QuickLogin__MasterPassword: ${QUICK_LOGIN_MASTER_PASSWORD:-123456} + # OCR invoice: Claude CLI via ssh на dev-vm. + Invoices__ClaudeCli__RemoteHost: ${INVOICE_REMOTE_HOST:-nns@192.168.1.192} + Invoices__ClaudeCli__Executable: /home/nns/.local/bin/claude + Invoices__ClaudeCli__SshKey: /app/.ssh/invoice-ocr-key + Invoices__ClaudeCli__SshKnownHosts: /tmp/.fm-ssh/known_hosts + ports: + - "8085:8080" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health/ready || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + volumes: + - api-data:/app/App_Data + - api-logs:/app/logs + - /opt/food-market-stage-data/uploads:/app/uploads + - ./secrets/invoice-ocr-key:/app/.ssh/invoice-ocr-key:ro + + web: + image: ${REGISTRY:-127.0.0.1:5001}/food-market-web:${WEB_TAG:-stage} + container_name: food-market-stage-web + restart: unless-stopped + depends_on: + api: + condition: service_healthy + ports: + - "8086:80" + +volumes: + postgres-data: + name: food-market-stage-postgres-data + api-data: + name: food-market-stage-api-data + api-logs: + name: food-market-stage-api-logs diff --git a/docs/stage-prod-isolation-progress.md b/docs/stage-prod-isolation-progress.md new file mode 100644 index 0000000..0b733f1 --- /dev/null +++ b/docs/stage-prod-isolation-progress.md @@ -0,0 +1,118 @@ +# stage ↔ prod isolation — progress + +**Задача:** stage-compose (`~/food-market-stage/deploy/docker-compose.yml` на prod-vm) делит с прод-compose +`/opt/food-market-data/uploads` и названные тома (`food-market-postgres-data`, `food-market-api-data`, +`food-market-api-logs`). Это третья утечка stage↔prod. Прод сейчас лежит — чиним до того, как его поднимут. + +Prod-vm: `nns@192.168.1.190`. Dev-vm: 192.168.1.193 (где этот репо и registry :5001). + +## Хронология утечек + +1. **До 22.09.2026** — stage писал в прод-БД (fixed, разнесли на отдельные контейнеры). +2. **Инцидент compose-проекта «deploy»** — одинаковое `-p deploy` снесло прод-контейнеры при stage-up (fixed, stage теперь `-p food-market-stage`). +3. **Сегодня (05.10.2026)** — общий bind-mount `/opt/food-market-data/uploads` + общие named volumes. + +## Checklist + +- [x] **1. uploads: стенд → /opt/food-market-stage-data/uploads.** Перенесена пустая `invoices/2026-10/` + (единственное «после августа»); остальное в прод-папке (13 export-zip Июн'26 + старые products) + оставлено как есть. Stage-compose на prod-vm переписан, в репозитории добавлен + `deploy/docker-compose.stage.yml`. +- [x] **2. Полный аудит общих ресурсов.** Таблица ниже. Разведены: uploads bind, 3 named volume, + container_name, project name. Issuer стенда теперь дефолтится на `test.admin.food-market.kz`. +- [x] **3. Guard в `~/deploy-stage.sh` + quality-watchdog.** `deploy-stage.sh` падает до билда, + если local/remote stage-compose ссылается на прод-ресурсы (uploads/volumes/container names/ + issuer). В `quality-watchdog.sh` добавлен step 10 (суточная каденция, 06:XX) — тот же набор + паттернов + `docker inspect` реального запущенного стенда. +- [x] **4. Smoke.** `/health/ready` Healthy, демо-admin логин, GET каталога 31 233 товара, invoice + upload → `parsing` → `failed` (OCR вернул не-JSON на 1×1 jpg — ожидаемо; исторические invoice + в restore'ом DB: `needs_review` с распарсенными `supplierName/invoiceNumber/totalSum`), + GDPR org-export → zip упал в `/opt/food-market-stage-data/uploads/exports//`. + Прод-папка `/opt/food-market-data/uploads/` до/после миграции: 13 файлов, без изменений. + +## Audit — before (snapshot 05.10.2026 11:xx MSK) + +| Ресурс | STAGE (project `food-market-stage`) | PROD (project `deploy`) | Общий? | +|-------------------------------------|-----------------------------------------------------------|-----------------------------------------------------|:------:| +| uploads bind | `/opt/food-market-data/uploads` → `/app/uploads` | `/opt/food-market-data/uploads` → `/app/uploads` | **ДА** | +| postgres volume | `food-market-postgres-data` (shared pgdata) | `food-market-postgres-data` | **ДА** | +| api App_Data (OpenIddict certs) | `food-market-api-data` | `food-market-api-data` | **ДА** | +| api logs | `food-market-api-logs` | `food-market-api-logs` | **ДА** | +| api OCR ssh-key (bind) | `./secrets/invoice-ocr-key:/app/.ssh/invoice-ocr-key:ro` | нет | нет | +| networks | `food-market-stage_default` (172.19/16) | host-net (prod web + api — порты наружу) | нет | +| api host port | `8085:8080` | `8080:8080` | нет | +| web host port | `8086:80` | `8081:80` | нет | +| postgres host port | `127.0.0.1:5435:5432` | `127.0.0.1:5434:5432` | нет | +| image tag | `food-market-api:stage`, `food-market-web:stage` | `food-market-api:latest`, `food-market-web:latest` | нет | +| OpenIddict Issuer | `https://admin.food-market.kz/` *(продовый домен!)* | не задано (default) | **ДА** | +| container name | дефолтные (`food-market-stage-api-1` и т.п.) | явные (`food-market-api`, `food-market-postgres`) | нет | + +**Красные ячейки:** uploads, postgres volume, api-data, api-logs, OpenIddict Issuer. + +(Issuer стенда равен прод-URL — токены, выпущенные стендом, валидируются как прод-токены. Это пятая общая точка, не входит в прямой scope задачи «тома/папки», но фиксируем в прогресс-доке, чтобы не потерять.) + +## Audit — target state + +| Ресурс | STAGE | PROD | +|-----------------------|------------------------------------------------|----------------------------------------| +| uploads bind | `/opt/food-market-stage-data/uploads` | `/opt/food-market-data/uploads` | +| postgres volume | `food-market-stage-postgres-data` | `food-market-postgres-data` | +| api App_Data | `food-market-stage-api-data` | `food-market-api-data` | +| api logs | `food-market-stage-api-logs` | `food-market-api-logs` | +| OpenIddict Issuer | `https://test.admin.food-market.kz/` | `https://admin.food-market.kz/` | + +## Audit — after (snapshot 05.10.2026 12:xx MSK) + +| Ресурс | STAGE | PROD | Общий? | +|-----------------------------|-------------------------------------------------------------|-------------------------------------------------------------|:------:| +| uploads bind | `/opt/food-market-stage-data/uploads` → `/app/uploads` | `/opt/food-market-data/uploads` → `/app/uploads` *(не трогали)* | нет | +| postgres volume | `food-market-stage-postgres-data` (fresh initdb + pg_restore)| `food-market-postgres-data` *(не трогали)* | нет | +| api App_Data | `food-market-stage-api-data` | `food-market-api-data` | нет | +| api logs | `food-market-stage-api-logs` | `food-market-api-logs` | нет | +| container_name | `food-market-stage-{postgres,api,web}` | `food-market-{postgres,api,web}` | нет | +| compose project | `food-market-stage` | `deploy` | нет | +| OpenIddict Issuer | `https://test.admin.food-market.kz/` (default + .env) | `https://admin.food-market.kz/` | нет | +| сеть | `food-market-stage_default` (bridge, 172.19/16) | host-net (прод на bare ports) | нет | +| порты | 8085 api / 8086 web / 5435 pg | 8080 api / 8081 web / 5434 pg | нет | +| image tag default | `:stage` | `:latest` | нет | + +## Как разводили + +1. `pg_dump -F c` стенда (16.6 MB из общего pgdata). +2. `docker compose -p food-market-stage down` (прод-postgres автореcтартовался — он + фактически всё время был zombie на залоченном общим pgdata, теперь живой). +3. Старые stage-named volumes (`food-market-stage-{postgres-data,api-data,api-logs}`) переименованы + в `*-pre-isolation-20261005` (June stale data, в итоге не нужны). +4. `docker compose -p food-market-stage up -d` создал новые волюмы, API пробежал migrations + по пустой БД (EFCore). +5. `docker stop food-market-stage-api` → `DROP DATABASE food_market` → `CREATE DATABASE` → + `pg_restore --no-owner --no-privileges` → `docker start food-market-stage-api`. +6. Единственный FK-warning: `hangfire.state → hangfire.job` для одного «висящего» job'а в середине + OCR — не критично (новые job'ы пишутся с нуля). + +## Что оставили read-only (per инструкции) + +- Прод-контейнеры `food-market-{postgres,web}`, прод-том `food-market-postgres-data`, + прод-папка `/opt/food-market-data/` — только read-only inspect. +- `global.json`, gateway nginx — не касались. +- `/opt/food-market-data/uploads/invoices/2026-10/` оставлена пустая (туда и был `cp -a`, но файлов в + исходнике не было). + +## Защитные swapchain'ы + +- `~/deploy-stage.sh`: guard в начале (проверяет шаблон репо и stage-compose на prod-vm). +- `~/quality-watchdog.sh` step 10 (06:XX ежесуточно) — тот же набор паттернов + `docker inspect` + реального стенда. Падение → красный шаг + инцидент в очередь Server-Claude. + +## Что НЕ в scope этой задачи + +- Общий postgres-инстанс `food-market-postgres-data` всё ещё хостит DB соседей (`calcman_staging`, + `purchase`, `sklad`) — это их прод, не наш. Прод food-market, когда его поднимут, получит свою + чистую пустую БД в этом же волюме; восстанавливать его будем из backup'а. +- Legacy волюм `food-market-stage-minio-data` остался (не в compose) — snapshotнутый кусок старого + аборт-рефакторинга, не удаляю (`остаться как есть`). + +## Follow-ups + +- Поднять прод (отдельная задача, не этот тикет). +- Решить, что делать с pre-isolation-20261005 волюмами — через ~30 дней можно удалить.