Образ: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.
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 — публичного on.snolla.com/admin не существует (никогда не было: RUVDS catch-all IIS не
маршрутизировал /admin на публичном on.snolla.com — cert на 80.64.31.36:443 SNI on.snolla.com = чужой
CN=kupimknigi.spb.ru, /admin → 404). Админка — только локальный catch-all IIS snolla (Task A,
воркстейшн). Site alias переименован on→internal (2026-07-20), доступ = http://internal.snolla.com/admin
через hosts override → 127.0.0.1 → 302→login. Web.config primaryAlias тоже обновлён on→internal
(default-site resolution; иначе catch-all NRE-500 на ВСЕХ запросах — primaryAlias ссылается на алиас,
переименование сайта без обновления primaryAlias ломает весь catch-all). НЕ часть snolla-app.
Cutover 2026-07-20 (первый вынос на VDS — DNS-gated)
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.
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 регрессий.
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.
Poll — контейнер healthy, LE-серт issued on first hit: CN=on.snolla.com, issuer YR1, until 2026-10-18.
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).