Files
admin/.wiki/concepts/stostayer-web-deploy-runbook.md
vitya 23b1a47c14 wiki(stostayer): ingest stostayer-admin-minio-config — :8090 admin on stostayer MinIO
Fix 2026-07-22: local .NET admin (C:\sites\stostayer\Web.config) S3 providers
re-pointed to stostayer MinIO. 3 bugs in original config: wrong creds
(AKIAJ2YJP72W6ZHCRE6Q = books-vds root, not stayer_minio), wrong endpoint
(minio.stostayer.ru is not S3 API port; prod uses minio-api.stostayer.ru),
wrong region (local -> AmazonS3Exception expecting us-west-1). Creds NOT in
pass — live in stostayer.new config/default.json. Admin now shares prod
storage; manual App_Data mirroring retired.

+ index.md, log.md. Also carries pending handoff (NEXT_SESSION) + runbook
update from 2026-07-21 stostayer-web 0.3.20 deploy session.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 10:45:13 +03:00

14 KiB
Raw Permalink Blame History

title, status, tags, related, updated
title status tags related updated
stostayer-web — деплой легаси web на прод клиента (runbook) live
stostayer
docker
portainer
deployment
nuxt2
esm
ops
rollback
portainer-stack-management-vds
2026-06-17

stostayer-web — деплой легаси web на прод клиента

Прод https://www.stostayer.ru = легаси packages/web (Nuxt 2, CJS, SSR) из монорепо victor/stostayer.new. web4 (Nuxt 4) ещё НЕ переключён. Хостится контейнером, управляется Portainer-стеком у клиента.

Машина клиента, настраивает их админ — мы только деплоим свой web-образ, конфиги хоста не трогаем. Деталь доступов/инфры — в stostayer.new/.wiki/concepts/client-infra-access.md. Все креды — pass stostayer/client.

Параметры

Что Значение
Хост 91.222.236.225 (SSH :20435, юзер victor + sudo-с-паролем; парольный SSH)
Reverse-proxy Angie (форк nginx) на :443, навешивает Basic-auth на сервисные сабдомены
Registry docker.stostayer.ru (BA те же, что Angie BA) — образы stostayer-web:<tag>
Стек Portainer stostayer-web Id 16, EndpointId 3, compose в Portainer-volume /data/compose/16/docker-compose.yml
Прод-тег (2026-06-17) 0.3.18. На хосте лежат старые теги 0.3.6…0.3.18 под откат
Образ packages/web/Dockerfile: node:16yarn installyarn build (nuxt build) → CMD yarn start. pm2/ecosystem.config.js в проде НЕ используется

Канал деплоя (build здесь → registry → Portainer)

1. Build образа (локально, offline)

Тег образа независим от packages/web/package.json version — это чисто image-тег, инкремент от прода (0.3.180.3.19). Кода/версий не бампать.

# в чекауте нужного коммита, .yarn/cache populated
docker build -f packages/web/Dockerfile -t docker.stostayer.ru/stostayer-web:<tag> .   # контекст = корень репо

Гочи build:

  • .dockerignore обязателен (его нет в репо) — иначе node_modules/.git улетают в build-контекст. Создать временный: node_modules **/node_modules .git apps docs build *.zip и т.п. (на явные COPY не влияет).
  • Локальная npmAuthToken: "${VERDACCIO_TOKEN}" в .yarnrc.yml (рабочая модификация, не в коммите) ломает build: yarn внутри образа падает Usage Error: Environment variable not found (VERDACCIO_TOKEN). Убрать эту строку из build-копии (потом вернуть).
  • Offline против .yarn/cache: добавить enableNetwork: false в .yarnrc.yml — depы пекутся из cache, verdaccio-токен в образе не нужен. (Cache содержит third-party зипы; их версии ESM-миграция не меняла, так что cache от master-tip обычно покрывает и более старые коммиты.)

2. Push в registry

echo '<ba_pass>' | docker login docker.stostayer.ru -u Victor --password-stdin
docker push docker.stostayer.ru/stostayer-web:<tag>

⚠️ ОДИН push, без retry-циклов. Инцидент 2026-06-17: retry-шторм (6 попыток, образ 3.29GB) на docker.stostayer.ru засветил egress нашего VPN → хостинг-провайдер забанил VPN-IP → ВЕСЬ HTTPS дом→хост (:443: сайт + registry + portainer) стал TLS-fail (schannel: failed to receive handshake), выглядело как «сайт лёг» (а TCP 443/SSH 20435 — открыты). Лечится сменой VPN. На client-инфру: одна попытка, сбой → стоп и к человеку, не долбить. (см. verify-on-real-client-not-own-curl-tests)

⚠️ Инцидент 2026-07-21: единичный large-push ТОЖЕ триггерит бан (не только retry-storm). Первый push 0.3.20 (3.33GB) с VPN → тот же VPN-IP бан → :443 к хосту TLS-fail с операторского IP (сайт для остальных посетителей работал — Angie access-log это подтвердил; SSH:20435 оставался открыт). Бан НЕ IP-специфичный для всех — для остальных сайт жив, режется только egress-IP источника push'а. Митигация: (a) resume-push после смены egressdocker push resumable, уже залитые слои «Layer already exists» (без re-upload), добивается только остаток + manifest → egress мал → повторный бан не триггерится; (b) build-on-host + push в localhost-registry (registry-контейнер на хосте, 127.0.0.1 — вообще без внешнего egress) — самый чистый путь для крупных образов. Не пытаться долбить упавший push повторно с того же egress.

3. Передеплой стека — с ХОСТА, мимо Angie BA

Portainer published только в docker-сеть (на host-localhost его НЕТ — там MinIO на :9000). Прямой Portainer-API из дома ломает Angie BA (Bearer затирает Basic → 401; auth-cookie не ставится). Решение — бить API по IP контейнера portainer с самого хоста:

# SSH (PuTTY plink; host-key уже известен)
plink -ssh -batch -hostkey SHA256:6Zk14J/UakVqBYm/fMPFrL3gosdBdyZOKZnbA0GVnD0 \
  -P 20435 -pw '<ssh_pass>' victor@91.222.236.225 "bash -s" <<'EOF'
base=http://172.18.0.2:9000      # docker inspect portainer → его IP в сети bridge
JWT=$(curl -s $base/api/auth -H 'Content-Type: application/json' \
  -d '{"username":"victor","password":"<app_pass>"}' | jq -r .jwt)
FILE=$(curl -s -H "Authorization: Bearer $JWT" $base/api/stacks/16/file | jq -r .StackFileContent)
NEW=$(printf '%s' "$FILE" | sed 's#stostayer-web:0.3.18#stostayer-web:<NEWTAG>#')
PAYLOAD=$(jq -n --arg c "$NEW" '{stackFileContent:$c, env:[], prune:false, pullImage:true}')
curl -s -w "\nHTTP=%{http_code}\n" -X PUT -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" -d "$PAYLOAD" "$base/api/stacks/16?endpointId=3"
EOF
  • docker на хосте — через sudo (victor не в группе docker): echo '<ssh_pass>' | sudo -S <cmd>. Sudo-пароль = SSH-пароль victor.
  • jq на хосте есть. Скрипт через bash -s/stdin — чисто с кавычками; UTF-8 round-trip compose не портит (в отличие от PowerShell Invoke-RestMethod, portainer-stack-management-vds gotcha #9).
  • pullImage:true → Portainer сам тянет новый тег из docker.stostayer.ru (хост-docker уже авторизован в registry).

4. Verify (read-only, без нагрузки на публичный сайт)

docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' | grep stostayer-web   # образ = новый тег, Up
docker logs --tail 20 stostayer-web                                             # чистый старт Nuxt, без ошибок/краш-лупа

Verify контента (напр. цен) — с хоста, прямым curl в nuxt-контейнер (Angie даёт 403 на no-UA запрос с самого хоста; обход — localhost:3000, host-network):

# на хосте (sudo docker + host-network контейнер на :3000)
curl -s -H "Host: www.stostayer.ru" -A "Mozilla/5.0" http://localhost:3000/<page> -o /tmp/v.html
grep -c "<expected-text>" /tmp/v.html

Гоча verify-URL: bare /remont-kondicionerov 301-редиректит на /remont/remont-kondicionerov (DB-driven oldPageRedirect/unit-slug mapping) — это CMS-контент-страница БЕЗ nuxt-компонента. Компоненты pages/<dir>/*.vue реально live на своих nuxt-маршрутах (напр. /remont-kondicionerov/{zapravka,diagnostika}-kondicionera) — верифицируй по ним, не по bare-URL.

Гоча локального smoke-контейнера: packages/web/config/default.json содержит прод-БД (www.stostayer.ru:3306 MariaDB + :1433 MSSQL), и www.stostayer.ru резолвится в публичный IP хоста → smoke-контейнер без env-overrides подключается к prod-БД клиента по интернету (read-only SSR, без writes, но всё равно нежелательно). Для чистого локального smoke — переопределяй data.sequelize.*.host env'ом на localhost/заглушку, либо глуши контейнер сразу после Server Listening.

Откат

Тот же PUT с прежним тегом (<NEWTAG>0.3.18), pullImage:true. 0.3.18 на хосте есть. RTO ~30-60с (пересоздание контейнера).

БЛОКЕР РАЗРЕШЁН 2026-07-21 (retrofit + snolla 0.7.6)

Legacy web снова собирается+запускается с master. Решение: ретрофит packages/web (require()→dynamic import() в 5 местах, 2 serverMiddleware → CJS) + фикс @snollajs/snolla@0.7.6 (bare-get-баг, см. ниже). Образ 0.3.20 собран, запушен, передеплоен на прод stostayer.ru, verify зелёный. Ретрофит лежит в working-tree stostayer.new (незакоммичен на 2026-07-21) — stostayer.new должен закоммитить его в master, иначе следующий legacy-web деплой упрётся в ту же стену. Секция ниже оставлена как история.

БЛОКЕР (история, 2026-06-17): легаси web НЕ пересобирается с текущего master (ESM-стена)

На 2026-06-17 фикс формы жалоб (120bc07, задача stostayer-web-complaint-form-deploy) выкатить не удалось — два ESM-барьера:

  1. Build-time: с master-tip yarn build (nuxt) падает SyntaxError: await is only valid in async functions...@stostayer/data после таски esm-data-dual грузит модели через top-level await; цепочка nuxt.config.js → @stostayer/api → @stostayer/data, Nuxt2 читает конфиг через jiti → top-level await недопустим. Корень — коммит c805e7e (ESM-миграция data/api).
  2. Runtime: cherry-pick 120bc07 на d02f740 (родитель c805e7e, «последний CJS-собираемый master») собирается, но контейнер краш-лупит: ERR_REQUIRE_ESMpackages/web/server/index.js:6 require('@snollajs/snolla'), а версия @snollajs/snolla из lockfile d02f740 уже ESM. Т.е. d02f740 не runtime-чистая.

Прод 0.3.18 жив только потому, что собран на ещё более старом стейте (CJS @snollajs/snolla, CJS data/api).

Вердикт (stostayer.new подтвердил 2026-06-17): легаси-web require()-ит 5 ESM-ставших пакетов — @snollajs/snolla (0.7.4, server/index.js:6), @stostayer/api (nuxt.config.js:3), @snollajs/content-api (0.8.0, nuxt.config.js:4), @stostayer/data (serverMiddleware/oldPagesRedirections.js:14 + redirections.js:14). Чтобы найти base где ВСЕ пять ещё CJS — надо к ~0.3.18 и потерять всё с тех пор. Чейз базы бесполезен. packages/web (node16/CJS/Nuxt2) не пересобираем ни с какого свежего дерева — весь dep-граф pure-ESM. 0.3.18 заморожен (последний собираемый артефакт).

Путь: фикс формы (120bc07) едет вместе с переездом формы в web4 (Opt 3). Ретрофит require→dynamic import() в 5 местах технически возможен на node16, но это часы на стек, который удаляется web4 — не рекомендуется. До web4-cutover — остаёмся на 0.3.18, admin-сессия код не правит (verify-on-real-client-not-own-curl-tests).

Образ 0.3.19 (краш-лупный) лежит в docker.stostayer.ruНЕ деплоить. Оставлен в registry по решению vitya (2026-06-17), не удаляем.

Связи

  • portainer-stack-management-vds — родственный паттерн (наш VDS), оттуда PowerShell-кодировочные гочи и redeploy-рецепт.
  • Доступы/инфра клиента (детально): stostayer.new/.wiki/concepts/client-infra-access.md.
  • Задача: .tasks/stostayer-web-complaint-form-deploy.md (🔵 blocked на victor/stostayer.new).