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

124 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 при пуше больших слоёв)
```bash
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 живого контейнера:
```bash
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'ы), НЕ список страниц. Разворачивать:
```bash
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).
```bash
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)
```js
// 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, на стороне прога.