diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index 135f2c7..d485bc5 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -1069,7 +1069,7 @@ Dev-source: `victor/labtools.ru/apps/web` (parity-док/буфер — в `.wor --- -## ⚪ [labtools-pro-web-vds-deploy] — Вынести `victor/labtools.pro` (snolla-app, `@snollajs/snolla@0.28.7`, server-side Liquid, БД-контент + MinIO-ассеты) с RUVDS IIS в docker-контейнер на VDS kzntsv (89.253.255.94) за traefik. **Зеркало `[emspb-web-vds-deploy]`** — тот же паттерн; рунбуки `emspb-vds-deploy-runbook` / `labtools-vds-deploy-runbook` (.admin вика) читать как шаблон. Код-DONE: ре-ревью PASS (все 9 дименшнов, ноль хаков), независимый ревьюер на живой БД+MinIO. +## 🟡 [labtools-pro-web-vds-deploy] — Вынести `victor/labtools.pro` (snolla-app, `@snollajs/snolla@0.28.7`, server-side Liquid, БД-контент + MinIO-ассеты) с RUVDS IIS в docker-контейнер на VDS kzntsv (89.253.255.94) за traefik. **Зеркало `[emspb-web-vds-deploy]`** — тот же паттерн; рунбуки `emspb-vds-deploy-runbook` / `labtools-vds-deploy-runbook` (.admin вика) читать как шаблон. Код-DONE: ре-ревью PASS (все 9 дименшнов, ноль хаков), независимый ревьюер на живой БД+MinIO. **Weight:** needs-human **Notify:** OpeItcLoc03/workshop @@ -1103,9 +1103,9 @@ Acceptance = внешний smoke на задеплоенном URL: ## Скилы / отчёт invoke `using-vds-ops`/`using-tasks`/`project-discipline`. Написать рунбук `labtools.pro-vds-deploy-runbook` в .admin вике (зеркало emspb). Репорт → OpeItcLoc03/workshop. Ops-acceptance = staging smoke-parity green; cutover — отдельным словом оператора. -**Status:** ready -**Where I stopped:** (not started) -**Next action:** Собрать образ registry.kzntsv.site/labtools-pro:7bd9fae на VDS из чистого git-archive 7bd9fae (guard: default.json absent); создать Portainer stack labtools-pro (env verbatim из labtools stack 17) на labtools-pro.vds.kzntsv.site за traefik+LE; staging smoke-parity vs www.labtools.pro (каталог+редиректы+theme-md5+cache=none+sitemap www); репорт workshop. Cutover live DNS — отдельным gated-словом оператора. +**Status:** 🟡 STAGING GREEN — ждём DNS-cutover (gated, оператор). Runbook: `.wiki/concepts/labtools.pro-vds-deploy-runbook.md`. +**Where I stopped:** Образ `registry.kzntsv.site/labtools-pro:7bd9fae` (+latest, digest `sha256:23d0bc59…`) собран+запушен НА VDS (guard default.json absent OK). Portainer stack `labtools-pro` **Id 19** создан (env verbatim из stack 17, 8 секретов), контейнер healthy, MSSQL+S3 подключены, лог чист. Staging `labtools-pro.vds.kzntsv.site` LE-cert выпущен. **Smoke-parity vs бой `www.labtools.pro` GREEN** (гнал с VDS): статусы nav+3 каталог-секции+продукты 200/200; редиректы (каталог#1→lshm-750, /Contacts→lowercase, /contacts/→trailing) 301 parity; sitemap = index из 7 дочерних, в сумме 25 URL набор==бой, host=www (R3); theme-ассеты 7/7 md5-identical; Cache-Control absent (корректно, `CachingOptions=[]`); контент-страницы реальное тело (delta +155…+222B = host-длина). Косметика (не дефект, отдана workshop): стартовый баннер лога = `labtools.ru (snolla)` (константа из скаффолда). Compose: `host-stacks/vds-kzntsv/labtools-pro.compose.yml`. Репорт workshop отправлен. +**Next action:** Ждать сигнал оператора «флипнул DNS labtools.pro». ТОЛЬКО ПОСЛЕ external-resolve (8.8.8.8) что именно `labtools.pro`→89.253.255.94 (НЕ путать с labtools.ru!): в стеке 19 заменить traefik-rule на `Host(labtools.pro)||Host(www.labtools.pro)` (Portainer PUT, env сохранить), дождаться LE-cert, live-smoke на боевом хосте, убрать staging-хост post-cutover, репорт. Рунбук § Cutover. НЕ добавлять Host-rule до флипа (LE rate-limit). НЕ выводить RUVDS. **Branch:** master **Notify:** OpeItcLoc03/workshop diff --git a/.wiki/concepts/labtools.pro-vds-deploy-runbook.md b/.wiki/concepts/labtools.pro-vds-deploy-runbook.md new file mode 100644 index 0000000..bf2c94b --- /dev/null +++ b/.wiki/concepts/labtools.pro-vds-deploy-runbook.md @@ -0,0 +1,78 @@ +--- +title: labtools.pro snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback) +type: concept +tags: [labtools-pro, labtools, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, catalog] +related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[emspb-vds-deploy-runbook]], [[labtools-vds-deploy-runbook]] +updated: 2026-07-02 +--- + +# labtools.pro → VDS deploy runbook + +Вынос `labtools.pro` (snolla-приложение, `@snollajs/snolla` **0.28.7**, server-side Liquid, **каталожный сайт**) +с [[../entities/ruvds-iis-host]] (catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]] +(89.253.255.94), за traefik. Модель — snolla-app: читает БД-контент MoreThenCms (`mssql.kzntsv.site`) ++ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy. **Зеркало [[emspb-vds-deploy-runbook]]** — тот же паттерн; +отличие: каталог `/products/*` + `Culture=en` (англоязычный близнец labtools.ru) + Cache-Control отсутствует (см. ниже). + +## Артефакты +- **Код:** `victor/labtools.pro` @ `7bd9fae` (apps/web, ре-ревью PASS все 9 дименшнов, ноль хаков, snolla 0.28.7 final). `deploy/Dockerfile` (multi-stage node:22-slim, non-root, healthcheck `/robots.txt`). +- **Образ:** ⚠️ `registry.kzntsv.site/labtools-pro:7bd9fae` (+`:latest`) — имя **`labtools-pro`**, НЕ `labtools` (тот у labtools.ru!). Собран НА VDS (обход traefik-499). digest `sha256:23d0bc597c8930a1bf443c418a4af68d63b692903963c0e1f9f25baa4a1dd236`. +- **Стек Portainer:** `labtools-pro` (**Id 19**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/labtools-pro.compose.yml`. +- **siteId:** `663F9410-A6CC-4651-9A5C-62844A313957`, activeTheme `389AD745-E3EE-4F23-BE16-DF38440A0941` (theme store MinIO `themes/389ad745e3ee4f23be16df38440a0941/`). Non-secret — в `production.json`, НЕ env. +- **siteUrl:** `https://www.labtools.pro` — в `production.json` (R3: sitemap/robots/canonical отдают www). + +## Сборка образа (на VDS) +```bash +git -C ~/projects/labtools.pro archive --format=tar 7bd9fae \ + | ssh vitya@89.253.255.94 'rm -rf ~/build/labtools-pro && mkdir -p ~/build/labtools-pro && tar -x -C ~/build/labtools-pro' +ssh vitya@89.253.255.94 "cd ~/build/labtools-pro && VERDACCIO_TOKEN='' docker build -f deploy/Dockerfile \ + --build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/labtools-pro:7bd9fae -t registry.kzntsv.site/labtools-pro:latest . \ + && docker push registry.kzntsv.site/labtools-pro:7bd9fae && docker push registry.kzntsv.site/labtools-pro:latest" +``` +- VERDACCIO_TOKEN — `pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN`, build-time only (не в финальный образ; docker linter warn-only). +- `config/default.json` (dev-секреты) исключён `.dockerignore` — guard проверен на архиве: default.json ABSENT (только production.json + custom-environment-variables.json + default.example.json). + +## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git) +**Значения идентичны labtools/emspb** (все snolla-тенанты = одна MoreThenCms + общий MinIO/imgproxy/smtp; tenant-разница = siteId, не секрет). При создании стека env-массив **переиспользован verbatim из labtools stack (Id 17)** через Portainer API. 8 env: `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD`. Маппинг env→config: `apps/web/config/custom-environment-variables.json`. +PORT — не переопределять (образ 5000; EXPOSE 5000; healthcheck `/robots.txt`). traefik service-port = 5000. + +## Создание стека (Portainer API, X-API-Key) +```bash +K=$(pass show vds-kzntsv/full-env | sed -n 's/^PORTAINER_API_KEY=//p'); BASE=https://portainer.vds.kzntsv.site +ENV=$(curl -ksS -H "X-API-Key: $K" $BASE/api/stacks/17 | jq '.Env') # verbatim из labtools stack 17 +COMPOSE=$(cat ~/build/labtools-pro.compose.yml) +PAYLOAD=$(jq -n --arg name labtools-pro --arg compose "$COMPOSE" --argjson env "$ENV" '{name:$name,stackFileContent:$compose,env:$env,fromAppTemplate:false}') +curl -ksS -X POST "$BASE/api/stacks/create/standalone/string?endpointId=1" -H "X-API-Key: $K" -H 'Content-Type: application/json' --data "$PAYLOAD" # → Id 19 +``` +Предусловие pull: `registry.kzntsv.site` зарегистрирован в Portainer как Custom registry Id 1 (иначе `no basic auth`). + +## Runtime egress +`mssql.kzntsv.site:1433` (БД-контент) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы). + +## Staging smoke (2026-07-02, GREEN) — `labtools-pro.vds.kzntsv.site` vs бой `www.labtools.pro` (RUVDS IIS 80.64.31.36) +Гнать с самого VDS (воркстейшн ловит LAN-DNS-перехват на `*.labtools.pro`; с VDS бой резолвится публично → RUVDS). +- **Status-паритет:** nav (`/ /about /contacts`) + 3 каталог-секции (`/products/laboratory-presses`, `/products/press-forms`, `/products/milling-accessories`) + продукты (`plg-20`, `press-forms/ring`) — все 200/200. +- **Редиректы (301 parity, target идентичен modulo host):** каталог#1 `/products/laboratory-ball-mills`→`…/laboratory-ball-mills/lshm-750`; `/Contacts`→lowercase `/contacts`; `/contacts/`→trailing `/contacts`. +- **Sitemap — структура index (snolla) vs плоский (legacy), покрытие идентично:** бой отдаёт плоский `/sitemap.xml` с 25 URL; snolla отдаёт **sitemap-INDEX** из 7 дочерних (`sitemap-pages-1` 3, `sitemap-static-pages-1` 2, `sitemap-sections-1` 3, 4× `sitemap-catalog--products-1` = 1+2+2+12=17), все 200, **в сумме ровно 25 URL, набор == бой** (comm — ноль расхождений). `` host = `www.labtools.pro` (R3). **НЕ дефект** — index-структура штатна для snolla (как pilonuxt). +- **theme-ассеты из MinIO md5-IDENTICAL бою** (проверены css/js/png/svg под `/themes/389ad745e3ee4f23be16df38440a0941/`): 7/7 MD5-OK, http 200. +- ⚠️ **Cache-Control ОТСУТСТВУЕТ на theme-ассетах — КОРРЕКТНО** (labtools.pro `CachingOptions=[]` empty-config; движок 0.28.7 не ставит заголовок = бой). НЕ флагать как дефект (в отличие от emspb/labtools где был `max-age=86400`). +- **Контент-страницы рендерят реальное тело** (12–22 KB), дельта vs бой +155…+222 B — стабильна, объясняется длиной staging-host в canonical/og (на 11 симв. длиннее www; на cutover уравняется в byte-parity). +- Контейнер healthy, MSSQL (tedious) + S3 (aws-sdk) подключены, `listening at http://localhost:5000`, лог чист (только deprecation-warnings tedious/aws-sdk-v2). + +### Косметика (не дефект, отдано workshop) +Стартовый баннер в логе — `labtools.ru (snolla) listening…` — строка-константа унаследована из labtools.ru-референса при скаффолде. Идентификация сайта = siteId в production.json (663F9410…), на рендер/контент не влияет. + +## Cutover (live DNS) — GATED, действие ОПЕРАТОРА (ещё НЕ выполнен) +Бой live на [[../entities/ruvds-iis-host]] (`80.64.31.36`) **остаётся живым** до подтверждения. Порядок (как emspb): +1. **(оператор)** reg.ru: A-записи `labtools.pro` + `www.labtools.pro` → `89.253.255.94` (с RUVDS `80.64.31.36`). Понизить TTL заранее. +2. ⚠️ **Проверить внешним резолвером (8.8.8.8), что флипнут именно `labtools.pro`** (НЕ `labtools.ru` — он тоже ждёт cutover на RUVDS; не перепутать домены). Cм. memory [operator-dns-flip-verify-domain-before-cutover]. +3. **(ops) ТОЛЬКО ПОСЛЕ flip:** в стеке `labtools-pro` (Id 19) заменить traefik-rule на `Host(\`labtools.pro\`) || Host(\`www.labtools.pro\`)` (Portainer PUT, env сохранить, pullImage=false). LE HTTP-01 выпустит cert на первый хит. +4. Проверить LE cert выпущен + live-smoke на боевом хосте; убрать staging-хост `labtools-pro.vds.kzntsv.site` из rule post-cutover. +5. Отрепортить workshop. + +⚠️ **Порядок критичен:** Host-правило добавлять ТОЛЬКО ПОСЛЕ flip DNS (иначе traefik проактивно тянет LE-cert HTTP-01, challenge упадёт на RUVDS → сожжём rate-limit). + +## Rollback +- **DNS:** вернуть A-записи `labtools.pro`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS. +- **Стек:** Portainer → stack `labtools-pro` (Id 19) → remove (или откат тега образа). Образ в registry остаётся. +- RUVDS IIS labtools.pro **не выводить** до явного решения оператора (директива 2026-07-01). (Ср.: emspb.ru биндинги на RUVDS сняты 2026-07-02 после подтверждённого cutover — см. [[../entities/ruvds-iis-host]].) diff --git a/host-stacks/vds-kzntsv/labtools-pro.compose.yml b/host-stacks/vds-kzntsv/labtools-pro.compose.yml new file mode 100644 index 0000000..119ef9e --- /dev/null +++ b/host-stacks/vds-kzntsv/labtools-pro.compose.yml @@ -0,0 +1,58 @@ +# labtools-pro — прод-фронт labtools.pro (snolla-приложение, server-side Liquid, node) на vds-kzntsv (89.253.255.94). +# +# STAGING-FIRST. Source-of-truth этого compose — admin-репо; применяется через Portainer +# API/UI (VDS-rule: стеки через Portainer, не ssh+compose). +# +# Модель: НЕ pilonuxt/Nuxt-contentApi. Это монолит @snollajs/snolla (0.28.7) — читает БД-контент +# MoreThenCms (mssql.kzntsv.site) + server-side Liquid render. Ассеты из MinIO (minio.kzntsv.site) +# через imgproxy (imgproxy.kzntsv.site). node-config: NODE_ENV=production → default(dev,gitignored)+ +# production.json+ENV-override. КАТАЛОЖНЫЙ сайт (secции /products/*, редиректы). +# +# Образ собирается АДМИНОМ на VDS из victor/labtools.pro/deploy/Dockerfile (build-arg VERDACCIO_TOKEN, +# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha монорепы labtools.pro. +# ⚠️ Имя образа labtools-pro (НЕ labtools — тот у labtools.ru). +# +# Домен: STAGING — labtools-pro.vds.kzntsv.site (под wildcard *.vds.kzntsv.site, cert LE HTTP-01 авто). +# Cutover live labtools.pro/www.labtools.pro — ОТДЕЛЬНЫЙ gated шаг: бой живой на RUVDS IIS, НЕ выводить. +# DNS переключает ОПЕРАТОР. ⚠️ НЕ путать с labtools.ru — тот флипается отдельно. План отката = revert DNS. +# +# Runtime egress (наружу из proxy-сети): mssql.kzntsv.site:1433, minio.kzntsv.site:443, +# imgproxy.kzntsv.site:443, smtp.yandex.ru:465. +# +# Секреты — env на VDS (Portainer stack env), НЕ в образ, НЕ в git. Значения ${...} инжектит Portainer +# из stack-переменных (переиспользованы verbatim из labtools stack Id 17). Контракт: +# DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD +# siteId=663F9410-A6CC-4651-9A5C-62844A313957 — non-secret, в production.json (не env). + +services: + labtools-pro: + container_name: labtools-pro + image: registry.kzntsv.site/labtools-pro:7bd9fae # git-sha victor/labtools.pro master + restart: unless-stopped + environment: + NODE_ENV: production + TZ: Europe/Moscow + # PORT не переопределяем — образ дефолтит 5000 (EXPOSE 5000, healthcheck на $PORT) + # --- runtime-секреты (значения из Portainer stack env; НЕ хранить тут) --- + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + IMGPROXY_KEY: ${IMGPROXY_KEY} + IMGPROXY_SALT: ${IMGPROXY_SALT} + S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} + S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} + SMTP_USER: ${SMTP_USER} + SMTP_PASSWORD: ${SMTP_PASSWORD} + networks: + - proxy + labels: + traefik.enable: "true" + traefik.http.routers.labtools-pro.entrypoints: websecure + # STAGING rule. Cutover: заменить на Host(`labtools.pro`) || Host(`www.labtools.pro`) + # ТОЛЬКО ПОСЛЕ флипа DNS на 89.253.255.94 (иначе LE HTTP-01 упадёт на RUVDS → rate-limit). + traefik.http.routers.labtools-pro.rule: Host(`labtools-pro.vds.kzntsv.site`) + traefik.http.routers.labtools-pro.tls.certresolver: letsEncrypt + traefik.http.services.labtools-pro.loadbalancer.server.port: "5000" + +networks: + proxy: + external: true