Files
admin/.wiki/concepts/emspb-vds-deploy-runbook.md
vitya 8e3e5b4353 wiki(snolla-0.42.1): рецепт live-prod in-place bump + секции в 3 рунбука + index/log
Закрыт пропуск: тираж 0.42.1 не был зафиксирован в вике (таски emspb/labtools.pro
просили using-wiki).

- NEW concepts/snolla-live-prod-inplace-image-bump.md — переиспользуемый рецепт
  (build на VDS → throwaway-staging-acceptance С VDS → env-preserving Portainer PUT
  put-stack.js → live-smoke), встроен put-stack.js, гочи.
- UPDATE 3 рунбука секцией «0.42.1 in-place bump» (labtools.ru/emspb/labtools.pro)
  с образами/acceptance/rollback.
- FIX orphan: все 3 deploy-рунбука добавлены в index.md (не были каталогизированы).
- log.md: decision-запись.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:16:15 +03:00

11 KiB
Raw Permalink Blame History

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, snolla-live-prod-inplace-image-bump updated: 2026-07-05

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)

# чистый 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='<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-кириллица-гочу)

# 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":"<pw>"}' | 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.ru89.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/www80.64.31.36 (RUVDS IIS живой, нетронут). Откат = смена DNS.
  • Стек: Portainer → stack emspb (Id 18) → remove (или откат тега образа). Образ в registry остаётся.

0.42.1 in-place bump — ВЫПОЛНЕН 2026-07-05

Тираж snolla 0.42.1. emspb.ru обновлён на живом стеке 18 in-place (домен уже на VDS, DNS не трогали). Рецепт — snolla-live-prod-inplace-image-bump. Оператор дал отмашку на боевой apply.

  • Образ: registry.kzntsv.site/emspb:95a5c42 (snolla 0.28.4→0.42.1, digest d6299cf…). Собран на VDS.
  • Acceptance С VDS (throwaway-staging из env стека 18): 29 sitemap-роутов все 200 (0 non-2xx/3xx), content-not-lost 29/29 (0 потерь). Sitemap НЕ реструктурировался (29==29, в отличие от каталожного labtools.ru).
  • TLS: серт — SAN покрывает emspb.ru/www.emspb.ru/emspb.vds.kzntsv.site, CN косметически = staging-хост (curl validates sslverify=0). Это тот самый серт с cutover 07-02 (см. выше: «Cert перевыпустится на 2 боевых SAN при renewal») — swap его не тронул. При renewal можно пере-выпустить с CN=emspb.ru, опционально.
  • Swap: Portainer PUT стека 18 (env 8/8 сохранён) → healthy → live-smoke GREEN, sitemap 29 вживую.
  • Rollback: тег emspb:b6e361a (0.28.4, в registry) / стек 18 PUT назад / DNS→RUVDS.
  • Compose обновлён на 95a5c42. Таска [emspb-deploy-snolla-0-42-1] 🟢. Notify=victor/emspb.ru.