Ingest .wiki/concepts/stostayer-web-deploy-runbook.md — канал деплоя легаси web (build offline → push docker.stostayer.ru → Portainer-стек 16 через container-IP API с хоста, мимо Angie BA) + rollback + гоча VPN-IP бан хостером при retry-push-шторме. БЛОКЕР задокументирован: легаси packages/web (node16/CJS/Nuxt2) не пересобрать ни с какого свежего дерева — 5 ESM-ставших депов (@snollajs/snolla, @snollajs/content-api, @stostayer/api, @stostayer/data ×2). 0.3.18 заморожен. Фикс формы (120bc07) едет с web4-cutover. Таска stostayer-web-complaint-form-deploy → 🔵 park (blocker=web4-cutover); попытка деплоя 0.3.19 откатана на 0.3.18, сайт восстановлен. Дохлый 0.3.19 оставлен в registry по решению vitya. + index.md, log.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.9 KiB
title, status, tags, related, updated
| title | status | tags | related | updated | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| stostayer-web — деплой легаси web на прод клиента (runbook) | live |
|
|
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:16 → yarn install → yarn build (nuxt build) → CMD yarn start. pm2/ecosystem.config.js в проде НЕ используется |
Канал деплоя (build здесь → registry → Portainer)
1. Build образа (локально, offline)
Тег образа независим от packages/web/package.json version — это чисто image-тег, инкремент от прода (0.3.18 → 0.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)
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, без ошибок/краш-лупа
Откат
Тот же PUT с прежним тегом (<NEWTAG> → 0.3.18), pullImage:true. 0.3.18 на хосте есть. RTO ~30-60с (пересоздание контейнера).
⛔ БЛОКЕР: легаси web НЕ пересобирается с текущего master (ESM-стена)
На 2026-06-17 фикс формы жалоб (
120bc07, задачаstostayer-web-complaint-form-deploy) выкатить не удалось — два ESM-барьера:
- 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). - Runtime: cherry-pick
120bc07наd02f740(родитель c805e7e, «последний CJS-собираемый master») собирается, но контейнер краш-лупит:ERR_REQUIRE_ESM—packages/web/server/index.js:6require('@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).