Compare commits

..

2 Commits

Author SHA1 Message Date
vitya
4c4d19382b feat(on-snolla): CUTOVER DONE — on.snolla.com live on VDS (stack 22, LE cert)
DNS reg.ru on.snolla.com A -> 89.253.255.94 (operator flip, authoritative NS verified).
Traefik rule swapped staging->Host(on.snolla.com), LE cert issued (CN=on.snolla.com,
YR1, until 2026-10-18). Live-smoke GREEN, homepage byte-identical prod-HTML, sitemap
3/3 parity. RUVDS IIS untouched (rollback = revert DNS).

- compose source-of-truth updated to LIVE rule.
- runbook .wiki/concepts/on-snolla-vds-deploy-runbook.md (cutover as-built + gotchas:
  snolla robotsTxt crash on undefined app.locals.domain, no Liquid form-tag, /c dead-link
  parity, Portainer JWT re-auth before PUT).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 13:17:59 +03:00
vitya
9705468ef4 feat(on-snolla): Portainer stack 22 source-of-truth (staging Host, mem_limit 512m, 8 env secrets)
on.snolla.com snolla-app live behind traefik on staging Host on-snolla.vds.kzntsv.site.
Image registry.kzntsv.site/on-snolla:473923e494db (snolla 0.42.1). Pending DNS cutover
(reg.ru on.snolla.com A 80.64.31.36 -> 89.253.255.94, then PUT rule -> Host(on.snolla.com)).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 13:17:59 +03:00
2 changed files with 151 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
---
title: on.snolla.com snolla-app — VDS deploy runbook (cutover 2026-07-20)
type: concept
tags: [on-snolla, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, landing]
related: [[../entities/vds-kzntsv], [../entities/ruvds-iis-host], [tandemmebel-vds-deploy-runbook], [snolla-live-prod-inplace-image-bump], [portainer-stack-management-vds], [snolla-local-admin-and-on-snolla-migration-design]]
updated: 2026-07-20
---
# on.snolla.com → VDS deploy runbook
Вынос посадочной `on.snolla.com` (snolla-app, `@snollajs/snolla` **0.42.1**, server-side Liquid) с
[[../entities/ruvds-iis-host]] (catch-all CMS, 80.64.31.36) в docker-контейнер на [[../entities/vds-kzntsv]]
(89.253.255.94), за traefik. Паттерн = [[tandemmebel-vds-deploy-runbook]]. Отличие: **лендинг** (1 Page `/`
+ 2 StaticPages), БЕЗ блога/каталога/e-commerce → reconstruction = layout.liquid byte-identical prod-HTML.
Culture `en`. Спека: [[snolla-local-admin-and-on-snolla-migration-design]] §Task B.
## Артефакты
- **Код:** `victor/on.snolla.com` @ `473923e494db` (apps/web, snolla 0.42.1). Структура-калька tandemmebel.ru.
`deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root, healthcheck `/robots.txt`).
- **Образ:** `registry.kzntsv.site/on-snolla:473923e494db` (+`:latest`). Имя **`on-snolla`** (= стек/контейнер).
Собран НА VDS (обход traefik-499).
- **Стек Portainer:** `on-snolla` (**Id 22**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/on-snolla.compose.yml`.
- **siteId:** `B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7` (non-secret, в `production.json`).
- **siteUrl:** `https://on.snolla.com``production.json`).
- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD`
— Portainer stack env (переиспользованы verbatim из tandemmebel стека 20 — общая snolla-инфра). НЕ в образе, НЕ в git.
- **mem_limit:** `512m`.
## Site model (MoreThenCms DB, siteId B9ECDB50)
- 1 Page `/` (template `content_page`, `customDefaultLayout=layout`). **Content-поля пустые** → весь
bootstraptor-лендинг baked в `views/layout.liquid` (byte-identical prod). `{{ item.content }}` (пуст) wired.
- Form "Join us" (Path `/`, FormTemplate `join_us`: first_name/last_name/email/website/description/captcha)
baked в `layout.liquid`; POST `/` → snolla `forms` middleware → `form_ajax_response.liquid` (AJAX replace).
Legacy reCAPTCHA v1 в разметке мертва (Google shut down v1) — parity с продом, НЕ чиним.
- StaticPages: `/yandex_7a510ac1f8311fca.html` (verification, IncludeInSitemap=true), `/4a052808276c.html` (not in sitemap).
- `/c`**404** (stale dead-link в .NET sitemap; snolla-app sitemap тоже его несёт → parity `/`+`/c`+`/yandex`).
- Theme assets `/themes/c406a987ebe14244b0eefe7b8f959a6f/{css,js,images}/*` — из MinIO bucket `themes` (snolla
`themeFiles` middleware; пути literal в `layout.liquid`).
- `/admin` остаётся на RUVDS .NET — НЕ часть этого snolla-app.
## Cutover 2026-07-20 (первый вынос на VDS — DNS-gated)
1. **Build на VDS** (archive `473923e494db``~/build/on-snolla` → `docker build -f deploy/Dockerfile
--build-arg VERDACCIO_TOKEN -t .../on-snolla:473923e494db -t .../on-snolla:latest . && push`). Guard:
`config/default.json` ABSENT в архиве (.dockerignore), пин snolla = 0.42.1.
2. **Throwaway-staging :5077** из собранного env (8 секретов inline, т.к. нового live-контейнера ещё нет —
секреты общие с тиражом). `docker run -d --name on-snolla-staging --env-file .staging.env -p 127.0.0.1:5077:5000`.
Healthy. robots/`/`/sitemap 200.
3. **Completeness-gate С VDS** — sitemapindex разворот. **NEW==PROD locs: 3 = 3 IDENTICAL** (`/`, `/c`, `/yandex_…`).
Self-consistency: `/`→200, `/yandex`→200, `/c`→404 (dead-link, parity с прод-оракулом — prod /c тоже 404).
**Byte-parity:** homepage + /yandex **byte-identical** staging vs prod (RUVDS, via `--resolve :443:80.64.31.36`).
GREEN = 0 регрессий.
4. **Portainer стек 22** создан со **staging-Host** `on-snolla.vds.kzntsv.site` (wildcard → VDS), LE=default cert.
Healthy за traefik. (`create-stack.mjs` node-скрипт, Portainer JWT auth, env array 8/8, `mem_limit 512m`.)
5. **DNS flip** — operator: reg.ru `on.snolla.com` A `80.64.31.36` → `89.253.255.94`.
6. **Verify авторит. NS** — `nslookup on.snolla.com ns1/ns2.reg.ru` = `89.253.255.94` (BEFORE traefik rule swap —
memory `operator-dns-flip-verify-domain-before-cutover`: wrong domain burns LE rate-limit). Public resolvers propagated.
7. **PUT стека 22** — rule `Host(on-snolla.vds.kzntsv.site)` → `Host(on.snolla.com)`, env-preserving (8/8),
`prune:false, pullImage:false`. node `cutover-put.mjs` (Portainer JWT; **re-auth right before PUT** —
jwt из GET-фазы успел протухнуть к моменту PUT, первый PUT дал 401). PUT 200.
8. **Poll** — контейнер healthy, **LE-серт issued on first hit**: CN=on.snolla.com, issuer YR1, until 2026-10-18.
9. **Live-smoke С VDS + external** — `https://on.snolla.com/{robots.txt,/,/sitemap.xml,/yandex_…}` = 200,
`/c` = 404 parity. TLS verify `ssl_verify_result=0` (trusted LE chain). Homepage **byte-identical** prod-HTML.
## Гочи (специфичные)
- **snolla `robotsTxt` middleware падает на undefined `app.locals.domain`** — on.snolla.com НЕ имеет row в
MoreThenCms `Domains` (served by primaryDomain без per-domain row). `robotsTxt.js` читает
`res.app.locals.domain.robotsTxt` без null-guard → GET /robots.txt (и Dockerfile healthcheck) крашится 500.
**Фикс в `apps/web/index.js`**: после `initApp`, если `!app.locals.domain`, synthesized `{robotsTxt: site.robotsTxt}`
(only robotsTxt.js reads locals.domain; theme resolution uses site.activeThemeId — safe). Без мутации shared DB.
**Альтернатива** (не применена): добавить Domains row (Name=on.snolla.com, SiteId=B9ECDB50, Public=1, ThemeId=null)
— но это мутация shared prod MoreThenCms; предпочтён app-code fallback.
- **Form "Join us" не имеет Liquid form-tag** — snolla не предоставляет form-rendering tag. Форма HTML baked в
`layout.liquid` (поля по FormTemplate `join_us`); POST обрабатывает `forms` middleware (мэтч по Path `/`).
`form_ajax_response.liquid` — AJAX-ответ (`$form.replaceWith(response)`).
- **`/c` в sitemap — dead-link** — snolla SitemapService эмбитит `/c` из sections-source, но страница 404.
Prod-оракул идентичен (404) → benign parity, не регрессия.
- **hosts override на воркстейшне** — Task A оставил `127.0.0.1 on.snolla.com` в hosts (локальный админ catch-all).
External verify прод-on.snolla.com с воркстейшна = только через `curl --resolve on.snolla.com:443:89.253.255.94`
(иначе попадёшь в локальный IIS). См. memory `workstation-lan-dns-serves-local-cms-copy`.
- **Portainer JWT short-lived** — GET stack + GET file + PUT в одном скрипте: jwt из GET-фазы может протухнуть
к PUT. Re-auth (`POST /api/auth`) прямо перед PUT. API-key `ptr_*` даёт 401 (см. [[portainer-stack-management-vds]]).
- **Образных rollback-тегов пока нет** — первый deploy, `473923e494db` = `:latest`. Rollback = revert DNS (RUVDS жив).
## Rollback
- **DNS (предпочт, мгновенный):** revert reg.ru `on.snolla.com` A → `80.64.31.36` (RUVDS IIS жив, не тронут).
- **Образный (будущие bump'ы):** PUT стека 22 назад на предыдущий тег (rollback-теги в registry).
## Связанное
- on.snolla.com admin (`/admin`) остаётся на RUVDS .NET — catch-all IIS `snolla` (см. [[snolla-local-admin-and-on-snolla-migration-design]] §Task A).
- Тираж snolla на VDS: labtools.ru(17), emspb(18), labtools.pro(19), tandemmebel(20), kupimknigi(21), **on-snolla(22)**.

View File

@@ -0,0 +1,61 @@
# on-snolla — посадочная on.snolla.com (snolla-app, 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).
#
# Модель: монолит @snollajs/snolla (0.42.1) / core / liquid / data — читает БД-контент MoreThenCms
# (mssql.kzntsv.site) + server-side Liquid render. Ассеты из MinIO (minio.kzntsv.site). In-process sharp,
# delivery=serve-bytes, cache=local-variant. imgproxy в путях нет (как tandemmebel — сырой /imgproxy -> 404 норма).
# node-config: NODE_ENV=production -> default(dev,gitignored)+production.json+ENV-override.
# ЛЕНДИНГ (1 Page / + 2 StaticPages), БЕЗ блога/каталога/e-commerce.
#
# Образ собирается АДМИНОМ на VDS из victor/on.snolla.com/deploy/Dockerfile (build-arg VERDACCIO_TOKEN,
# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha репо on.snolla.com (текущий 473923e494db).
# Homepage render byte-identical прод-HTML (verified direct diff + completeness-gate 3/3 loc parity 2026-07-20).
#
# Домен: LIVE — on.snolla.com (cert LE HTTP-01, CN=on.snolla.com, issued 2026-07-20, until 2026-10-18).
# Cutover завершён 2026-07-20: DNS reg.ru on.snolla.com A 80.64.31.36 -> 89.253.255.94 (operator flip,
# авторит. NS ns1/ns2.reg.ru verified -> VDS), traefik rule staging->боевой (LE-порядок соблюдён),
# LE-серт issued on first hit. Live-smoke GREEN, homepage byte-identical прод-HTML, sitemap 3/3 parity.
# Предыстория: жил на RUVDS IIS (80.64.31.36) до 2026-07-20. RUVDS IIS не тронут (rollback = revert DNS).
# План отката = revert DNS reg.ru -> 80.64.31.36 (RUVDS IIS жив) ИЛИ PUT стека 22 назад на тег (пока
# единственный тег 473923e494db = :latest — первый deploy; образных rollback-тегов пока нет).
#
# Runtime egress: mssql.kzntsv.site:1433, minio.kzntsv.site:443 (оригиналы + variant-cache), smtp.yandex.ru:465.
#
# Секреты — env на VDS (Portainer stack env), НЕ в образ, НЕ в git. Значения ${...} инжектит Portainer
# из stack-переменных (переиспользованы verbatim из tandemmebel стека 20 — общая snolla-инфра). Контракт:
# DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD
# siteId=B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7 — non-secret, в production.json (не env). Culture en. siteUrl=https://on.snolla.com
services:
on-snolla:
container_name: on-snolla
image: registry.kzntsv.site/on-snolla:473923e494db # git-sha victor/on.snolla.com master 473923e494db (snolla 0.42.1). rollback-теги в registry: 473923e494db (:latest он же — первый deploy, отката пока нет, revert DNS -> RUVDS).
restart: unless-stopped
mem_limit: 512m # guardrail: snolla baseline ~100-200M; OOM -> restart. standalone-compose -> mem_limit (НЕ deploy.resources, swarm-only).
environment:
NODE_ENV: production
TZ: Europe/Moscow
# --- 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.on-snolla.entrypoints: websecure
# LIVE (cutover 2026-07-20). Rollback = Host(`on-snolla.vds.kzntsv.site`) + revert DNS -> 80.64.31.36.
traefik.http.routers.on-snolla.rule: Host(`on.snolla.com`)
traefik.http.routers.on-snolla.tls.certresolver: letsEncrypt
traefik.http.services.on-snolla.loadbalancer.server.port: "5000"
networks:
proxy:
external: true