- 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) <noreply@anthropic.com>
9.3 KiB
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). digestsha256: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, activeThemeDD8D6F7A-CEDC-4D5A-8BFB-E5F9BE9DC035(theme store MinIOthemes/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;/services404/404 (nav-якорь, не страница);/index.php404/404;/robots.txt200 байт-в-байт (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
Порядок был:
- (оператор) reg.ru: A-записи
emspb.ru+www.emspb.ru→89.253.255.94(с RUVDS80.64.31.36). — сделал оператор (сигнал «поменял DNS у провайдера»). Проверено внешним резолвером (8.8.8.8): оба хоста → VDS; labtools остался на RUVDS (не путать домены!). - (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. - Live-smoke GREEN:
www.emspb.ru+emspb.ru→ 200ssl_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 остаётся.