Files
admin/.wiki/concepts/snolla-live-prod-inplace-image-bump.md
vitya 9a92ff0b87 chore(vds): mem_limit 512m всему тиражу snolla (5 стеков live + compose-копии + вики-конвенция)
Тираж snolla (labtools.ru/17, emspb/18, labtools.pro/19, tandemmebel/20,
kupimknigi/21) шёл без mem_limit → cgroup-cap = вся память хоста (12.9 GiB),
одна течь могла съесть весь бокс. Выставил 512m (baseline ~100-200M, 2.5-5x запас)
на всех 5 живых стеках через env-preserving Portainer PUT + синхронизировал
source-of-truth compose. Все healthy, limit=536870912 подтверждён, labtools.pro 200.

Конвенция «app-стек обязан нести mem_limit» закреплена в
portainer-stack-management-vds § Convention + step 6 snolla-bump-рецепта.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:32:55 +03:00

9.5 KiB
Raw Blame History

title: snolla live-prod in-place image bump — рецепт (build → staging-acceptance → env-preserving PUT → live-smoke) type: concept tags: [snolla, vds, deploy, docker, portainer, runbook, recipe, in-place, image-bump, acceptance] related: ../entities/vds-kzntsv, portainer-stack-management-vds, labtools-vds-deploy-runbook, labtools.pro-vds-deploy-runbook, emspb-vds-deploy-runbook updated: 2026-07-05

snolla live-prod in-place image bump

Переиспользуемый рецепт для случая: сайт УЖЕ боевой на ../entities/vds-kzntsv в docker-стеке за traefik (cutover сделан ранее), надо обновить движок/образ на живом стеке in-place — без staging-first, без DNS-флипа. Отработан 2026-07-05 на тираже snolla 0.42.1: labtools.ru (стек 17), emspb.ru (18), labtools.pro (19). Класс риска = needs-human (боевой контейнер): боевой swap тега подтверждает оператор ПЕРЕД apply.

Отличие от per-site cutover-рунбуков (те про ПЕРВЫЙ вынос на VDS + DNS-флип): здесь домен уже на VDS, меняется только тег образа на существующем стеке. rollback = предыдущий тег (в registry) / стек PUT назад.

Инвариант: acceptance на НОВОМ образе ДО подмены (не наследовать dev-GREEN)

Прог даёт dev-верификацию с воркстейшна, но там LAN-DNS перехватывает прод-домены (см. memory workstation-lan-dns-serves-local-cms-copy) → его «vs бой» не вполне боевой. Перепрогонять гейты С VDS против живого прода-оракула, на throwaway-контейнере из нового образа, ДО касания живого стека.

Шаги

1. Build на VDS (обход traefik-499 при пуше больших слоёв)

git -C ~/projects/<site> archive --format=tar <sha> \
  | ssh vitya@89.253.255.94 'rm -rf ~/build/<img> && mkdir -p ~/build/<img> && tar -x -C ~/build/<img>'
# guard: config/default.json ABSENT в архиве (.dockerignore); пин snolla в apps/web/package.json = целевой
TOKEN=$(pass show vds-kzntsv/full-env | sed -n 's/^VERDACCIO_CI_TOKEN=//p')
ssh vitya@89.253.255.94 "cd ~/build/<img> && VERDACCIO_TOKEN='$TOKEN' docker build -f deploy/Dockerfile \
  --build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/<img>:<sha> -t registry.kzntsv.site/<img>:latest . \
  && docker push registry.kzntsv.site/<img>:<sha> && docker push registry.kzntsv.site/<img>:latest"

⚠️ Имя образа = имя стека (labtools.ru→labtools, labtools.pro→labtools-proНЕ коллизить!).

2. Throwaway-staging из env ЖИВОГО стека (не трогает прод)

Новый образ рендерит только с runtime-env (DB/S3/imgproxy/smtp). Копируем env живого контейнера:

ssh vitya@89.253.255.94 'set -e
  docker inspect <container> --format "{{range .Config.Env}}{{println .}}{{end}}" \
    | grep -vE "^(HOSTNAME|PATH|NODE_VERSION|YARN_VERSION|HOME)=" > ~/build/<img>/.staging.env
  docker run -d --name <img>-staging --env-file ~/build/<img>/.staging.env \
    -p 127.0.0.1:50NN:5000 registry.kzntsv.site/<img>:<sha>
  # health: poll curl -H "Host: <domain>" http://127.0.0.1:50NN/robots.txt → 200'

3. Completeness-gate С VDS (sitemap — ИНДЕКС, разворачивать!)

/sitemap.xml у snolla = sitemapindex (ссылки на под-sitemap'ы), НЕ список страниц. Разворачивать:

expand(){ base="$1"; shift; hdr=("$@")
  for su in $(curl -s "${hdr[@]}" "$base/sitemap.xml" | grep -oE "<loc>[^<]+" | sed "s|<loc>||"); do
    su="${su#https://<domain>}"; su="${su#https://www.<domain>}"; su="${su#$base}"
    curl -s "${hdr[@]}" "$base$su" | grep -oE "<loc>[^<]+" | sed "s|<loc>||; s|https://[^/]*||"
  done | sort -u; }

Два чека: (1) self-consistency — все page-locs нового sitemap → 200 на новом образе; (2) content-not-lost — каждая реальная страница прода-оракула → 200/3xx на новом. GREEN = 0 регрессий в 404/5xx.

  • Каталожные сайты — плюс order-парити секций ({% order by list_priority %}), новый == прод.
  • Benign: <loc> секции-с-одним-изделием отдаёт 301→изделие (прод идентичен) — не дефект.
  • 0.42.x реструктурирует sitemap (per-category product-sitemaps sitemap-catalog-<GUID>-products-1.xml); старые 0.28.x sitemap'ы бывали дефицитными (labtools.ru: sections 1→6, products 2→24). Сверять page-locs, не имена под-sitemap'ов.

4. Боевой swap — Portainer PUT с сохранением env (operator-gated)

НЕ через PS Invoke-RestMethod (корраптит кириллицу в compose — см. portainer-stack-management-vds). Node-скрипт: GET stack-file → заменить тег → PUT с Env массивом (name+value) живого стека. Полный скрипт ниже (§ put-stack.js).

export PK=$(pass show vds-kzntsv/full-env | sed -n 's/^PORTAINER_API_KEY=//p')
export PU=$(pass show vds-kzntsv/full-env | sed -n 's/^PORTAINER_URL=//p')
curl -sk -H "X-API-Key: $PK" "$PU/api/stacks/<ID>" -o /tmp/st<ID>.json   # Env с values
META=/tmp/st<ID>.json node put-stack.js <stackId> <endpointId=1> <oldTag> <newTag>

prune:false, pullImage:true. После PUT — poll docker inspect <container> --format {{.Config.Image}}+{{.State.Health.Status}} до <newTag>+healthy.

5. Live-smoke боевого домена + cleanup

curl -L https://www.<domain>/… ключевые страницы 200; order вживую; sitemap page-locs; TLS-серт (CN не должен дёрнуться — in-place swap серт не трогает). docker rm -f <img>-staging.

6. Закрытие

Compose source-of-truth (admin/host-stacks/vds-kzntsv/<img>.compose.yml) — обновить тег + коммент rollback. Проверить mem_limit на стеке (обязателен для всех app-стеков — см. portainer-stack-management-vds § Convention). snolla → 512m; если стек его ещё не несёт (старые до 2026-07-05) — добавить в этот же PUT и в compose-копию. Борд-таска 🟢, отчёт прогу (Notify=сам сайт, минуя workshop), rollback-тег остаётся в registry.

put-stack.js (env-preserving Portainer stack PUT)

// args: <stackId> <endpointId> <oldTag> <newTag> ; env: PK (api key), PU (url), META (/tmp/stNN.json)
const https = require('https'), fs = require('fs');
const [stackId, endpointId, oldTag, newTag] = process.argv.slice(2);
const K = process.env.PK, U = process.env.PU;
const meta = JSON.parse(fs.readFileSync(process.env.META, 'utf8')); // Env with values
const base = new URL(U);
function req(method, path, bodyObj) {
  return new Promise((res, rej) => {
    const body = bodyObj ? Buffer.from(JSON.stringify(bodyObj), 'utf8') : null;
    const r = https.request({ hostname: base.hostname, port: 443, path, method,
      headers: { 'X-API-Key': K, 'Content-Type': 'application/json',
        ...(body ? { 'Content-Length': body.length } : {}) }, rejectUnauthorized: false },
      resp => { let d = []; resp.on('data', c => d.push(c));
        resp.on('end', () => res({ status: resp.statusCode, body: Buffer.concat(d).toString('utf8') })); });
    r.on('error', rej); if (body) r.write(body); r.end();
  });
}
(async () => {
  const compose = JSON.parse((await req('GET', `/api/stacks/${stackId}/file`)).body).StackFileContent;
  if (!compose.includes(oldTag)) { console.error('OLD TAG NOT FOUND:', oldTag); process.exit(2); }
  const env = (meta.Env || []).map(e => ({ name: e.name, value: e.value }));
  const put = await req('PUT', `/api/stacks/${stackId}?endpointId=${endpointId}`,
    { stackFileContent: compose.split(oldTag).join(newTag), env, prune: false, pullImage: true });
  console.log('PUT', put.status, put.status >= 300 ? put.body.slice(0, 500) : 'OK');
})();

Гочи

  • Не гнать два тяжёлых docker build разом на VDS — последовательно (CPU/диск/слои).
  • MSYS на Windows ломает ~-пути при export MSYS_NO_PATHCONV=1 и корраптит git show rev:path (двоеточие) — для git show ставить MSYS_NO_PATHCONV=1, но тогда git -C ~/... пути ломаются; не смешивать в одном шелле.
  • Токен передавать через локальную переменную в двойных кавычках ssh-строки ('$TOKEN') — значение раскрывается локально, в транскрипт попадает литерал $TOKEN.
  • build-secret (VERDACCIO_TOKEN печётся в ARG/ENV build-стадии) — известный follow-up на всех snolla-Dockerfile; НЕ блокер (runtime-стадия отдельная, токена в финальном образе нет). Фикс = docker build-secret mount, на стороне прога.