From c3b722bb6d8da22c3d0aea01426f41009ec61eac Mon Sep 17 00:00:00 2001 From: vitya Date: Thu, 2 Jul 2026 00:38:03 +0300 Subject: [PATCH] =?UTF-8?q?deploy(emspb):=20b6e361a=20LIVE=20on=20vds-kznt?= =?UTF-8?q?sv=20=E2=80=94=20staging=20GREEN=20+=20cutover=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - image registry.kzntsv.site/emspb:b6e361a (digest 8f5ba02) built on VDS from clean git-archive - Portainer stack emspb (Id 18) traefik+LE; 8 runtime-env secrets reused verbatim from labtools stack-17 - staging smoke vs prod www.emspb.ru: 23/23 status-parity, theme CSS+image (MinIO) md5-identical, CC 86400 - cutover (operator flipped emspb.ru/www DNS -> VDS): traefik-rule Host(emspb.ru)||Host(www.emspb.ru), staging host emspb.vds.kzntsv.site removed post-cutover (-> 404), single LE cert, live smoke www.emspb.ru+emspb.ru 200 ssl_verify=0, canonical normalized, live == old-RUVDS byte-identical - RUVDS IIS 80.64.31.36 intact = rollback path - runbook .wiki/concepts/emspb-vds-deploy-runbook.md (mirror labtools) Co-Authored-By: Claude Opus 4.8 (1M context) --- .tasks/STATUS.md | 9 ++- .wiki/concepts/emspb-vds-deploy-runbook.md | 90 ++++++++++++++++++++++ host-stacks/vds-kzntsv/emspb.compose.yml | 56 ++++++++++++++ 3 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 .wiki/concepts/emspb-vds-deploy-runbook.md create mode 100644 host-stacks/vds-kzntsv/emspb.compose.yml diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index 6dad484..f97257f 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -1028,6 +1028,9 @@ Dev-source: `victor/labtools.ru/apps/web` (parity-док/буфер — в `.wor --- +## 🟢 [emspb-web-vds-deploy] — closed 2026-07-02 — staging GREEN + cutover LIVE. Образ `registry.kzntsv.site/emspb:b6e361a` (digest `8f5ba02…`) собран на VDS, Portainer stack `emspb` (Id 18). Staging smoke vs бой 23/23 status-parity, theme-CSS+image из MinIO md5-identical, Cache-Control 86400. **Cutover выполнен в ту же сессию** (оператор флипнул DNS emspb.ru/www→VDS): traefik-rule `Host(emspb.ru)||Host(www.emspb.ru)` (staging-хост убран post-cutover→404), единый LE-cert, live-smoke `www.emspb.ru`+`emspb.ru`→200 `ssl_verify=0`, canonical нормализован, live==старый RUVDS байт-в-байт. RUVDS IIS `80.64.31.36` цел = rollback. Рунбук [emspb-vds-deploy-runbook](../.wiki/concepts/emspb-vds-deploy-runbook.md). Notify→workshop. + + ## ⚪ [emspb-web-vds-deploy] — Деплой `victor/emspb.ru` `apps/web` (snolla-приложение, БД-контент+SSR Liquid) на Rusonyx VDS в docker за traefik, **staging-first**. Зеркало `[labtools-web-vds-deploy]` — тот же паттерн, адаптировать хосты/siteId. Код готов (ре-ревью PASS, ноль хаков, 29/29 parity). `deploy/` артефакты уже в репо. ## Источник @@ -1057,9 +1060,9 @@ Dev-source: `victor/labtools.ru/apps/web` (parity-док/буфер — в `.wor **weight:** needs-human **notify:** OpeItcLoc03/workshop -**Status:** ready -**Where I stopped:** (not started) -**Next action:** Собрать образ на VDS из victor/emspb.ru deploy/Dockerfile (чистый git-archive, секретов нет) → registry.kzntsv.site/emspb: → Portainer stack STAGING emspb.vds.kzntsv.site за traefik+LE → runtime-env-секреты → smoke parity vs бой www.emspb.ru. Рунбук в admin-вику. Cutover live DNS — НЕ трогать (отдельный gated-шаг). +**Status:** 🟢 closed 2026-07-02 — staging + cutover оба GREEN, emspb.ru LIVE на VDS. +**Where I stopped:** (done) Образ b6e361a собран+запушен на VDS, stack Id 18, staging smoke 23/23, cutover выполнен (traefik-rule на боевые хосты, LE-cert, live-smoke 200/valid, паритет vs RUVDS байт-в-байт). RUVDS rollback цел. +**Next action (было):** Собрать образ на VDS из victor/emspb.ru deploy/Dockerfile (чистый git-archive, секретов нет) → registry.kzntsv.site/emspb: → Portainer stack STAGING emspb.vds.kzntsv.site за traefik+LE → runtime-env-секреты → smoke parity vs бой www.emspb.ru. Рунбук в admin-вику. Cutover live DNS — НЕ трогать (отдельный gated-шаг). **Branch:** n/a **Notify:** OpeItcLoc03/workshop diff --git a/.wiki/concepts/emspb-vds-deploy-runbook.md b/.wiki/concepts/emspb-vds-deploy-runbook.md new file mode 100644 index 0000000..9bffa10 --- /dev/null +++ b/.wiki/concepts/emspb-vds-deploy-runbook.md @@ -0,0 +1,90 @@ +--- +title: emspb.ru snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback) +type: concept +tags: [emspb, snolla, vds, deploy, docker, portainer, traefik, runbook, migration] +related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[labtools-vds-deploy-runbook]] +updated: 2026-07-02 +--- + +# emspb.ru → VDS deploy runbook + +Вынос `emspb.ru` (snolla-приложение, `@snollajs/snolla` 0.28.4, server-side Liquid) с [[../entities/ruvds-iis-host]] +(catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]] (89.253.255.94), за traefik. +Модель — snolla-app (НЕ pilonuxt/Nuxt-contentApi): читает БД-контент MoreThenCms (`mssql.kzntsv.site`) ++ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy. **Зеркало [[labtools-vds-deploy-runbook]]** — тот же паттерн. + +## Артефакты +- **Код:** `victor/emspb.ru` @ `b6e361a` (apps/web, ре-ревью PASS, 29/29 parity, snolla 0.28.4 final). `deploy/Dockerfile` (multi-stage node:22-slim, non-root, healthcheck `/robots.txt`). +- **Образ:** `registry.kzntsv.site/emspb:b6e361a` (+`:latest`). Собран НА VDS (обход traefik-499). digest `sha256:8f5ba02651b71f340fa9bc3b079fd2853b01b1461e6c479fb2cf788691d31a74`. +- **Стек Portainer:** `emspb` (Id 18, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/emspb.compose.yml`. +- **siteId:** `96EBC481-D26A-47BE-B660-13D49E7D0A61`, activeTheme `DD8D6F7A-CEDC-4D5A-8BFB-E5F9BE9DC035` (theme store MinIO `themes/dd8d6f7acedc4d5a8bfbe5f9be9dc035/`). Non-secret — в `production.json`, НЕ env. + +## Сборка образа (на VDS) +```bash +# чистый git-archive (только tracked → без config/default.json и node_modules) на VDS +git -C ~/projects/emspb.ru archive --format=tar b6e361a \ + | ssh vitya@89.253.255.94 'rm -rf ~/build/emspb && mkdir -p ~/build/emspb && tar -x -C ~/build/emspb' +# build с build-arg VERDACCIO_TOKEN (pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN), передаётся через env (не в argv), push из VDS +ssh vitya@89.253.255.94 "cd ~/build/emspb && VERDACCIO_TOKEN='' docker build -f deploy/Dockerfile \ + --build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/emspb:b6e361a -t registry.kzntsv.site/emspb:latest . \ + && docker push registry.kzntsv.site/emspb:b6e361a && docker push registry.kzntsv.site/emspb:latest" +``` +- VERDACCIO_TOKEN — build-time only (discarded build-стейдж; в финальный образ не попадает; docker linter warn-only ×2 — ожидаемо). +- `config/default.json` (dev-секреты) исключён `.dockerignore` — в образе только `production.json` + `custom-environment-variables.json` (guard проверен на архиве: default.json ABSENT). + +## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git) +Маппинг env→config: `apps/web/config/custom-environment-variables.json`. Non-secret (host/siteId/endpoints) — в `production.json`. +**Значения идентичны labtools** (все snolla-тенанты читают одну MoreThenCms + общий MinIO/imgproxy/smtp; tenant-разница = siteId, не секрет). При создании стека env-массив переиспользован verbatim из labtools stack (Id 17) через Portainer API. + +| ENV | Значение | Источник | +|---|---|---| +| `DB_USER` | `snolla` | web.config боя MoreThenCmsEntities | +| `DB_PASSWORD` | ⟨секрет⟩ | web.config боя MoreThenCmsEntities (User Id=snolla) | +| `IMGPROXY_KEY` | ⟨128hex⟩ | `pass minio-vds/full-env` (== сервер imgproxy books-vds — HMAC совпадает) | +| `IMGPROXY_SALT` | ⟨128hex⟩ | `pass minio-vds/full-env` | +| `S3_ACCESS_KEY_ID` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_USER) | +| `S3_SECRET_ACCESS_KEY` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_PASSWORD) | +| `SMTP_USER` | `noreply@snolla.com` | `pass snolla-smtp/full-env` | +| `SMTP_PASSWORD` | ⟨секрет⟩ | `pass snolla-smtp/full-env` (SMTP_PASS) | + +PORT — не переопределять (образ дефолтит 5000; EXPOSE 5000; healthcheck `/robots.txt` на $PORT). traefik service-port = 5000. + +## Создание стека (Portainer API, на VDS — bash+curl+jq, обходит PS-кириллица-гочу) +```bash +# JWT (pass vds-kzntsv/full-env PORTAINER_PASS); env переиспользован из labtools stack 17 +JWT=$(curl -ksS -X POST https://portainer.vds.kzntsv.site/api/auth -d '{"username":"vitya","password":""}' | jq -r .jwt) +ENV=$(curl -ksS -H "Authorization: Bearer $JWT" https://portainer.vds.kzntsv.site/api/stacks/17 | jq '.Env') +COMPOSE=$(cat ~/build/emspb.compose.yml) +PAYLOAD=$(jq -n --arg name emspb --arg compose "$COMPOSE" --argjson env "$ENV" '{name:$name,stackFileContent:$compose,env:$env,fromAppTemplate:false}') +curl -ksS -X POST "https://portainer.vds.kzntsv.site/api/stacks/create/standalone/string?endpointId=1" \ + -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" --data "$PAYLOAD" # → Id=18 +``` +Предусловие pull: `registry.kzntsv.site` зарегистрирован в Portainer как Custom registry Id 1 (иначе `no basic auth`). + +## Runtime egress (наружу из proxy-сети) +`mssql.kzntsv.site:1433` (БД-контент, обязателен) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы). + +## Staging smoke (2026-07-02, GREEN) — `emspb.vds.kzntsv.site` vs бой `www.emspb.ru` (RUVDS IIS 80.64.31.36, через `--resolve`) +- **Status-паритет 23/23:** 5 nav (`/ /contacts /portfolio /prices`) + 17 service-детальных (`/ustanovka-*`, `/almaznoe-burenie-*` …) все 200/200; `/services` 404/404 (nav-якорь, не страница); `/index.php` 404/404; `/robots.txt` 200 байт-в-байт (31 B). +- **Дельта размера контент-страниц стабильна +8 B**, полностью объяснена: +9 host-строка в canonical/og itemprop (`emspb.vds.kzntsv.site` на 9 символов длиннее `www.emspb.ru`), −1 — новый snolla почистил битый двойной слэш боя `/images//3.jpg`→`/images/3.jpg` (оба варианта → 200, картинка не сломана). `/` — байт-в-байт (canonical из DB-домена, не request-host). +- **theme CSS ×3 (framework/bootstrap/slick) из MinIO — md5 IDENTICAL** с боем; `Cache-Control: max-age=86400, public, must-revalidate`, `text/css`. +- **theme image `/images/3.jpg` из MinIO — md5 IDENTICAL** (115618 B, image/jpeg). +- Редиректы: `/contacts/`→301→`/contacts` (trailing-slash), `/Contacts`→301→lowercase, `/Portfolio`→301→lowercase — как бой. +- Контейнер healthy (t+10s), MSSQL (tedious) + S3 (aws-sdk) подключены, `listening at http://localhost:5000`, лог чист (только deprecation-warnings tedious/aws-sdk-v2). + +## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02 +Порядок был: +1. **(оператор)** reg.ru: A-записи `emspb.ru` + `www.emspb.ru` → `89.253.255.94` (с RUVDS `80.64.31.36`). — сделал оператор (сигнал «поменял DNS у провайдера»). Проверено внешним резолвером (8.8.8.8): оба хоста → VDS; labtools остался на RUVDS (не путать домены!). +2. **(ops)** В стеке `emspb` (Id 18) traefik-rule расширен: `Host(\`emspb.ru\`) || Host(\`www.emspb.ru\`)` — Portainer PUT (env сохранён, pullImage=false). LE HTTP-01 выпустил cert на первом хите после flip (~t+20-70s). **staging-хост `emspb.vds.kzntsv.site` убран из rule post-cutover** (проверено: → 404). Cert перевыпустится на 2 боевых SAN при renewal. +3. **Live-smoke GREEN:** `www.emspb.ru`+`emspb.ru` → 200 `ssl_verify=0` (cert доверенный); все страницы 200; canonical нормализован на `www.emspb.ru`; live VDS `/contacts` == старый RUVDS **байт-в-байт** (host уравнялся). + +⚠️ **Порядок критичен:** Host-правило добавлено ТОЛЬКО ПОСЛЕ flip DNS (иначе LE HTTP-01 challenge упал бы на RUVDS → сожгли бы rate-limit). Перед PUT — обязательно проверить внешним резолвером, что DNS реально на VDS. + +### Guard (как labtools) +- **НЕ добавлять `Host(emspb.ru)` в traefik-rule ДО флипа DNS** — traefik на reload проактивно тянет LE-cert (HTTP-01), challenge упадёт на RUVDS → сожжём LE rate-limit. +- **НЕ выводить RUVDS IIS emspb из эксплуатации** — это rollback-путь. +- **НЕ флипать DNS самим** — это делает хозяин домена/оператор. + +## Rollback +- **DNS:** вернуть A-записи `emspb.ru`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS. +- **Стек:** Portainer → stack `emspb` (Id 18) → remove (или откат тега образа). Образ в registry остаётся. diff --git a/host-stacks/vds-kzntsv/emspb.compose.yml b/host-stacks/vds-kzntsv/emspb.compose.yml new file mode 100644 index 0000000..ce96992 --- /dev/null +++ b/host-stacks/vds-kzntsv/emspb.compose.yml @@ -0,0 +1,56 @@ +# emspb — прод-фронт emspb.ru (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 — читает БД-контент 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. +# +# Образ собирается АДМИНОМ на VDS из victor/emspb.ru/deploy/Dockerfile (build-arg VERDACCIO_TOKEN, +# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha монорепы emspb.ru. +# +# Домен: STAGING — emspb.vds.kzntsv.site (под wildcard *.vds.kzntsv.site, cert LE HTTP-01 авто). +# Cutover live www.emspb.ru — ОТДЕЛЬНЫЙ gated шаг: бой живой на текущем хостинге, НЕ выводить. +# DNS переключает ОПЕРАТОР по сигналу «staging green». План отката = 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-переменных. Контракт (custom-environment-variables.json): +# DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD +# siteId=96EBC481-D26A-47BE-B660-13D49E7D0A61 — non-secret, в production.json (не env). + +services: + emspb: + container_name: emspb + image: registry.kzntsv.site/emspb:b6e361a # git-sha victor/emspb.ru 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.emspb.entrypoints: websecure + # CUTOVER 2026-07-02: DNS emspb.ru/www флипнут на VDS (89.253.255.94) оператором. Боевые хосты. + # staging-хост emspb.vds.kzntsv.site УБРАН post-cutover (2026-07-02). RUVDS IIS 80.64.31.36 — rollback, не тронут. + traefik.http.routers.emspb.rule: Host(`emspb.ru`) || Host(`www.emspb.ru`) + traefik.http.routers.emspb.tls.certresolver: letsEncrypt + traefik.http.services.emspb.loadbalancer.server.port: "5000" + +networks: + proxy: + external: true