Compare commits
232 Commits
838f51daa8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 23b1a47c14 | |||
| 95168e301a | |||
| ec3ac79272 | |||
| 535ed6170f | |||
|
|
50e263860d | ||
|
|
bec7784ffa | ||
|
|
4c4d19382b | ||
|
|
9705468ef4 | ||
| 019f5bae26 | |||
| 73ad7669b4 | |||
| dcc84cec4b | |||
| 863b11be28 | |||
| c196964d03 | |||
| d4278e79f9 | |||
| b1b0092cdc | |||
| a02c4b86a2 | |||
| 543ded79bd | |||
| 3e8c580b13 | |||
| f0965cf512 | |||
| 37c179601a | |||
| 5d613c3405 | |||
| ee7f5a2d18 | |||
| fe8e58824e | |||
| 632c843fd5 | |||
| 94e5d77c82 | |||
| 816c0e027d | |||
| 7e02d61128 | |||
| 8230f89198 | |||
| 2e18002958 | |||
| f3395b3a64 | |||
| 135da254f2 | |||
| b5839bd02d | |||
| d3d0ff48ed | |||
| 3adbe839f2 | |||
| 9a92ff0b87 | |||
| 3bc9992350 | |||
| 8e3e5b4353 | |||
| 0fd1c69515 | |||
| bcb4dc36be | |||
| 55a873731f | |||
| 5e1b0b2538 | |||
| ddb82815c7 | |||
| 173278dab7 | |||
| d3b442a775 | |||
| a57504c4e5 | |||
| 78f23a6561 | |||
| 96ae400941 | |||
| 6f79ffc64e | |||
| 84f4852f7f | |||
| 76f52661ca | |||
| a668313797 | |||
| 5ef3dc1559 | |||
| 4aa04d6f0f | |||
| ed4c7b669b | |||
| 056fdb2344 | |||
| a19fac880a | |||
| 048954bca1 | |||
| ea8672f09c | |||
| f1fb102328 | |||
| b299c7d5da | |||
| 725c889d78 | |||
| 5be6662c60 | |||
| 34df24be85 | |||
| 9475d9130d | |||
| ebcdd128f0 | |||
| 9daea393a2 | |||
| 38368adccd | |||
| 0e1bceb14d | |||
| 85fa2d7ad9 | |||
| f5a8a0256a | |||
| 0188507344 | |||
| 7127e92a4d | |||
| 3380b56a19 | |||
| 2e5adcf11e | |||
| d677f530b4 | |||
| a9f4f3a673 | |||
| ed99f2225b | |||
| 531d7f0fda | |||
| 253f427a55 | |||
| 47f24752aa | |||
| 353e3dcd8a | |||
| c3b722bb6d | |||
| a15fd65ae9 | |||
| 1a33c429d3 | |||
| 9447a86f2b | |||
| fea8cc0bb3 | |||
| 0382864afa | |||
| 8bae9e3afd | |||
| 4389f80783 | |||
| c0afaeca4e | |||
| 685f273571 | |||
| 7cd65fd84a | |||
| dca1bf77d9 | |||
| c3e845831d | |||
| 231e074779 | |||
| 8ae6a73435 | |||
| 415132b114 | |||
| fb47d5deb2 | |||
| 027cc79d23 | |||
| 3ad5ad446a | |||
| b166ca2d79 | |||
|
|
45ebe629c9 | ||
|
|
8f72f1fffd | ||
|
|
3958107715 | ||
|
|
e03d5c504a | ||
|
|
d29dae1ea9 | ||
| be2f9eb869 | |||
| 89edf9b6b9 | |||
| e5aeafa914 | |||
| b7f2edc5bd | |||
| 93b4ef1d13 | |||
| 85b13ae6a4 | |||
| 18a1e3ff61 | |||
| 202b7c32af | |||
| 28de8e60e0 | |||
| 53c2756265 | |||
| b1b862b28e | |||
| 23e00df3a7 | |||
| 120af8bfcb | |||
| 97fce8be92 | |||
| 5ffca96934 | |||
| 5be0855f4e | |||
| c677b23f81 | |||
| cad9f02c0e | |||
| af779a36e2 | |||
| 103df9fa46 | |||
| 5adf00735a | |||
| e66a8b8db3 | |||
| e3b87485c5 | |||
| b4cbb6dca2 | |||
| 9add1ac164 | |||
| 2911f2b3e5 | |||
| 637942e914 | |||
| 1725425972 | |||
| 5fd1a1d68b | |||
| 38e84ce20b | |||
| 2ba7b6df5c | |||
| 7ebab0a79d | |||
| ed581f4ccf | |||
| 82d724f108 | |||
| 8dc856b92d | |||
| f04426617c | |||
| 4c640dec65 | |||
| e001b8e331 | |||
| aeb82a6a70 | |||
| b332af66e4 | |||
| 1fb7fc2e90 | |||
| eb21b282d8 | |||
| 15c273c153 | |||
| 7f998d5222 | |||
| f8ca0794ad | |||
| e2338b6fdd | |||
| 4fd8288e8c | |||
| 352a5f6de4 | |||
| 9304847e05 | |||
| f6ab02f85c | |||
| 054fceeae8 | |||
| 949724729d | |||
| 47057484a9 | |||
| 1eb58aa940 | |||
| b686ac132f | |||
| 52098c821b | |||
| eca4f24e11 | |||
| 21e3d4266c | |||
| e6e2479970 | |||
| 2811b5a180 | |||
| 67fb97fbba | |||
| 99dba5138f | |||
| a704f9d313 | |||
| 2094f90e31 | |||
| 3c7ca3416b | |||
| 9cd6d4e22c | |||
| 7bba8cf853 | |||
| 539f1a07ed | |||
| 5cdedb376a | |||
| 9359f9ec4f | |||
| cacd582303 | |||
| b23ca3a890 | |||
| c87d7e7442 | |||
| fc245c23c7 | |||
| 5116abae07 | |||
| 4fc7cbf688 | |||
| b04033d15a | |||
| b0559fdbed | |||
| adecf36ffd | |||
| e3f9d24621 | |||
| c588326c28 | |||
| fea9fd4f53 | |||
| b8a07fee85 | |||
| 7ec6200901 | |||
| f102d9ec19 | |||
| 129b75a018 | |||
| 76c261cc4f | |||
| 2cf0b536de | |||
| 91fac7014a | |||
| 2a153d573c | |||
| 59cb5c2fbd | |||
| 063910e265 | |||
| 63708fe659 | |||
| 48a5cf397e | |||
| e9b0a9edbc | |||
| 068f7b2691 | |||
| 11554d45ac | |||
| dacc39a113 | |||
| c895cf52e6 | |||
| feeba28433 | |||
| 8238ef46a8 | |||
| 12c7dc5ab9 | |||
| 10e80dc036 | |||
| e82b9a905f | |||
| c84ccf4360 | |||
| 974afa1e52 | |||
| 744c384bec | |||
| b7696aacb3 | |||
| f91e51d937 | |||
| b430bce111 | |||
| ce8bf8a646 | |||
| 4f1a4cf436 | |||
| 7ae20c2d9f | |||
| 2eba1c5c01 | |||
| d746d3bd29 | |||
| 3246dabbb5 | |||
| 97757b89ad | |||
| 86c44c3db9 | |||
| 141efe9759 | |||
| bfb942cd1d | |||
| 3769e83640 | |||
| 439da3cef6 | |||
| fe41ee4422 | |||
| 1eefb4d45a | |||
| 2931a43242 | |||
| a02de817a5 |
18
.gitignore
vendored
18
.gitignore
vendored
@@ -20,3 +20,21 @@ Thumbs.db
|
||||
# Transient operational drops — findings captured в .tasks/, не в repo root.
|
||||
*-log.txt
|
||||
*-size.txt
|
||||
|
||||
# Operational scratch — per-task throwaway (API payloads, frozen mappings, ad-hoc dumps).
|
||||
# Контент часто содержит credentials (reindex bodies, stack PUTs) — никогда не commit.
|
||||
.scratch/
|
||||
|
||||
# smoke artifacts (ephemeral)
|
||||
pilonuxt-home-smoke.jpeg
|
||||
.playwright-mcp/
|
||||
|
||||
# harness scratchpad (plan-mode plans, session artifacts) — not project code
|
||||
.zcode/
|
||||
|
||||
# local throwaway scratch (plink scripts, recon dumps) — часто содержит креды,
|
||||
# никогда не commit. Аналог .scratch/.
|
||||
.tmp/
|
||||
|
||||
# task-runner runtime lock (not project content)
|
||||
.tasks/.lock
|
||||
|
||||
@@ -1,42 +1,50 @@
|
||||
---
|
||||
_last_updated_: 2026-05-25T07:50:00+03:00
|
||||
session_id: 2026-05-25-close-unify-backup-notifications
|
||||
_last_updated_: 2026-07-21T14:45:00Z
|
||||
session_id: 2026-07-21-stostayer-web-0320-deploy
|
||||
prev_session_id: 2026-07-21-ruvds-decomm
|
||||
---
|
||||
|
||||
# Next session handoff
|
||||
|
||||
## Recent commits
|
||||
## Эта сессия — деплой цен кондиционеров на stostayer.ru (legacy-web 0.3.20). ЗАВЕРШЁН ✅
|
||||
|
||||
- `tasks(unify-backup-notifications): close 🟢 — unified push+email across VDS/RUVDS/windows-host` (этот раунд)
|
||||
- `tasks: rm obsolete NEXT-SESSION-PROMPT.md (leaked creds in history since 2026-05-21)` (этот раунд)
|
||||
- `scripts(backup-notifications): unify push + email format across VDS/RUVDS/windows-host` (`73ad6dd0`)
|
||||
- `wiki(claude): add session-handoff trigger line`
|
||||
- `wiki(ingest): RUVDS IIS migration + daily backup pipeline`
|
||||
`stostayer.new` прислал deploy-request (повышение цен в `packages/web/components/ConditionerPrice.vue`, commit `4bbdbbf`). Задеплоено на прод. По ходу вскрылся и разрешён давний ESM-блокер.
|
||||
|
||||
### Что сделано
|
||||
- **ESM-блокер подтверждён + разрешён.** `packages/web` (Nuxt2/CJS) не собирался с master (ESM-миграция `c805e7e` data/api + ESM-переиздание snolla). Ретрофит 5 `require()`→dynamic `import()` (`nuxt.config.js`, `server/index.js`, 2 `serverMiddleware`→CJS). snolla-сессия починила bare-`get`-баг → опубликовала `@snollajs/snolla@0.7.6`. Образ `0.3.20` собран, build+runtime зелёные.
|
||||
- **Деплой на прод.** Push `0.3.20` в `docker.stostayer.ru`, redeploy стека 16 (`0.3.18`→`0.3.20`, Portainer PUT через SSH:20435 + внутренний IP portainer). Контейнер Up, `RestartCount=0`, обслуживает реальный трафик.
|
||||
- **Verify.** `/remont-kondicionerov/{zapravka,diagnostika}-kondicionera` → 200, компонент `ConditionerPrice` рендерит новые цены (4790/3290/6190/3990/7890/4840), старые (4400/2900/5800/7500/4450) ушли. Bare `/remont-kondicionerov` 301→`/remont/remont-kondicionerov` (CMS, без компонента) — verify по sub-страницам.
|
||||
|
||||
### Инцидент по ходу (разрешён)
|
||||
Первый push 3.33GB с VPN → VPN-IP бан у провайдера хоста → `:443` к хосту TLS-fail с операторского IP (сайт для остальных работал). Остановил push, оператор убрал VPN, **resume-push** добил остаток малым egress → без повторного бана. **Урок: единичный large-push тоже триггерит бан** (не только retry-storm). Memory `stostayer-push-ban-single-large-push`.
|
||||
|
||||
### ОСТАЁТСЯ (follow-up)
|
||||
- **🔴 ESM-ретрофит НЕ закоммичен/НЕ запушен в stostayer.new master.** stostayer.new-сессия сказала «коммить и пушь ты» (у неё изменений нет локально, ретрофит делался в working-tree .admin-сессии на этой воркстейшн). Commit-message одобрен stostayer.new. **Оператор НЕ дал «го» на пуш в эту сессию** (Rule 4 — пуш в git-master = outward). → Ретрофит лежит **незакоммиченным в `C:/Users/vitya/projects/stostayer.new` working-tree** (5 файлов + `package.json` snolla `^0.7.6` + `yarn.lock` + `.dockerignore`; `.yarnrc.yml` восстановлен в оригинал — чист; `.tasks/*` не трогать — ими stostayer.new управляет). **Следующая сессия: получить «го» оператора → `git add` ретрофит-файлов → commit (message ниже) → `git push origin master`.** НЕ делать `git checkout`/`stash`/`clean` в stostayer.new до коммита — потеряешь ретрофит. Если потерян — реконструируем из образа 0.3.20 (client registry) + ранбука + memory `stostayer-web-esm-retrofit-snolla-076`.
|
||||
- Commit-message (одобрен stostayer.new): `fix(web): ESM-deps retrofit — require()→dynamic import(), pin snolla ^0.7.6` + body (см. письмо `stostayer.new/.claude-inbox/2026-07-21T14-28-00Z-admin.md`).
|
||||
- После пуша stostayer.new тянет master + verify `packages/web` собирается.
|
||||
- **✅ snolla yank 0.7.5 — DONE.** snolla-сессия сняла `@snollajs/snolla@0.7.5` с verdaccio (`npm unpublish --force`), verify: 0.7.x = только 0.7.6, `latest`=0.42.1 не сбит, exact-0.7.5 консьюмеров не было. Линия 0.7.x полностью закрыта.
|
||||
- **Операторский IP 46.151.25.64** может ещё флапать по :443 к stostayer-хосту (бан egress-IP с инцидента) — для оператора; для посетителей сайта ограничений нет. Проходит само.
|
||||
- **Побочная нота (не чинил):** vds-ops `ops.docker.logs` на контейнере `verdaccio` висит 300с (docker-socket-proxy + тяжёлый stdout) — snolla-сессия не смогла взять verdaccio-логи через MCP. Если понадобится独立-verify verdaccio-логов — чинить vds-ops (таймаут/стрим) или грепнуть лог-файл на хосте.
|
||||
|
||||
### Wiki / memory обновлено
|
||||
- Ранбук `.wiki/concepts/stostayer-web-deploy-runbook.md`: БЛОКЕР→RESOLVED, push-секция (single-large-push gotcha), verify-секция (sub-страницы + smoke→prod-DB gotcha).
|
||||
- Memory: `stostayer-web-esm-retrofit-snolla-076`, `stostayer-push-ban-single-large-push`, `stostayer-smoke-hits-prod-db`.
|
||||
|
||||
### Креды/доступы — не менялись
|
||||
snolla-сессия: commit `63e5c60` (snolla repo, branch `fix/snolla-0.7.6-s3-get`), 0.7.6 в verdaccio. snolla-задача `snolla-074-get-bugfix-republish` закрыта (commit `46fe4040` в victor/snolla .tasks).
|
||||
|
||||
## Open треки
|
||||
|
||||
| Трек | Готовность | Entry-point |
|
||||
|---|---|---|
|
||||
| `iis-migration-to-ruvds` 🟡 | soak (kupimknigi + emspb DNS flipped 2026-05-24, оставшиеся 22 hostnames в очереди, tandemmebel.ru excluded) | `.tasks/iis-migration-to-ruvds.md` |
|
||||
| `infra-inventory` ⚪ | ready, не начат | `.tasks/infra-inventory.md` |
|
||||
| `books-bookva-user-whitelist-gathering` ⚪ | ready, не начат — long-lead gathering | `.tasks/books-bookva-user-whitelist-gathering.md` |
|
||||
| `books-vds-bookva-bootstrap` 🔵 | blocked on victor/books tenant-split Phase 4 | `.tasks/books-vds-bookva-bootstrap.md` |
|
||||
| `books-dns-cutover-bookva` 🔵 | blocked on books-vds-bookva-bootstrap | `.tasks/books-dns-cutover-bookva.md` |
|
||||
| stostayer.new закоммитить ESM-ретрофит | ⚪ за stostayer.new | working-tree stostayer.new (5 файлов + lockfile + .dockerignore) |
|
||||
| snolla yank 0.7.5 | ⚪ за snolla (го дано) | snolla выполнит `npm unpublish @snollajs/snolla@0.7.5 --force` |
|
||||
| books-vds → vds-kzntsv консолидация | оценено, не лезет по RAM/disk | За user. |
|
||||
| MinIO upload-acceptance on.snolla | ⚪ follow-up оператора | из prev handoff |
|
||||
|
||||
## Спроси user'а
|
||||
- **«го» на коммит+пуш ESM-ретрофита в `victor/stostayer.new` master** (незакоммичен в working-tree, stostayer.new ждёт пуш чтобы подтянуть+verify). Без него следующий legacy-web деплой упрётся в ту же ESM-стену.
|
||||
|
||||
- **windows-host deploy сделан?** Если `Get-FileHash C:\ProgramData\backup\run.ps1` matches `scripts/windows-host-fallback-backup-daily/run.ps1` (elevated check) — deploy ок, и сегодня ночью 03:00 MSK пушнёт в едином формате. Если нет — старый формат на одну ночь.
|
||||
- **На night-run 2026-05-25 → 26 push'и + emails пришли все 3?** Title pattern `<HOST> backup OK <date>` / subject `[<HOST>] backup OK <date>`. Если что-то отвалилось — `.bak-pre-unify` файлы лежат на каждом хосте для quick rollback.
|
||||
- **DNS swap для 22 hostnames** (`iis-migration-to-ruvds` next-action #3) — продолжаем или ещё soak?
|
||||
- **Plaintext creds в git history** (NEXT-SESSION-PROMPT.md): user сегодня acceptуnул risk без rotation. Если позже передумаем — rotate MSSQL SA + snolla DB user + stayer DB user + VM admin (`Pryakhin9`).
|
||||
|
||||
## Не делать (preemptive guards)
|
||||
|
||||
- **Не push без grant'а** (project-discipline Rule 4 — session-start = ask-mode). Каждая сессия начинает спрашивать заново.
|
||||
- **Не trigger полный backup-run для smoke** (21-81 min wall-clock). Smoke = только notify-channel через `scripts/<slug>/smoke-notify.ps1`.
|
||||
- **Не history-rewrite + force-push** для cleanup creds в `NEXT-SESSION-PROMPT.md` без явного user-approve — user уже принял risk на ad-hoc rotation, не на destructive history operations.
|
||||
- **Не fold `traefik`/`portainer` управление через SSH compose** на VDS — Portainer-managed per `.wiki/concepts/portainer-stack-management-vds.md`, кроме `traefik`+`portainer` themselves.
|
||||
|
||||
## Memory updates за сессию
|
||||
|
||||
- (нет на этом раунде — все правила/паттерны уже зафиксированы в project CLAUDE.md + global skills)
|
||||
## Не делать
|
||||
- **НЕ пушить упавший push повторно с того же egress** (VPN-ban).
|
||||
- **НЕ коммитить в stostayer.new master** без координации с stostayer.new-сессией (их репо, их ESM-стратегия) — я оставил ретрофит в working-tree, они коммитят.
|
||||
- snolla 0.7.5 yank — за snolla-сессией (го дано, но она исполняет).
|
||||
1511
.tasks/STATUS.md
1511
.tasks/STATUS.md
File diff suppressed because one or more lines are too long
70
.tasks/agents-task-runner-vds-deploy.md
Normal file
70
.tasks/agents-task-runner-vds-deploy.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# agents-task-runner-vds-deploy
|
||||
|
||||
> Slug legacy-named `-vds-deploy`; scope corrected 2026-06-08 — VDS-as-worker DROPPED.
|
||||
> Per-machine federation: each WORKER runs its own `agents-task-runner`, claims from the
|
||||
> SHARED Gitea board, runs the agent LOCALLY, commits/pushes. VDS hosts the Gitea board
|
||||
> ONLY (already up) — not a worker, nothing deploys on it.
|
||||
|
||||
## Goal
|
||||
Поднять `agents-task-runner` (the poller) на первой worker-машине, нацелив на shared
|
||||
Gitea-board. Машина клеймит только то, что умеет (capability-filter), `.admin` исключён
|
||||
из автономного клейма до governance L2/L3. Сначала DRY_RUN на prod-board, затем один
|
||||
живой цикл на реальной non-`.admin` таске: spawn → commit → push, verified.
|
||||
|
||||
## Key files
|
||||
- `~/projects/.common/lib/agents-task-runner/` — runtime (plain JS, `type:module`, v0.16.2)
|
||||
- `server.js` — reconcile loop entry (`npm start`)
|
||||
- `task-runner/server.js` — task-runner entry (`npm run task-runner`)
|
||||
- `lib/cross-project-poller.js` — claim loop, requirements-filter, confirm-gate
|
||||
- `lib/consult-backend/` — execution-policy governance (consult_policy + needs-human gate)
|
||||
- `docker-compose.yml` / `docker-compose.host.yml` / `Dockerfile`
|
||||
- `secrets.env.example` — секреты-шаблон (ANTHROPIC_API_KEY / Gitea token / projects-meta)
|
||||
- `config/` — node-config overlays
|
||||
- `~/projects/.common/lib/projects-meta-mcp/dist/` — уже собран (build-prereq для НЕГО, не для раннера)
|
||||
|
||||
## Decisions log
|
||||
- 2026-06-08: Взята в работу из ⚪ ready. **Build-prereq в Next action неточен** — раннер
|
||||
это чистый JS (нет tsconfig / `npm run build`, запускается `node server.js`), собирать
|
||||
нечего; `npm run build` релевантен только `projects-meta-mcp`, а его `dist/` уже есть.
|
||||
Источник заблуждения — общий BUILD-prereq из consult-execution-policy-governance review,
|
||||
где TS-only касался projects-meta-mcp.
|
||||
- 2026-06-08: Re-scoped (из STATUS-блока): VDS = board-host only, VDS-as-worker dropped.
|
||||
Poller живёт на worker-машинах (workstation / factory Linux).
|
||||
- 2026-06-08: guard `.admin/.tasks/policy.toml` (default_weight=needs-human) **запушен**
|
||||
(`94972472`) + **верифицирован**: прогон dist-модулей claim-gate на реальном `.admin` board
|
||||
→ все 4 claimable `.admin`-таски скипаются `reason=needs-human`, ноль проскочивших.
|
||||
- 2026-06-08: always-on bring-up НАЧАТ (`.env` RECONCILER_ENABLED=true, secrets.env, docker
|
||||
build + `mongo`+`reconciler` up через host-override) — затем **ОСТАНОВЛЕН и снесён** (`compose
|
||||
down`) по запрету user'а "не запускай поллеров без разрешения". host task-runner так и НЕ
|
||||
стартовал → ни одного claim/spawn не было. Конфиг (.env/secrets.env) остался на диске, готов.
|
||||
- 2026-06-08: **находка, валидирующая gating** — DRY_RUN-кандидат
|
||||
`OpeItcLoc03/MoreThenCms/cms-maljarka-https-mode-bug-fix` был ⚪ ready, хотя баг починен сегодня
|
||||
(cross-board stale: фикс жил в `.admin`, таска — на MoreThenCms board, никто не закрыл). Без
|
||||
паузы always-on заспавнил бы агента «чинить» уже починенное. Таска закрыта user-confirmed
|
||||
(MoreThenCms `890eae27`). Урок: до always-on нужен sweep board'ов на stale-claimable.
|
||||
|
||||
## Open questions
|
||||
- [ ] **GATE: запуск поллеров требует явного разрешения user'а** (2026-06-08 — "не запускай
|
||||
поллеров без моего разрешения"). До гранта — НЕ стартовать host task-runner и НЕ поднимать
|
||||
reconciler с RECONCILER_ENABLED=true.
|
||||
- [x] Первая worker-машина → **эта workstation** (user-решение 2026-06-08).
|
||||
- [x] Headless-auth → `claude login` достаточно (spawn наследует env хост-процесса; API-key не нужен).
|
||||
- [x] Capability-filter → `AGENT_CAPABILITIES=needs-internet` (минимально), `AGENT_RUNTIME=claude-opus`.
|
||||
|
||||
## Completed steps
|
||||
- [x] 2026-06-08: orientation — раннер локализован (`~/projects/.common/lib/agents-task-runner`,
|
||||
v0.16.2, JS), build-prereq разобран (фантом для раннера), common pull актуален (`e7b1143`).
|
||||
- [x] 2026-06-08: prereq-чек на workstation зелёный — node v22.22, docker 29.3.1 up,
|
||||
`projects-meta-mcp/dist/tasks-cli.js` собран, runner `node_modules` есть, `auth.toml` есть.
|
||||
- [x] 2026-06-08: **DRY_RUN-смок прошёл end-to-end.** Хост-процесс `task-runner` (PORT 3000,
|
||||
`DRY_RUN=1 AGENT_RUNTIME=claude-opus AGENT_CAPABILITIES=needs-internet SPAWN_MODE=local`),
|
||||
один тик через `POST /tasks/crossProjectAgentPoller` + `GET /runs/:id` (curl `--noproxy "*"`).
|
||||
Результат: `{dryRun:true, claimed:false, candidate:{project:"OpeItcLoc03/MoreThenCms",
|
||||
slug:"cms-maljarka-https-mode-bug-fix"}}`. Полный cross-boundary путь
|
||||
(reconciler→runner→CLI→реальный shared-board read) работает; claim-token НЕ записан, spawn НЕ
|
||||
вызван. Capability-фильтр (`needs-internet`) отработал — кандидат non-`.admin`. Процесс заглушён.
|
||||
|
||||
## Notes
|
||||
- HARD: `.admin` исключён из автономного клейма до governance L2/L3 (см. STATUS-блок § blocker).
|
||||
- anti-self-review: distinct machine-id на каждый worker.
|
||||
- Секреты per-machine: 600 root, не в git.
|
||||
92
.tasks/books-ops-mcp-host-promote.md
Normal file
92
.tasks/books-ops-mcp-host-promote.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# books-ops-mcp-host-promote
|
||||
|
||||
Promote `books-docker-proxy-ro` + `books-ops-mcp` из slovo's stack в host-level stack на books VDS. Дропнуть idle-stub `bookva-ops-mcp`. Идеология: **books VDS = наш host, slovo/bookva = клиентские tenant-стеки**; ops-mcp видит host-wide docker socket → один экземпляр на VDS, не per-tenant.
|
||||
|
||||
## Контекст / триггер
|
||||
|
||||
В сессии 2026-05-26 auto-deploy `tenant=all` упал на `bookva-ops-mcp not running (state=restarting)`. Hot-fix — добавлен `command: ["tail","-f","/dev/null"]` в `bookva-overlay/deploy/ops-mcp.compose.yml` (commit `437d024`), контейнер стал idle-stub без proxy/creds → deploy зелёный, MCP non-functional у bookva tenant'а. User отверг этот long-term shape: "books VDS — наш хост, slovo и bookva — клиентские стэки приложений".
|
||||
|
||||
Current legacy layout (наследие времени когда books-* совпадало и с host, и с единственным tenant'ом):
|
||||
- `books/deploy/ops-mcp.compose.yml` содержит **два** сервиса: `books-docker-proxy-ro` (host-level docker socket gateway) + `books-ops-mcp` (host-wide observability MCP). Deploy'ится через CI как часть slovo-tenant pipeline.
|
||||
- `bookva-overlay/deploy/ops-mcp.compose.yml` — idle-stub без функциональности, deployed via tenant=bookva pipeline.
|
||||
|
||||
## Target state
|
||||
|
||||
- Новый host-level stack (имя tentative — `books-ops`) с `books-docker-proxy-ro` + `books-ops-mcp`. Naming `books-*` сохраняется потому что **books = имя хоста**, не tenant.
|
||||
- Stack живёт в Portainer на books VDS как management-plane (как `traefik` + `portainer` сейчас — anti-pattern по project-discipline portainer rule, но user уже сделал исключение для management plane).
|
||||
- Deploy pipeline отдельный от tenant deploy (либо `deploy-host.yml` в `victor/books`, либо manual через Portainer как traefik/portainer).
|
||||
- `bookva-ops-mcp` stack удалён из Portainer + secret `PORTAINER_STACK_ID_BOOKVA_OPS_MCP` отозван из Gitea + `bookva-overlay/deploy/ops-mcp.compose.yml` удалён + `ops-mcp` убран из tenant=bookva service list в `books/.gitea/workflows/deploy.yml`.
|
||||
- Slovo's `books/deploy/ops-mcp.compose.yml` либо удалён, либо превращён в pointer на host-level (slovo больше не owns эти контейнеры).
|
||||
|
||||
## Open design questions
|
||||
|
||||
1. **Где жить host-level compose?**
|
||||
- (a) `.admin/host-stacks/books-vds/ops-mcp.compose.yml` (private ops-репо, единая точка для host-level VDS configs).
|
||||
- (b) Новый `victor/books-vds-host` репо.
|
||||
- (c) `victor/books/deploy/host/ops-mcp.compose.yml` (рядом, но другой sub-pipeline).
|
||||
- Recommendation: **(a)** — `.admin/` уже private + ops-flavored, host configs естественно ложатся рядом с `.tasks/` и `.wiki/concepts/portainer-stack-management-vds.md`.
|
||||
|
||||
2. **Как deploy'ить?**
|
||||
- Через CI: новый `deploy-host.yml` workflow в `.admin/` (или в books). Trigger: workflow_dispatch + push на host-stacks/.
|
||||
- Manual через Portainer UI: matches current `traefik`/`portainer` exception. User уже зафиксировал исключение из portainer-rule для management plane.
|
||||
- Recommendation: **manual** — management plane уже исключение, не плодим CI poll специально для одного stack'а.
|
||||
|
||||
3. **MARIADB_PASSWORD scope.** Сейчас slovo's books-ops-mcp получает `MARIADB_PASSWORD` (slovo's books-db root). Host-level ops-mcp должен видеть обе tenant DBs?
|
||||
- Если yes: env превращается в `SLOVO_MARIADB_PASSWORD` + `BOOKVA_MARIADB_PASSWORD`, MCP lib/server.js должен поддержать обе connection strings (probably TENANT param per query).
|
||||
- Если no (ops-mcp работает только с docker, без DB): drop `MARIADB_PASSWORD` env вообще, MCP не делает DB-queries.
|
||||
- Check actual usage in `books-ops-mcp` package source — нужно прочитать `victor/books/packages/ops-mcp/` чтобы понять что server.js делает с MARIADB_PASSWORD.
|
||||
|
||||
4. **Config volume.** Сейчас mount `/opt/books/books-ops-mcp/config/default.json` → MCP config. Host-level — тот же path или `/opt/books-vds-ops/config/default.json` (если хочется убрать books- prefix как tenant-namespace)?
|
||||
|
||||
5. **Какие ещё host-level контейнеры спрятаны в slovo stack?** Полезно одним проходом аудит сделать. Текущие host-level (по моему знанию): `traefik`, `portainer`. Возможные кандидаты на промоушн: `ntfy` (push.kzntsv.site — single endpoint per VDS), backup-cron'ы. См. [infra-inventory.md](infra-inventory.md) — частично пересекается.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Новый stack (`books-ops` или similar) running на books VDS, contains `books-docker-proxy-ro` + `books-ops-mcp`, healthy.
|
||||
- `bookva-ops-mcp` stack удалён из Portainer, secret отозван, compose-файл удалён из bookva-overlay.
|
||||
- `ops-mcp` service исключён из tenant=bookva pipeline в `books/.gitea/workflows/deploy.yml` (либо tenant-loop, либо service-list).
|
||||
- Slovo's `books/deploy/ops-mcp.compose.yml` либо удалён, либо ясно помечен как deprecated pointer. Slovo deploy pipeline больше не trying to deploy ops-mcp.
|
||||
- `ssh + docker exec -i books-ops-mcp node lib/server.js` продолжает работать (slovo's caller path не сломан, имя контейнера `books-ops-mcp` сохранено).
|
||||
- Smoke: MCP tool calls (например `mcp__books-ops__ops_docker_ps`) видят все tenant containers (`books-db`, `bookva-db`, `slovo-db` etc).
|
||||
|
||||
## Closure note (2026-05-26)
|
||||
|
||||
🟢 Closed одну сессию следом за заведением. **Решение design Qs:**
|
||||
- **Q1 host compose location:** `.admin/host-stacks/books-vds/ops-mcp.compose.yml` (canonical, private ops-репо).
|
||||
- **Q2 deploy mechanism:** manual через Portainer UI (как traefik/portainer — management plane exception).
|
||||
- **Q3 MARIADB_PASSWORD scope:** Option A — host instance видит только slovo's books-db + slovo's Mongo. ops-mcp source connects к ОДНОМУ MariaDB pool и ОДНОМУ MongoClient; multi-tenant DB observability — design call отложен (если нужно: extend lib + separate pools per-tenant, либо drop DB tools оставив только docker).
|
||||
- **Q4 config volume:** path unchanged (`/opt/books/books-ops-mcp/config/default.json` на host'е). Books-prefix остался — host = books VDS.
|
||||
- **Q5 audit прочих host-level кандидатов:** punt, отдельная задача (kicks off `infra-inventory`).
|
||||
|
||||
**Executed:**
|
||||
- ✅ Read `books/packages/ops-mcp/{server,mariadb,mongo}.js` + `config/{default,custom-environment-variables}.json` — locked design Qs.
|
||||
- ✅ Created `.admin/host-stacks/books-vds/ops-mcp.compose.yml` (host-level canonical). Image hardcoded `:master` (manual update, не env-var).
|
||||
- ✅ Portainer stack 26 (books-ops-mcp) **in-place PUT** с новой compose. Containers `books-docker-proxy-ro` + `books-ops-mcp` остались running без recreate. Env `MARIADB_PASSWORD` preserved.
|
||||
- ✅ Portainer stack 43 (bookva-ops-mcp) **deleted**. Container bookva-ops-mcp gone (404).
|
||||
- ✅ `victor/books ae3ab14`: deploy.yml drop ops-mcp service + STACK_ID_*_OPS_MCP envs + healthcheck case; `deploy/ops-mcp.compose.yml` deleted.
|
||||
- ✅ `victor/bookva-overlay 50f5bbb`: `deploy/ops-mcp.compose.yml` deleted (idle-stub no longer needed).
|
||||
- ✅ Gitea secrets revoked: `PORTAINER_STACK_ID_BOOKVA_OPS_MCP` + `PORTAINER_STACK_ID_OPS_MCP`. `SLOVO_OPS_MCP` was never set (fallback chain used OPS_MCP).
|
||||
- ✅ Smoke: `books-ops-mcp State=running`, `books-docker-proxy-ro State=running`, `bookva-ops-mcp` 404.
|
||||
|
||||
**Open follow-ups (если возникнет потребность):**
|
||||
- Multi-tenant DB observability через MCP — extend lib (per-tenant pool array, TENANT param на queries) ИЛИ drop DB tools оставив только docker.
|
||||
- Stack name "books-ops-mcp" → "books-ops" в Portainer (cosmetic; rename требует recreate, не делал).
|
||||
- Audit прочих host-level кандидатов (ntfy? backup-cron?) — через `infra-inventory`.
|
||||
|
||||
**Status:** closed 2026-05-26
|
||||
**Where I stopped:** all 10 steps done; Stack id=26 промоутен в host-level семантически без container churn; bookva-side полностью удалён.
|
||||
**Next action:**
|
||||
1. Прочитать `victor/books/packages/ops-mcp/` источник — понять что MCP реально делает с `MARIADB_PASSWORD` (resolves design Q3).
|
||||
2. Принять design decisions Q1–Q5 (recommendation drafts выше).
|
||||
3. Создать host-level compose в выбранном месте.
|
||||
4. В Portainer: создать новый stack `books-ops` (manual upload compose) → start → verify `docker exec books-ops-mcp` works.
|
||||
5. В Portainer: stop + delete `bookva-ops-mcp` stack.
|
||||
6. В Gitea secrets: отозвать `PORTAINER_STACK_ID_BOOKVA_OPS_MCP`.
|
||||
7. В `victor/books/.gitea/workflows/deploy.yml`: убрать `ops-mcp` из tenant=bookva service list (или из `services="api web job-scheduler ops-mcp"` глобально + добавить host-level deploy в отдельный workflow).
|
||||
8. В `victor/bookva-overlay`: `rm deploy/ops-mcp.compose.yml`.
|
||||
9. В `victor/books`: `rm deploy/ops-mcp.compose.yml` (или превратить в README pointer).
|
||||
10. Manual smoke: `mcp__books-ops__ops_docker_ps` видит bookva-* containers.
|
||||
|
||||
**Blocker:** —
|
||||
**Branch:** master
|
||||
<!-- created-by: vitya / 2026-05-26 / trigger: deploy run#437/439 fail + user ideology clarification «books VDS = host, slovo/bookva = клиентские стэки» -->
|
||||
61
.tasks/books-vds-backup-daily-kreknin.md
Normal file
61
.tasks/books-vds-backup-daily-kreknin.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# books-vds-backup-daily-kreknin
|
||||
|
||||
Ежедневный backup pipeline books VDS (`89.253.255.133`, host4g.ru, CentOS 7) → kreknin Synology (`195.19.90.188:/volume1/NetBackup/books-vds/<date>/`) в 06:00 MSK.
|
||||
|
||||
## Goal
|
||||
|
||||
Восстановимость books-стека после полного отказа VDS: DB dumps + MinIO blobs + ES snapshot + app configs + traefik state + system config на NAS с 7-day retention.
|
||||
|
||||
## Key files
|
||||
|
||||
- `scripts/books-vds-backup-daily-kreknin/run.sh` — backup logic; deploy → `/opt/stacks/backup/run.sh` на VDS.
|
||||
- `scripts/books-vds-backup-daily-kreknin/.env.example` — placeholder; live `.env` built from `pass show`.
|
||||
- `scripts/books-vds-backup-daily-kreknin/README.md` — install + revert recipe.
|
||||
- `.wiki/entities/books-vds.md` — host entity page.
|
||||
- `.wiki/sources/books-vds-backup-daily-kreknin-2026-05-25.md` — standup chronology (создаётся при closure).
|
||||
|
||||
## Decisions log
|
||||
|
||||
- 2026-05-25: pattern mirror `scripts/vds-backup-rsync-kreknin/run.sh` (VDS-infra). Same `vds-backup` ntfy topic, same SMTP relay (`snolla-smtp` Yandex), same `--link-dest` rsync hardlink pattern, same `RETENTION_DAYS=7`.
|
||||
- 2026-05-25: **ES snapshot via REST API**, not data-dir rsync. One-time stack edit `/usr/docker/elasticsearch/docker-compose.yml`: added env `path.repo=/snapshots` + bind `./snapshots:/snapshots`. Registered repo `kreknin` (type=fs, location=/snapshots, compress=true).
|
||||
- 2026-05-25: **`curl --ssl-reqd` SMTP** instead of msmtp (CentOS 7 EOL repo issue). Yandex 587 STARTTLS works через native curl без msmtprc setup.
|
||||
- 2026-05-25: `StrictHostKeyChecking=yes` (NOT `accept-new`) — CentOS 7 OpenSSH 7.4 не поддерживает `accept-new`. kreknin host key pre-populated в `/root/.ssh/known_hosts` на VDS bootstrap.
|
||||
- 2026-05-25: `books-job-scheduler-mongo` — **без auth** (нет `MONGO_INITDB_ROOT_*` env). `mongodump --archive` без `--uri` достаточен. shared `mongo` — root auth (uri credential).
|
||||
- 2026-05-25: **Cron time 06:00** MSK — после VDS-infra (05:00) + windows-host (05:30), даёт break перед US-EU business start.
|
||||
- 2026-05-25: `BOOKS-VDS` host label в ntfy title — отличать от обычного `VDS` (infra) на shared `vds-backup` topic.
|
||||
- 2026-05-25: Consolidated `books-vds-portainer/full-env` → `books-vds/full-env` (one-file-per-host pattern, parity с vds-kzntsv).
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] (resolved 2026-05-25) `path.repo` на ES — required one-time stack edit. Done, recreated container, snapshot test SUCCESS.
|
||||
- [ ] Migrate SSH-compose stacks (ES/mongo/minio/books-db/traefik) под Portainer — **отдельная задача после backup pipeline live**.
|
||||
- [ ] kreknin space check — current free 5.7T (per `kreknin-synology.md` 2026-05-19); per-day footprint TBD после initial sync. Quarterly disk audit на NAS.
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] Phase 0: ES path.repo bootstrap + snapshot repo `kreknin` registered + test snap SUCCESS
|
||||
- [x] Phase 1: написал `run.sh` + `.env.example` + `README.md` локально
|
||||
- [x] Phase 2: deployed `/opt/stacks/backup/{run.sh,.env}`, chmod 600 .env / 755 run.sh
|
||||
- [x] Phase 3: generated `/opt/stacks/backup/kreknin-key` ed25519 на books VDS
|
||||
- [x] Phase 4: pubkey deployed на kreknin `~vitya/.ssh/authorized_keys`, dest `/volume1/NetBackup/books-vds/` created
|
||||
- [x] Phase 5: kreknin host key pre-populated `/root/.ssh/known_hosts`, SSH path verified
|
||||
- [x] Phase 6: smoke run (initial sync) — 4.87GB in 13m28s, ntfy push sent, **email send pending fix → fixed → re-tested OK**
|
||||
- [x] Phase 6a: SMTP fix — `smtp://` + `--ssl-reqd` дала 30-сек timeout (curl попытался STARTTLS на implicit-TLS-only порту 465). Fix `smtps://` schema + CRLF headers + Date header. Re-tested standalone — SMTP send OK.
|
||||
- [x] Phase 7: installed `/etc/cron.d/books-vds-backup` (`0 6 * * * root /opt/stacks/backup/run.sh`); crond active
|
||||
- [x] Phase 8: wiki — `entities/books-vds.md` created; `concepts/books-ssh-access.md` renamed → `vds-kzntsv-ssh-access.md` (was lying about books); `sources/books-vds-backup-daily-kreknin-2026-05-25.md` created; `index.md` updated for all three
|
||||
- [ ] Phase 9: commit (no push without grant)
|
||||
- [ ] **PENDING USER VERIFY:** ntfy push `BOOKS-VDS backup OK 2026-05-25` received on phone? Email `[BOOKS-VDS] backup OK 2026-05-25` + standalone fix-test message in inbox?
|
||||
|
||||
## Notes
|
||||
|
||||
- root password `Pryakhin9~` — сохранён в `pass show books-vds/full-env BOOKS_VDS_ROOT_PASS`. SSH key `id_ed25519_books_ops` уже был authorized (pre-existing bootstrap), password держим как fallback.
|
||||
- Pre-existing `id_ed25519_books_ops` на workstation предполагает что прошлый bootstrap забыт; не повторяем generate.
|
||||
- ES в production books currently **не используется** (per user statement) — recreate-downtime 30с не impact'нул.
|
||||
- `vds-kzntsv` ≠ `books-vds`. Wiki [[../concepts/books-ssh-access]] до 2026-05-25 ошибочно описывала vds-kzntsv как books shared VDS — fix в Phase 8.
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- [[../wiki/entities/books-vds]] — host entity.
|
||||
- [[../wiki/entities/kreknin-synology]] — backup target.
|
||||
- [[../scripts/vds-backup-rsync-kreknin/run.sh]] — pattern source (VDS-infra backup).
|
||||
- [[../scripts/ruvds-backup-daily-kreknin/README.md]] — pattern source (RUVDS dual-channel notify).
|
||||
108
.tasks/books-vds-stacks-to-portainer.md
Normal file
108
.tasks/books-vds-stacks-to-portainer.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# books-vds-stacks-to-portainer
|
||||
|
||||
Migrate SSH-compose стеки на books VDS (`89.253.255.133`) в Portainer-managed через API. Pattern parallel `[[.wiki/concepts/portainer-stack-management-vds]]` (VDS-infra retro-migration 2026-05-22), но другой endpoint/Portainer URL/stack dirs.
|
||||
|
||||
## Goal
|
||||
|
||||
Унифицировать ops surface на books VDS — все docker-compose стеки управляются через `https://portainer.kzntsv.site` (endpoint 1). Конец hybrid pain'у (Portainer для `books-{api,web,scheduler,...}` + SSH-compose для `elasticsearch/mongo/minio/books-db/traefik/proxy-chain`). CI deploy workflow (`.gitea/workflows/deploy.yml` в victor/books) только через Portainer API; SSH-compose vert не подключается; debug требует знать «is it Portainer or SSH compose» — больно.
|
||||
|
||||
## Scope
|
||||
|
||||
| Stack | Current dir | Adapt notes |
|
||||
|---|---|---|
|
||||
| `books-db` (MariaDB latest) | `/usr/docker/books-db/` | bind `./data` → absolutize. Env inline (no env_file). |
|
||||
| `mongo` (shared, 4.2) | `/usr/docker/mongo/` | bind `./data/{db,configdb}` → absolutize. Env inline. |
|
||||
| `minio` (2020-07-13) | `/usr/docker/minio/` | bind `./data` → absolutize. Env inline. |
|
||||
| `elasticsearch` (7.10.0) | `/usr/docker/elasticsearch/` | bind `./data` + `./snapshots` (added 2026-05-25 для backup) → absolutize. path.repo env preserved. |
|
||||
| `proxy-chain` | `/usr/docker/proxy-chain/` | bind `./config` → absolutize. Internal-only (no traefik). |
|
||||
| `imgproxy` (2 containers: imgproxy + imgproxy-nginx) | `/usr/docker/imgproxy/` | added 2026-05-25 (Phase 0 discovery). Binds `./nginx/cache` + `./nginx/nginx.conf` → absolutize. Secrets inline. |
|
||||
|
||||
**Skip (management plane):**
|
||||
- `traefik` — `/usr/docker/traefik/` — recreate ломает access всему остальному.
|
||||
- `portainer` — `/usr/docker/portainer/` — self-managed; can't recreate себя.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- 6 стеков (books-db, mongo, minio, elasticsearch, proxy-chain, imgproxy) появляются в `GET /api/stacks` под endpoint 1 (наряду с книгами `books-{api,web,scheduler,ntfy,ops-mcp}` + `chrome`).
|
||||
- Контейнеры работают, бинды смонтированы, data preserved (named volumes + binds через migration).
|
||||
- Smoke: каждый стек проходит health check соответствующий своему типу:
|
||||
- books-db: `docker exec books-db mariadb -uroot -p... -e 'SELECT 1'` → `1`
|
||||
- mongo: `docker exec mongo mongosh --eval 'db.adminCommand({ping:1})'` → `ok:1`
|
||||
- minio: HTTP `https://elasticsearch.kzntsv.site` (или внутренний minio endpoint) — `200 OK`
|
||||
- elasticsearch: `docker exec elasticsearch curl localhost:9200/_cluster/health` → `green|yellow`
|
||||
- proxy-chain: smoke через тестового агента (TBD)
|
||||
- Backup pipeline (`books-vds-backup-daily-kreknin`) продолжает работать post-migration (compose dirs не используются runtime после migration, but `/usr/docker/<svc>/data` binds preserved — rsync paths остаются валидны).
|
||||
- Document в `.wiki/concepts/portainer-stack-management-books-vds.md` (или extend existing `portainer-stack-management-vds.md` с books VDS sections).
|
||||
|
||||
## Decisions log
|
||||
|
||||
- 2026-05-25: создана по запросу user'а после успешного `books-vds-backup-daily-kreknin`. Promised after backup pipeline live.
|
||||
- 2026-05-25: pattern reuse — `.wiki/concepts/portainer-stack-management-vds.md` migration script adaptable. Diff for books VDS: PORTAINER_URL, endpoint ID = 1, stack dirs `/usr/docker/<svc>/` instead of `/opt/stacks/<svc>/`. PAT в pass под `books-vds/full-env BOOKS_PORTAINER_API_KEY` (may need fallback to JWT if 401).
|
||||
- 2026-05-25 (session start): user confirmed **Endpoint 6 = stostayer VDS, НЕ наш** → scope не расширяем, Q1 closed. Auto-push grant active на сессию.
|
||||
- 2026-05-25 (Phase 0): SSH probe 5 target compose. Findings: (1) **no env_file** anywhere — inline env stays в stackFileContent, отдельный API env array не нужен; (2) **no surprise named volumes** — all bind, Q2 close to resolved; (3) mongo container image lost tag (digest-only) — fresh `mongo:4.2` pull при Portainer create acceptable; (4) Portainer API list endpoint 1 = 6 stacks (books-* + chrome), endpoint 6 = stostayer 8 stacks (confirmed not ours); (5) **discovered 6th stack:** `/usr/docker/imgproxy/` — `imgproxy` + `imgproxy-nginx` 2-container compose serving `imgproxy.kzntsv.site`, was missing from entity wiki. User confirmed → added to scope.
|
||||
- 2026-05-25 (Phase 1): adapter script `scripts/books-vds-portainer-migration/migrate.sh` clone-adapted from VDS-infra pattern. Auth via `X-API-Key` header (PAT works, JWT fallback not needed). Down step via `docker rm -f` by `com.docker.compose.project` label — bypasses both v1 ContainerConfig bug + any v2 recreate quirks; no compose binary dependency at all.
|
||||
- 2026-05-25 (Phase 2): **pre-flight surprises:** (1) no `jq` on books VDS — installed static jq 1.7.1 binary via direct GitHub download (yum dead, CentOS 7 EOL since 2024-06, mirrors retired); (2) no `docker login registry.kzntsv.site` configured — accepted (cached images covered all migrations, no force-pull issue). Both documented в concept doc.
|
||||
- 2026-05-25 (Phase 2): migration executed in ladder. Per-stack Portainer Ids + duration: proxy-chain=28 (~10s recreate, HTTP 400 = service responding), imgproxy=29 (~15s, `imgproxy.kzntsv.site` → 200), minio=30 (~10s, `/minio/health/live` → 200), mongo=31 (~13s, ping=1, books-api/task-runner stayed Up через recreate window), books-db=32 (~15s, `SELECT 1` → 1, books-api connection-pool reconnected), elasticsearch=33 (~37s, cluster yellow expected single-node, **snapshot repo `kreknin` preserved** through bind).
|
||||
- 2026-05-25 (Phase 3): created `.wiki/concepts/portainer-stack-management-books-vds.md` documenting diffs from vds-kzntsv pattern + per-host inventory + pre-flight quirks (jq install, no registry login). Updated `entities/books-vds.md` table: 6 stacks moved SSH-managed → Portainer-managed.
|
||||
|
||||
## Open questions
|
||||
|
||||
- [x] ~~**Endpoint 6** в books Portainer — stostayer VDS?~~ Resolved 2026-05-25: stostayer **не наш**, scope не расширяем.
|
||||
- [x] ~~**Volume preservation strategy**~~ Resolved 2026-05-25 (Phase 0 probe): все 6 target стеков — bind-only, no named volumes. `docker compose down` без `-v` preserves binds (paths остаются на disk).
|
||||
- [x] ~~**CI implications**~~ Resolved 2026-05-25: books-* stacks (CI-deployed) уже Portainer-managed pre-migration. Migrated stacks (mongo/minio/etc) — infra, не в CI flow. Никаких изменений в `victor/books/.gitea/workflows/deploy.yml` не нужно.
|
||||
- [x] ~~**ES snapshot path.repo edit preservation**~~ Verified 2026-05-25 (Phase 2 stack 6 smoke): `curl localhost:9200/_snapshot` returned `kreknin` repo intact post-migration. `path.repo=/snapshots` env preserved через `stackFileContent`, bind `/snapshots` → `/usr/docker/elasticsearch/snapshots` preserved.
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] Phase 0 — SSH probe 6 compose dirs (5 original + imgproxy discovered), Portainer API auth verified, surprise imgproxy stack added to scope.
|
||||
- [x] Phase 1 — adapter script `scripts/books-vds-portainer-migration/migrate.sh` written + committed (`141efe97`). Pre-flight: jq 1.7.1 static binary installed on books VDS.
|
||||
- [x] Phase 2 — 6 stacks migrated in ladder, all smoked green. Books-api + books-task-runner reconnected transparently through mongo + books-db recreate windows.
|
||||
- [x] Phase 3 — wiki concept created (`portainer-stack-management-books-vds.md`), entity updated (`books-vds.md` SSH-managed → Portainer-managed table flipped).
|
||||
|
||||
## Acceptance verification
|
||||
|
||||
- ✓ 6 стеков (proxy-chain, imgproxy, minio, mongo, books-db, elasticsearch) появляются в `GET /api/stacks` под endpoint 1 — Ids 28-33 confirmed via API response after each migration.
|
||||
- ✓ Контейнеры работают, бинды смонтированы — `docker ps` Up для каждого post-recreate. Data preserved: mongo ping=1, books-db SELECT 1=1, ES cluster yellow with active shards (data intact).
|
||||
- ✓ Smoke per stack type — все 6 commands green (см. concept doc § Smoke commands).
|
||||
- ⚠ Backup pipeline продолжает работать — **verified by inspection** (bind paths `/usr/docker/<stack>/data` + `.../snapshots` preserved, backup script paths unchanged). Full pipeline run pending next scheduled 06:00 MSK.
|
||||
- ✓ Wiki documented — `.wiki/concepts/portainer-stack-management-books-vds.md` + index updated + entity flipped.
|
||||
|
||||
## Plan (sketch)
|
||||
|
||||
**Phase 0 — pre-flight:**
|
||||
1. Read each stack's `docker-compose.yml` + `.env` (if exists).
|
||||
2. Document existing env vars + binds per stack.
|
||||
3. Backup current compose files (`docker-compose.yml.bak-pre-portainer-migration-2026-MM-DD`).
|
||||
|
||||
**Phase 1 — Portainer API setup:**
|
||||
1. Verify PAT works (`curl ... /api/stacks`). If 401 → fallback to JWT via admin password (from `pass show books-vds/full-env BOOKS_VDS_ROOT_PASS` adapted to Portainer admin — may differ).
|
||||
2. Build adapter script `scripts/books-vds-portainer-migrate.sh` (clone-and-adapt of VDS-infra version, see [[.wiki/concepts/portainer-stack-management-vds]] § Migration script).
|
||||
|
||||
**Phase 2 — per-stack migrate (one at a time, verify between):**
|
||||
1. **proxy-chain** (lowest blast radius — no traefik exposure)
|
||||
2. **imgproxy** (2-container compose, serves `imgproxy.kzntsv.site` — drive-by added 2026-05-25)
|
||||
3. **minio** (read-mostly, books-api retries OK)
|
||||
4. **mongo** (shared) — books-api + books-task-runner retry-tolerant? **VERIFY first**
|
||||
5. **books-db** (MariaDB) — books-api uses connection-pool, brief downtime survivable
|
||||
6. **elasticsearch** — currently unused (per user), zero impact
|
||||
|
||||
**Phase 3 — wiki + close:**
|
||||
1. Document в `.wiki/concepts/portainer-stack-management-books-vds.md` (or extend existing).
|
||||
2. Update `.wiki/entities/books-vds.md` — flip каждый стек в SSH-managed → Portainer-managed table.
|
||||
3. Update STATUS.md → 🟢.
|
||||
|
||||
## Notes
|
||||
|
||||
- **`books-job-scheduler-mongo`** уже Portainer-managed (часть `books-job-scheduler` stack). Не в scope этой таски.
|
||||
- **`books-docker-proxy` + `books-docker-proxy-ro`** — часть `books-ops-mcp` stack (Portainer). Not in scope.
|
||||
- **Buildx builders** (`buildx_buildkit_builder-*`) — transient containers без compose, игнорируем.
|
||||
- **Backup pipeline depend** — после migration paths `/usr/docker/<svc>/data` остаются как bind sources, поэтому `books-vds-backup-daily-kreknin/run.sh` не требует изменений. Compose files (`/usr/docker/<svc>/docker-compose.yml`) станут reference only после migration.
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- [[.wiki/concepts/portainer-stack-management-vds]] — VDS-infra retro-migration pattern (source of script).
|
||||
- [[.wiki/entities/books-vds]] — host factsheet (will update post-migration).
|
||||
- [[books-vds-backup-daily-kreknin]] — backup taskdone 2026-05-25; depend on stable bind paths.
|
||||
- victor/books `deploy/README.md` — books-* CI deploy через Portainer API (already migrated).
|
||||
|
||||
<!-- created-by: claude / 2026-05-25 / trigger: user-requested follow-up after books-vds-backup-daily-kreknin 🟢 -->
|
||||
24
.tasks/bookva-es-snapshot-repo.md
Normal file
24
.tasks/bookva-es-snapshot-repo.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# bookva-es-snapshot-repo — отдельный ES-снапшот для bookva tenant
|
||||
|
||||
**Status:** 🟢 closed 2026-06-12 (в ту же сессию). Сделано полностью — не отложено.
|
||||
**Close note:** Portainer stack 37 пересоздан (PUT API) с `path.repo=/snapshots` + bind `/usr/docker/bookva-es/snapshots` (1000:0), том bookva-es-data сохранён (epz/products целы). Repo `kreknin` зарегистрирован, snapshot-блок добавлен в `run.sh`. Поймана и устранена коллизия basename `snapshots` (rsync источник bookva-es = родительский `/usr/docker/bookva-es`). Verified green: `BOOKS-VDS backup OK 10m28s`, на kreknin раздельно `snapshots/`(slovo, index-41) и `bookva-es/snapshots/`(index-4).
|
||||
|
||||
## Контекст
|
||||
|
||||
`bookva-es` (отдельный ES 7.10 контейнер на [[../.wiki/entities/books-vds]]) **не снапшотится**: `path.repo` не сконфигурирован → REST `_snapshot` PUT невозможен (`repos={}`). bookva-db/mongo/minio уже в daily-бэкапе с 2026-06-12, bookva-es — нет.
|
||||
|
||||
**Почему не горит:** индексы bookva-es (`epz` 820604, `products` 105922) **идентичны slovo ES** (те же counts), который снапшотится daily в repo `kreknin`. То есть данные de-facto есть в бэкапе через slovo-снапшот. Плюс ES — derivative index, реконструируем reindex'ом из БД. Это покрытие-по-проксе, не source-of-truth дыра.
|
||||
|
||||
## Что сделать (когда дойдут руки)
|
||||
|
||||
1. Через **Portainer** (`portainer.kzntsv.site`, books-vds management plane — НЕ ssh+compose) отредактировать stack bookva-es:
|
||||
- добавить bind-mount под snapshot repo (напр. `/usr/docker/bookva-es/snapshots:/snapshots`),
|
||||
- добавить env `path.repo=/snapshots`,
|
||||
- recreate (ES restart ~30 сек).
|
||||
2. Зарегистрировать repo: `curl -X PUT localhost:9200/_snapshot/kreknin -d '{"type":"fs","settings":{"location":"/snapshots"}}'`.
|
||||
3. В `/opt/stacks/backup/run.sh` (и репо-копию) добавить второй ES-snapshot блок для bookva-es + rsync `bookva-es/snapshots`.
|
||||
4. Прогнать вручную, проверить green + артефакт на kreknin.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **2026-06-12** — заведена. bookva db/mongo/minio добавлены в backup в эту сессию; es отложен (нужен disruptive ES restart через Portainer, данные покрыты slovo-снапшотом). См. commit с bookva-backup.
|
||||
210
.tasks/bookva-tenant-cutover-prep.md
Normal file
210
.tasks/bookva-tenant-cutover-prep.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# bookva-tenant-cutover-prep
|
||||
|
||||
Подготовительные ops-шаги на books VDS перед cutover Bookva tenant в отдельный стек на `bookseller.kzntsv.site`. Code/CI part сделан в victor/books master (см. dev-side ниже).
|
||||
|
||||
## Контекст dev-source
|
||||
|
||||
- Дизайн: victor/books `.wiki/concepts/tenant-split.md` rev v3 (2026-05-25).
|
||||
- Overlay-репо: `victor/bookva-overlay` + `victor/slovo-overlay` (compose'ы + config-templates).
|
||||
- Code-side done в master:
|
||||
- `5e28fd1` embed-api в web (server/api/*) + zod^4 fix.
|
||||
- `4a9cafc` ES endpoint per-tenant через `custom-environment-variables.json`.
|
||||
- `c1e58cf` BookvaRedirectBanner — PWA handoff modal для user.id=1.
|
||||
- `88df172` deploy.yml per-tenant (tenant input + per-tenant stack IDs + bookva-overlay clone).
|
||||
|
||||
## Goal
|
||||
|
||||
Bookva стек готов взлететь — все secrets/stacks/volumes/DNS/PWA-redirect на месте. Сам `compose up` для bookva = последний шаг (отдельная задача `bookva-tenant-cutover` будет создана когда юзер скажет).
|
||||
|
||||
## Plan
|
||||
|
||||
### Step 1 — Gitea secrets (admin via gitea UI или API)
|
||||
|
||||
Добавить в repo `victor/books` settings → Secrets:
|
||||
|
||||
```
|
||||
PORTAINER_STACK_ID_BOOKVA_API
|
||||
PORTAINER_STACK_ID_BOOKVA_WEB
|
||||
PORTAINER_STACK_ID_BOOKVA_SCHEDULER
|
||||
PORTAINER_STACK_ID_BOOKVA_OPS_MCP
|
||||
GITEA_TOKEN # read-access к victor/bookva-overlay
|
||||
HEALTHCHECK_BOOKVA_API_URL # опционально, http healthcheck
|
||||
HEALTHCHECK_BOOKVA_WEB_URL
|
||||
```
|
||||
|
||||
Значения stack ID — после Step 2.
|
||||
|
||||
После Phase 2 cutover (отдельная задача) — переименовать `PORTAINER_STACK_ID_{API,WEB,SCHEDULER,OPS_MCP}` → `PORTAINER_STACK_ID_SLOVO_*` для symmetry. До тех пор deploy.yml использует fallback на legacy имена.
|
||||
|
||||
### Step 2 — Portainer bookva stacks
|
||||
|
||||
В Portainer (https://portainer.kzntsv.site) создать stacks из overlay-compose:
|
||||
|
||||
- `bookva-db` — `~/projects/bookva-overlay/deploy/db.compose.yml`
|
||||
- `bookva-mongo` — `mongo.compose.yml`
|
||||
- `bookva-es` — `elasticsearch.compose.yml` (новый, в-stack ES 7.10.0)
|
||||
- `bookva-api` — `api.compose.yml`
|
||||
- `bookva-web` — `web.compose.yml`
|
||||
- `bookva-scheduler` — `scheduler.compose.yml`
|
||||
- `bookva-task-runner` — `task-runner.compose.yml`
|
||||
- `bookva-ozon-mcp` — `ozon-mcp.compose.yml`
|
||||
- `bookva-ops-mcp` — `ops-mcp.compose.yml`
|
||||
- `bookva-ntfy` — `ntfy.compose.yml`
|
||||
|
||||
Env-vars stack-level (Portainer Stack → Environment):
|
||||
- `CORE_SHA` — последний master tag из registry (например `master-5e28fd1`).
|
||||
- `HOSTNAME_API` / `HOSTNAME_WEB` — `bookseller.kzntsv.site` (per design v3).
|
||||
- `HOSTNAME_NTFY` — `push.bookseller.kzntsv.site`.
|
||||
|
||||
После создания каждого stack — Portainer присваивает ID → записать в Gitea secrets (Step 1).
|
||||
|
||||
### Step 3 — Volume create + copy (stateful split)
|
||||
|
||||
См. отдельную задачу [stateful-split-volume-copy](stateful-split-volume-copy.md) — MariaDB.
|
||||
|
||||
Дополнительно для Phase 2 нужны (текущая задача охватывает только MariaDB):
|
||||
- `bookva-mongo-data` — copy с `books-job-scheduler-mongo_data` через `cp -a` + `deleteMany({'data.idSeller': 2})` для cleanup чужих agenda jobs.
|
||||
- `bookva-es-data` — пустой volume; данные через ES snapshot+restore из shared kzntsv ES в Phase 2.
|
||||
- `bookva-minio-data` ИЛИ FS-rename `books` → `slovo` + `cp -a slovo bookva` в shared MinIO контейнере.
|
||||
- Config volumes — `bookva-{api,scheduler,task-runner}-config` + `bookva-web-branding` + `bookva-ntfy-data` (см. README в bookva-overlay для bootstrap).
|
||||
|
||||
### Step 4 — bookva-db login-gate
|
||||
|
||||
После volume copy + bookva-db up:
|
||||
|
||||
```sql
|
||||
-- В bookva-db только (slovo-db не трогать!):
|
||||
UPDATE users SET password = SHA2(UUID(), 256) WHERE id NOT IN (1, 2);
|
||||
```
|
||||
|
||||
(точная команда — после проверки актуального password storage format в `packages/api/server/services/auth/*` или embedded `packages/web/server/api/auth/login.post.js`.)
|
||||
|
||||
Эффект: только id=1 (Bookva-учредитель) + id=2 (Slovo-учредитель) могут логиниться в `bookseller.kzntsv.site`. Остальные user-записи остаются, но без работающего пароля.
|
||||
|
||||
### Step 5 — DNS
|
||||
|
||||
Добавить A-record `bookseller.kzntsv.site` → books-vds IP (тот же что `bookva.kzntsv.site` сейчас).
|
||||
|
||||
После cutover (отдельная задача) — `bookva.kzntsv.site` остаётся на тот же IP (роутит на slovo стек).
|
||||
|
||||
### Step 6 — PWA redirect handoff (за 1-2 недели до cutover)
|
||||
|
||||
В Portainer для текущего `books-web` stack (= future slovo-web после cutover) добавить env:
|
||||
|
||||
```
|
||||
NUXT_PUBLIC_BOOKVA_REDIRECT_URL=https://bookseller.kzntsv.site
|
||||
```
|
||||
|
||||
После DEPLOY_AT bump → recreate web container → новый SW bundle подхватывает `BookvaRedirectBanner` компонент. user.id=1 видит modal «Bookva теперь на bookseller.kzntsv.site → Перейти».
|
||||
|
||||
### Step 7 — books-web embedded-api env (pending step из embed-api handoff)
|
||||
|
||||
В Portainer `books-web` stack environment:
|
||||
|
||||
ADD:
|
||||
```
|
||||
NUXT_AUTH_TOKEN=<значение из books-api.NITRO_AUTH_TOKEN>
|
||||
NUXT_JWT_SECRET_KEY=<значение из books-api.NITRO_JWT_SECRET_KEY>
|
||||
```
|
||||
|
||||
CHANGE/REMOVE:
|
||||
```
|
||||
NUXT_PUBLIC_BOOKS_API_URL=/api # было https://books.kzntsv.site, переключаем на embedded
|
||||
NUXT_PUBLIC_BOOKS_API_KEY= # удалить (legacy)
|
||||
```
|
||||
|
||||
После DEPLOY_AT bump → recreate. Embedded api начнёт обслуживать `/api/*` (вместо старого books-api стека).
|
||||
|
||||
После 24-48ч стабильности — остановить `books-api` stack (отдельная micro-task).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Gitea secrets выставлены (Step 1).
|
||||
- Portainer stacks `bookva-*` созданы (Step 2), stack IDs в Gitea secrets.
|
||||
- Volumes `bookva-*` существуют + data populated (Step 3).
|
||||
- bookva-db login-gate применён (Step 4).
|
||||
- DNS `bookseller.kzntsv.site` → books-vds (Step 5).
|
||||
- `NUXT_PUBLIC_BOOKVA_REDIRECT_URL` в books-web env (Step 6) — за 1-2 недели до cutover.
|
||||
- books-web env обновлён для embedded api (Step 7).
|
||||
- Acceptance не включает `compose up bookva-*` — это **отдельная** задача `bookva-tenant-cutover` (создаётся когда юзер скажет «давай поднимать Bookva»).
|
||||
|
||||
## Status
|
||||
|
||||
🟢 closed 2026-05-26 — 7/7 (Step 6 descoped) + extension: bookva-minio container + external port-bind для bookva-db/bookva-minio.
|
||||
|
||||
## Post-closure follow-up в той же сессии
|
||||
|
||||
- **bookva-minio container added** (user request — separate MinIO instance вместо bucket-split в shared MinIO). Image `minio:RELEASE.2020-07-13` (parity с books-minio), volume bookva-minio-data (1.2G — cp -a `/usr/docker/minio/data/books`), Portainer stack ID 49. Same MINIO creds для zero-config-change. Bookva apps reach через `http://bookva-minio:9000` (internal). bookva-overlay templates updated (s3.endpoint, bucket=books).
|
||||
- **External access для bookva-db** + **bookva-minio**: port-bind на ALT портах (books-db занимает 3306, books-minio занимает 9000).
|
||||
- `bookva-db` → `89.253.255.133:33306` (MariaDB). Verified — handshake `10.6.26-MariaDB-ubu2204` ✓.
|
||||
- `bookva-minio` → `http://89.253.255.133:9001` (S3 API). Verified — 403 anon (sig required) = service alive ✓.
|
||||
- Traefik HTTPS labels добавлены для `bookva-minio.kzntsv.site` (активируется когда DNS A-record добавлен в reg.ru).
|
||||
- **Step 6 descoped** — PWA redirect modal для одного user'а (id=1, Bookva founder) = over-engineering. Substitute = manual heads-up учредителю Bookva перед cutover. Trade-off acceptable (краткое недоумение «где Bookva?» если откроет старый bookmark post-cutover).
|
||||
|
||||
## Closure note
|
||||
|
||||
Scope полностью выполнен per acceptance кроме Step 6 (delayed by design) и одной дискретной деференции (bookva-ozon-mcp stack — image отсутствует в registry, требует build в victor/books). Все остальные шаги closed:
|
||||
|
||||
- **Step 1** ✅ 7 Gitea secrets созданы (BOOKVA_OVERLAY_TOKEN scope=read:repository, HEALTHCHECK_BOOKVA_{API,WEB}_URL, PORTAINER_STACK_ID_BOOKVA_{API,WEB,SCHEDULER,OPS_MCP}).
|
||||
- **Step 2** ✅ 9/10 Portainer stacks (ozon-mcp deferred). User-facing stopped pre-cutover (Status=2), infra (db/mongo/es) активны.
|
||||
- **Step 3** ✅ 6 volumes + 4 populated из corrected overlay templates. bookva-db-data (4.7G), bookva-mongo-data (517M) cp -a с downtimes 59s + 5s. MinIO bucket copy deferred (cutover-time через `mc cp books bookva`).
|
||||
- **Step 4** ✅ bookva-db login-gate `UPDATE users SET password=UUID() WHERE id_user NOT IN (1,2)` — 3 users (id≥3) scrambled, books-db verified untouched.
|
||||
- **Step 5** ✅ DNS bookseller.kzntsv.site pre-existed (closed by inspection).
|
||||
- **Step 6** 🔵 delayed — выставить `NUXT_PUBLIC_BOOKVA_REDIRECT_URL` за 1-2 нед до cutover.
|
||||
- **Step 7** ✅ books-web embed-api live (Portainer stack 24 PUT + books `75ea320` push). bookva.kzntsv.site/api/* via embed. books-api stack fallback 24-48ч soak до stop micro-task'и.
|
||||
|
||||
Discovered + fixed cross-repo (bookva-overlay 4 commits, books 2 commits):
|
||||
- bookva-overlay `5b173de` — config-templates align prod shape + design v3 hostname
|
||||
- bookva-overlay `264373d` — .env.example design v3 hostnames
|
||||
- bookva-overlay `23eb374` — db.compose.yml mariadb:10.11 → 10.6 (match books-db для cp -a)
|
||||
- bookva-overlay `93c2ea2` — mongo.compose.yml mongo:7 → 4.2 (WiredTiger format compat)
|
||||
- bookva-overlay `2b20a3e` — api.compose.yml remove traefik (internal-only per v3)
|
||||
- bookva-overlay `8bffd68` — depends_on dropped (per-stack model)
|
||||
- books `2f7b539` — secrets.GITEA_TOKEN → BOOKVA_OVERLAY_TOKEN (Gitea reserved prefix)
|
||||
- books `75ea320` — deploy/web.compose.yml embed-api refs (NUXT_AUTH_TOKEN + NUXT_JWT_SECRET_KEY)
|
||||
|
||||
Workflow E2E verified: dispatch tenant=bookva, all 4 svc deploy gracefully — BOOKVA_OVERLAY_TOKEN clone bookva-overlay OK, скип per-svc если stack_id отсутствовал (до записи).
|
||||
|
||||
## Where I stopped
|
||||
|
||||
closed — handoff to `bookva-tenant-cutover` task (когда user скажет «давай поднимать Bookva»). Cutover task должна: re-start stopped Portainer stacks (api/web/scheduler/task-runner/ops-mcp/ntfy), `mc cp books bookva` MinIO bucket, ES snapshot+restore from shared kzntsv → bookva-es (Phase 2), smoke с двух сетей, books-api stack stop после 24-48ч soak post-Step-7.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **2026-05-26 — Step 5 closed by inspection.** `bookseller.kzntsv.site` уже резолвится authoritative `ns1.reg.ru` → `89.253.255.133`. User либо добавил A-record ранее, либо wildcard. Zero work needed.
|
||||
- **2026-05-26 — Step 1: Gitea reserves `GITEA_*` prefix.** Попытка PUT `GITEA_TOKEN` secret вернула 400 "invalid variable or secret name". Создан новый PAT scope=`read:repository` для OpeItcLoc03 (UI generate, т.к. `POST /users/.../tokens` требует basic auth, не token). Token в pass `gitea/victor-books-ci-bookva-overlay`. Записан в victor/books secret под именем `BOOKVA_OVERLAY_TOKEN`. deploy.yml line 80 patched (`secrets.GITEA_TOKEN` → `secrets.BOOKVA_OVERLAY_TOKEN`) — commit `2f7b539` pushed.
|
||||
- **2026-05-26 — bookva-overlay templates stale + missing fields.** При populate выяснилось что `config-templates/api/default.json` + `scheduler/default.json` короче prod (missing `jobs[]`, `jobs-new[]`, `docker`, `mailSettings`, `ntfy`), а `api.url`/`web.url` в scheduler template использовали `api.bookva.kzntsv.site`/`bookva.kzntsv.site` (старая нотация, до design v3 hostname swap). `task-runner/default.json` отсутствовал вовсе. Переписал из prod baseline + per-tenant overrides (`bookva-db`, `mongodb://bookva-mongo:27017/agendaDb`, `https://bookseller.kzntsv.site`, `s3.bucket=bookva`, `http://bookva-es:9200`). README hostname table → design v3. bookva-overlay commit `5b173de` pushed.
|
||||
- **2026-05-26 — Step 7 atomic env+compose PUT.** books-web compose template references меняем legacy `NUXT_PUBLIC_BOOKS_API_KEY` → `NUXT_AUTH_TOKEN` + `NUXT_JWT_SECRET_KEY` (server-side, не публикуется в client bundle). Values из books-api container env (`NITRO_AUTH_TOKEN=81e123d0-...`, `NITRO_JWT_SECRET_KEY=505172e5...`). Portainer stack 24 updated via API (PUT compose+env atomically, container recreated, DEPLOY_AT bumped). Smoke: `/api/healthz=200`, `/api/auth/login.post=401 "JWT-required"` matches books-api behavior. books `75ea320` pushed. **Гонка race:** PUT в Portainer перед push в git, чтобы next CI auto-deploy не подхватил stale compose без NUXT_AUTH_TOKEN ref.
|
||||
- **2026-05-26 — Step 3 scope split.** Этот task охватывает 6 volumes (api-config, scheduler-config, task-runner-config, web-branding, ntfy-data, es-data) + populate из corrected templates. **Stateful data volumes** (bookva-db-data via cp -a `books-db`, bookva-mongo-data via cp -a books-job-scheduler-mongo) = scope of separate `stateful-split-volume-copy` task (under maintenance window). MinIO rename (`books`→`slovo` + `cp slovo bookva`) = separate consideration, требует books-api reconfig (новая bucket).
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] Step 5 — DNS `bookseller.kzntsv.site` → `89.253.255.133` (закрыто inspection, было pre-existing)
|
||||
- [x] Step 1 — Gitea secret `BOOKVA_OVERLAY_TOKEN` создан в victor/books + deploy.yml `secrets.GITEA_TOKEN` → `secrets.BOOKVA_OVERLAY_TOKEN` (books `2f7b539` pushed). Token в pass `gitea/victor-books-ci-bookva-overlay`. Stack ID secrets отложены до Step 2.
|
||||
- [x] Step 3 (config volumes part) — 6 empty external volumes созданы (`bookva-{api-config,scheduler-config,task-runner-config,web-branding,ntfy-data,es-data}`). 4 populated с corrected templates: `bookva-api-config`, `bookva-scheduler-config`, `bookva-task-runner-config`, `bookva-web-branding`. 2 intentionally empty: `bookva-ntfy-data` (чистый старт), `bookva-es-data` (Phase 2 snapshot/restore).
|
||||
- [x] Bookva-overlay template correctness fix — api/scheduler/task-runner templates aligned to prod shape + design v3 (commit `5b173de` pushed).
|
||||
- [x] Step 7 — books-web embed-api env switch live. Portainer stack 24: env={`NUXT_PUBLIC_BOOKS_API_URL=/api`, `NUXT_AUTH_TOKEN=81e123d0-...`, `NUXT_JWT_SECRET_KEY=505172e5...`}, compose template updated. Smoke green. books `75ea320` pushed. books-api stack продолжает работать как fallback — отдельной micro-task'ой stop после 24-48ч soak.
|
||||
|
||||
## Remaining (maintenance-window or delayed)
|
||||
|
||||
- **Step 2 — Portainer bookva-* stacks (10 stacks).** Требует все volumes existing (включая stateful). Запускается после `stateful-split-volume-copy` task. Env stack-level: `CORE_SHA=master-<latest>`, `HOSTNAME_API=HOSTNAME_WEB=bookseller.kzntsv.site`, `HOSTNAME_NTFY=push.bookseller.kzntsv.site`.
|
||||
- **Step 3 remaining — Mongo cp + MariaDB cp + MinIO rename.** Mongo + MariaDB → блок stateful-split-volume-copy. MinIO rename — disruptive (books-api config change нужен), пока defer; alternative — copy `books/*` объекты в new `bookva/*` bucket без rename (write-only path для bookva-API).
|
||||
- **Step 1 finish — записать `PORTAINER_STACK_ID_BOOKVA_{API,WEB,SCHEDULER,OPS_MCP}` после Step 2.** Без них deploy.yml skip'ает bookva tenant с warning (graceful).
|
||||
- **Step 4 — bookva-db login-gate SQL.** `UPDATE users SET password=SHA2(UUID(),256) WHERE id NOT IN (1,2)` против bookva-db. Точную команду подтвердить по `packages/api/server/services/auth/*` (password storage format в prod).
|
||||
- **Step 6 — PWA redirect handoff (delayed).** Per spec — за 1-2 нед до cutover bump `NUXT_PUBLIC_BOOKVA_REDIRECT_URL` env в books-web (= future slovo-web) → SW bundle подхватит `BookvaRedirectBanner` модал для user.id=1.
|
||||
|
||||
## Next action
|
||||
|
||||
Coordinate с user'ом 1-2h maintenance window slot — execute `stateful-split-volume-copy` playbook (bookva-db + bookva-mongo data via `cp -a`), затем Step 2 Portainer stacks create (через Portainer API с volumes уже existing), затем Step 1 finish (stack IDs → Gitea secrets), затем Step 4 login-gate. Step 6 — отдельный заход за 1-2 нед до cutover.
|
||||
|
||||
## Blocker
|
||||
|
||||
`stateful-split-volume-copy` task — не started. Без MariaDB+Mongo volumes — Step 2 stacks deploy fail (volume not found). Maintenance-window window needs user signoff.
|
||||
|
||||
## Branch
|
||||
|
||||
master
|
||||
|
||||
<!-- created-by: vitya / 2026-05-26 / Phase 2 cutover prep после tenant-split code-side done -->
|
||||
<!-- progress-2026-05-26: Steps 1, 3-partial, 5, 7 closed; bookva-overlay templates fixed + pushed; victor/books deploy.yml + web.compose.yml fixed + pushed -->
|
||||
|
||||
43
.tasks/decommission-windows-recovery-host.md
Normal file
43
.tasks/decommission-windows-recovery-host.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# decommission-windows-recovery-host
|
||||
|
||||
## Goal
|
||||
Прибраться на [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK) после Synology-аварии: сервисы поднимались здесь временно, теперь почти все уехали на VDS + бэкапы настроены, локальный контент остался. Освободить диск (было 62 ГБ free из 636 used). **Оставить только IIS-сайты stostayer (`stostayer` + `stostayer.old`).**
|
||||
|
||||
## User decisions (2026-06-08)
|
||||
1. snolla rollback-артефакты (VM 95ГБ + OVA 42ГБ + vm-sites 11ГБ) — **снести всё сейчас, без архива** (RUVDS cutover стабилен с 05.06, win-acme самодостаточен).
|
||||
2. `stostayer.old` (:8091, БД на локальном MSSQL) — **нужен → репойнт web.config на `mssql.kzntsv.site,1433`, локальный MSSQL снести**.
|
||||
3. Bulk safe — **всё разом**.
|
||||
|
||||
## Plan / phases
|
||||
- [x] **A. Docker images + build cache prune** — ✅ образы 118→7.1ГБ, build-cache 65→0ГБ (~176ГБ logical). + rm exited-мусор. ⚠ physical-reclaim на C: только после vhdx-compact (см. ниже).
|
||||
- [x] **B. Redundant local CMS-backend контейнеры** — ✅ stop+rm `minio`/`imgproxy`/`imgproxy-nginx`/`elasticsearch`.
|
||||
- [x] **C. Husk-стеки** — ✅ удалены 21 пустой `diskstation/*` + data-папки minio/es/imgproxy. Осталось в diskstation: `mssql`, `traefik`.
|
||||
- [x] **D. snolla rollback** — ✅ VBox VM `snolla-recovery` (unregistervm --delete, −95ГБ) + `C:\nas-recovery` (−~150ГБ, по содержимому: harness блокирует Remove-Item на root-C:-папке). **C: free 62→214ГБ.**
|
||||
- [x] **C.bis sites archives** — ✅ удалены `C:\sites\snolla.tar.gz`, `snolla.zip`, `snolla-identity-manager`.
|
||||
- [x] **F1 orphan-fix** — ✅ сделано agent-side 2026-06-08: `snolla@stostayer` на VDS был orphaned → `ALTER USER snolla WITH LOGIN=snolla` (как sa, пароль из `pass mssql-vds/sa-password` через env-var, не в чат). Verified snolla→VDS stostayer OK (128 таблиц). pass расшифровывается в agent-bash (не в PowerShell — там это не команда).
|
||||
- [x] **E+F finale** — ✅ user прогнал `scripts/.../finish-elevated.ps1` (elevated) 2026-06-08: репойнт stostayer.old web.config → `mssql.kzntsv.site` (idempotent, оригинальный localhost-бэкап сохранён) → smoke :8091=200 → снёс IIS-сайт `snolla` + `C:\sites\snolla` (8.66ГБ) + локальный MSSQL (контейнер+том+папка, ~28ГБ). **C: free 214→275ГБ.**
|
||||
- Gotcha: smoke сначала ложно упал — системный VPN-прокси глотает `Invoke-WebRequest` к localhost (вики bug #7); починено на `curl --noproxy`.
|
||||
- ✅ **Verify оставленного:** stostayer :8090 → 200 (Host: stostayer.snolla.com; внешняя БД www.stostayer.ru, не трогали), stostayer.old :8091 → 200 (на VDS-БД).
|
||||
|
||||
**ИТОГ: C: free 62 → 275 ГБ.** Машина: только stostayer/stostayer.old IIS + lightrag + markitdown(MCP) + mutable-dev VM. `diskstation/` пуст.
|
||||
|
||||
### Verify-факт (2026-06-08)
|
||||
БД `stostayer` НА VDS ЕСТЬ (все 5 уехали: MoreThenCms/StayerCalculator/StayerPrice/stostayer/TireService), логин `snolla` существует, но его user в `stostayer` **orphaned** → прямой репойнт даёт `Login failed`. Фикс = `ALTER USER snolla WITH LOGIN=snolla` (в скрипте, шаг F1).
|
||||
|
||||
- [x] **local traefik** — ✅ снесён 2026-06-08 (user-решение): контейнер + том `traefik_traefik_letsencrypt` (acme.json больше не нужен — RUVDS на своём win-acme) + `diskstation/traefik`. После него `diskstation/` = только `mssql`. Скрипт E обновлён (traefik-шаги убраны).
|
||||
|
||||
### Keep (user-решение 2026-06-08)
|
||||
- **lightrag-* + postgres** контейнеры — оставить (used).
|
||||
- **VM `mutable-dev-environment`** (3ГБ) — оставить.
|
||||
|
||||
### Follow-up
|
||||
- **vhdx compact** (вернуть ~176ГБ docker-reclaim на C:): `wsl --shutdown` + `Optimize-VHD ...docker_data.vhdx`. Гасит markitdown-MCP/lightrag — делать вне активной agent-сессии.
|
||||
|
||||
## Keep
|
||||
- IIS `stostayer` (:8090, БД внешняя www.stostayer.ru) + `stostayer.old` (:8091, после репойнта на VDS).
|
||||
- traefik (proxy-plane), markitdown-mcp (агент-инфра), `MoreThenCms` git-репо, SSH-ключи.
|
||||
- **lightrag-* + postgres контейнеры** — НЕ трогать пока не подтверждён статус (локальный MCP-инстанс vs leftover). Open question.
|
||||
|
||||
## Notes
|
||||
- Elevated-операции (IIS site remove, web.config в C:\sites, Restart-WebAppPool) — non-elevated tool не может; готовить команды для user или elevated shell.
|
||||
- ⚠ Soak snolla формально до ~12.06 — user разрешил снести раньше (cutover стабилен).
|
||||
34
.tasks/fix-nl-vds-reality-pq-dest.md
Normal file
34
.tasks/fix-nl-vds-reality-pq-dest.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# fix-nl-vds-reality-pq-dest
|
||||
|
||||
**Status:** 🟡 server-side done 2026-06-05 — awaiting client-side SNI change + user confirm
|
||||
**Created:** 2026-06-05
|
||||
|
||||
## Проблема
|
||||
Reality-инбаунд на 443 (NL VDS `213.176.64.253`, 3x-UI) не коннектится. Root cause доказан:
|
||||
ML-DSA-65 (post-quantum REALITY) × dest/SNI `www.intel.com` (Akamai, не поддерживает PQ key-exchange
|
||||
`X25519MLKEM768` → HelloRetryRequest → borrowed-TLS handshake REALITY не достраивается). Plain VLESS
|
||||
на 32030 работает (там `security: none`). Полный разбор + изоляционная матрица:
|
||||
`.wiki/concepts/reality-pq-mldsa65-dest-incompatibility.md`; узел: `.wiki/entities/nl-vds-3xui.md`.
|
||||
|
||||
## Fix (рекомендация — НЕ применён, ждёт команды)
|
||||
Сменить `www.intel.com` → `www.microsoft.com` (PQ-совместим, проверен 2026-06-05), симметрично:
|
||||
1. **Сервер, инбаунд 443:** `realitySettings.target` → `www.microsoft.com:443`, `serverNames` → `["www.microsoft.com"]`.
|
||||
2. **Клиент v2rayN, профиль `pqmkayaxo2`:** SNI → `www.microsoft.com`.
|
||||
|
||||
Менять через панель 3x-UI (Inbounds → edit) ИЛИ правкой `x-ui.db` + reload xray. Активное соединение
|
||||
на 32030 это не заденет. Альтернатива (проще, но теряем PQ): выключить ML-DSA-65 на инбаунде, оставить intel.
|
||||
|
||||
## Acceptance
|
||||
- v2rayN профиль `pqmkayaxo2` (Reality 443) подключается, трафик идёт.
|
||||
- Plain VLESS 32030 продолжает работать.
|
||||
|
||||
## Done 2026-06-05 (server-side, по команде user "и то и другое")
|
||||
- ✅ Reality dest `www.intel.com:443`→`www.microsoft.com:443` + serverNames→`["www.microsoft.com"]` (правка `x-ui.db` id=1 + `x-ui restart`). Проверено сквозным тоннелем (temp-клиент → реальный `:443`, SNI microsoft, uuid 5da48418+vision+mldsa65Verify) → **HTTP 204**. 32030 жив.
|
||||
- ✅ Креды ротированы (оригиналы были в плейнтексте): root SSH pass (новый работает, старый Access denied), panel username+password (CLI `x-ui setting`, bcrypt в БД), panel `secret` JWT (убил install API token/сессии). Всё в `pass nl-vds-3xui/full-env`. БД-бэкап `/root/x-ui.db.bak.20260605_083658`.
|
||||
|
||||
## ⏳ Остаётся (client-side, делает user)
|
||||
В v2rayN профиль `pqmkayaxo2`: **SNI** `www.intel.com` → `www.microsoft.com`, save, переподключиться → должно подняться. (Не правил guiNDB.db: v2rayN держит активное соединение.) После подтверждения — закрыть таску 🟢.
|
||||
|
||||
## Where I stopped
|
||||
Server-side fix + ротация применены и проверены. Жду от user: смену SNI в v2rayN + подтверждение, что профиль Reality поднялся.
|
||||
<!-- created-by: vitya@.admin-exec / 2026-06-05 / trigger: user-задача "не получается vless+Reality" -->
|
||||
62
.tasks/harden-books-vds-exposed-ports.md
Normal file
62
.tasks/harden-books-vds-exposed-ports.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# harden-books-vds-exposed-ports
|
||||
|
||||
## Goal
|
||||
|
||||
Закрыть/ограничить публично торчащие host-порты на books VDS (89.253.255.133). Выявлены
|
||||
во время инцидента `restore-elasticsearch-indices-books-vds` (2026-05-29): ES был снесён
|
||||
ransom-ботом через открытый `0.0.0.0:9200` в обход traefik+firewalld. Тот же класс дыры
|
||||
(docker-publish обходит firewalld INPUT-зоны) зияет ещё на ряде stateful-сервисов. Цель —
|
||||
ни один stateful-сервис не должен быть доступен с публичного IP напрямую; доступ либо через
|
||||
traefik (под auth), либо по внутренней docker-сети, либо по SSH-туннелю для admin.
|
||||
|
||||
## Exposure audit (2026-05-29, `docker ps` + `ss -tlnp`)
|
||||
|
||||
| Сервис | Порт наружу | Auth на сервисе | Действие |
|
||||
|---|---|---|---|
|
||||
| `elasticsearch` | ~~`0.0.0.0:9200`~~ | ❌ free-ES без auth | ✅ ЗАКРЫТ (Fix #3, инцидент) |
|
||||
| `mongo` | `0.0.0.0:27017` | ✅ enabled | убрать публикацию (app ходит по docker-сети) |
|
||||
| `books-db` (mariadb) | `0.0.0.0:3306` | ✅ root+pass | убрать публикацию / SSH-туннель для admin |
|
||||
| `bookva-db` (mariadb) | `0.0.0.0:33306` | ✅ | убрать публикацию |
|
||||
| `minio` | `0.0.0.0:9000` | ✅ | оставить только через traefik (minio.kzntsv.site) |
|
||||
| `bookva-minio` | `0.0.0.0:9001` | ✅ | то же |
|
||||
| rsync | `:873` | ? проверить | проверить нужен ли наружу |
|
||||
| imgproxy / imgproxy-nginx / proxy-chain | `:8787/8788/8778` | ? | проверить, internal? |
|
||||
|
||||
## Key files / endpoints
|
||||
|
||||
- Portainer API `https://portainer.kzntsv.site` (key в `pass books-vds/full-env`), PUT `/api/stacks/<id>?endpointId=1` — паттерн как в Fix #3 (убрать `ports:` блок из compose).
|
||||
- Stack ids: см. `.wiki/entities/books-vds.md` § Стек (22–33). mongo=31, books-db=32, minio=30, ES=33. bookva-* стек(и) — id уточнить через Portainer API list.
|
||||
- firewalld активен но docker его обходит — закрытие через docker port-publish, НЕ через firewalld rules.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- 2026-05-29: Не закрывать в emergency-режиме инцидента — БД credentialed (не дыра-нараспашку как free-ES), и публикация может использоваться для внешнего admin (DBeaver/Compass/mc). Нужно сперва проверить по каждому: ходит ли приложение через host-порт или по docker-сети, и нужен ли внешний admin-доступ (тогда SSH-туннель вместо публикации).
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] Каждый сервис: приложение коннектится через `service-name:port` (docker DNS) или через host-порт? Если через docker-сеть → публикацию убрать безопасно.
|
||||
- [ ] Нужен ли кому-то внешний admin к mongo/mariadb? Если да → SSH local-forward, не публичный порт.
|
||||
- [ ] rsync:873 — это backup-pipeline или забытый демон? Проверить.
|
||||
- [ ] imgproxy/proxy-chain порты — реально нужны наружу или artefact?
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] 2026-05-29: exposure audit (таблица выше)
|
||||
- [x] 2026-05-29: ES :9200 закрыт (в рамках инцидент-фикса)
|
||||
- [x] 2026-05-29: проверена auth — mongo/mariadb/minio требуют креды (не auth-less)
|
||||
|
||||
## Notes
|
||||
|
||||
Главный learning из инцидента: auth должна быть **на самом сервисе**, не только в
|
||||
traefik-мидлваре. firewalld не защищает от docker-publish. См.
|
||||
`.wiki/concepts/es-destructive-delete-incident-2026-05-26.md` § «Рецидив 2026-05-29».
|
||||
|
||||
## Branch
|
||||
|
||||
n/a (admin ops)
|
||||
|
||||
## Status
|
||||
|
||||
ready 2026-05-29
|
||||
|
||||
<!-- created-by: vitya@.admin-exec / 2026-05-29 / trigger: exposure-audit во время ES ransom-incident — 5 stateful-сервисов с публичными host-портами -->
|
||||
@@ -57,6 +57,14 @@ Migrate IIS hosting from [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK)
|
||||
- **2026-05-24 ~12:00 (second cutover):** `emspb.ru` + `www.emspb.ru` DNS flipped на 80.64.31.36 (1.1.1.1 + Yandex + reg.ru уже отдают новое, 8.8.8.8 кешировал старое). Smoke через --resolve и через partially-cached real DNS → 200 OK c correct content.
|
||||
- **2026-05-24 (scope narrow):** user-decision — `tandemmebel.ru` + `www.tandemmebel.ru` ОСТАЮТСЯ на windows-IIS на неопределённый срок. Cert на RUVDS уже импортирован, HTTPS binding existing — но DNS не свапаем. Source IIS `snolla` site нельзя decommission'ить пока tandemmebel на нём же (catch-all binding). Это разделяет migration на 24 hostnames мигрируют, 2 (`tandemmebel.ru` + www) остаются. Future migration tandemmebel — отдельная задача когда user решит.
|
||||
- **MinIO pipeline caveat (carry-over от `[iis-cutover-to-vds-services]`):** RUVDS IIS coupled к windows-host imgproxy через DNS `imgproxy.kzntsv.site`. SPOF home machine остаётся для image-rendering. Verified post-migration: `Test-NetConnection imgproxy.kzntsv.site -Port 443` с RUVDS = OK. Image pipeline working but не resilient.
|
||||
- **2026-05-25 (close-time DNS probe):** между cutover'ом 2026-05-24 и closure user тихо swap'нул ещё 5 пар hostnames через reg.ru. Реальное DNS состояние на 2026-05-25 11:30 MSK через `Resolve-DnsName -Server 8.8.8.8`:
|
||||
- **На RUVDS 80.64.31.36** (9): `kupimknigi.spb.ru`, `emspb.ru` + `www.emspb.ru`, `pilorama98.ru` + `www.pilorama98.ru`, `labtools.pro` + `www.labtools.pro`, `rimiz.ru` + `www.rimiz.ru`. TTL на swap'нутых: 3600s (auth ns1.reg.ru).
|
||||
- **На windows source 94.19.247.14** (16): `labtools.ru` + `www.labtools.ru`, `snolla.com` + 12× *.snolla.com (incl `rimiz.snolla.com`), `maljarka.tandemmebel.ru`, `tandemmebel.ru` + `www.tandemmebel.ru` (scope exception). TTL 86400s — не lowered.
|
||||
- **Live smoke** через real DNS: `kupimknigi.spb.ru`/`www.emspb.ru`/`www.pilorama98.ru`/`www.labtools.pro` → 200 OK + correct per-tenant title. Bare-domain 3 redirects работают (emspb/pilorama98/labtools.pro). `rimiz.ru` / `www.rimiz.ru` → 404 — pre-existing CMS-defect (тоже 404 на source), не migration regression.
|
||||
- **2026-06-05 (tandemmebel cutover — post-closure follow-up):** user (владелец tandemmebel) сам перенастроил DNS в reg.ru. Проверено `Resolve-DnsName`: `tandemmebel.ru` + `www.tandemmebel.ru` + `maljarka.tandemmebel.ru` → `80.64.31.36` (RUVDS) на authoritative ns1.reg.ru + 8.8.8.8 + 1.1.1.1 + Yandex. apex+CNAME сработал (www/maljarka follow apex автоматом). Smoke через real DNS: `tandemmebel.ru`/`www` → **200 OK** correct content; `maljarka.tandemmebel.ru` → **502** (pre-existing CMS-дефект, на source ровно так же — не регрессия cutover'а, см. Open questions). **Снимает прежнюю scope-exception 2026-05-24** (tandemmebel больше НЕ остаётся на windows). **Source НЕ заглушен** — user-decision держать как warm rollback: сайт `snolla` — общий catch-all, 11 `*.snolla.com` (incl `tandemmebel.snolla.com`) ещё резолвятся на `94.19.247.14`, полный `Stop-Website` невозможен; индивидуальное снятие 3 HTTPS-биндингов отложено до full-site decommission. Текущий баланс: **14 hostnames на RUVDS / 11 на windows-source**.
|
||||
- **2026-06-05 (LE auto-renewal pipeline построен — win-acme):** на RUVDS поднят постоянный self-renewing HTTP-01 pipeline (win-acme v2.2.9). Один **25-SAN cert** (store WebHosting, Issuer LE YR2, valid до **2026-09-03**) установлен во все 25 SNI-биндинга; **scheduled task `win-acme-renew-snolla`** (SYSTEM, daily, renew 55д до expiry). **Закрывает дедлайн cert-expiry 2026-07-22** и снимает зависимость RUVDS от домашнего traefik по сертификатам. Главный gotcha: OWIN-catch-all CMS (`owin:HandleAllRequests=true`) перехватывал `/.well-known/acme-challenge/` → решено выносом challenge-пути в **отдельное IIS-приложение в пуле «No Managed Code»** + патч шаблона `C:\win-acme\Web_Config.xml` (`<remove name="Owin"/>`). Staging + prod валидация всех 25 хостов зелёная; живые HTTPS-эндпоинты отдают новый cert (проверено TLS-смоком, incl maljarka/rimiz). Скрипт `scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1` + шаблон `winacme-Web_Config.xml`. Полный recipe: [[../.wiki/concepts/winacme-iis-owin-catchall-http01]].
|
||||
- **2026-06-05 (FULL CUTOVER достигнут):** проверка всех 25 hostname против authoritative ns1.reg.ru — **все → `80.64.31.36` (RUVDS), на windows-source authoritative не осталось ничего**. Вся зона `snolla.com` (apex + 10 субдоменов, независимые A-записи) переехала. Хвост кэша: `on.snolla.com` + `tandemmebel.snolla.com` ещё `94.19.247.14` на Google 8.8.8.8 (TTL 86400, дотекает; 1.1.1.1/Yandex уже RUVDS). RUVDS smoke: `snolla.com` 200 (default лендинг), `tandemmebel.snolla.com` 200 (tenant content). **Разблокирует decommission** `snolla` site + LE-renewal через win-acme+HTTP-01 (теперь challenge не отскочит на windows). **Source НЕ заглушен** — user-decision: 7-day soak warm rollback + ждём drain кэша. Целевой decommission ~2026-06-12; cert-renewal deadline ~2026-07-15 (certs expire 2026-07-22).
|
||||
- **2026-05-25 (close decision):** user-decision close — несмотря на 16 hostnames pending DNS swap + source IIS:8089 still live + LE renewal pipeline pending + cleanup pending, **task закрывается** на этом этапе. Reason: Phase 1 (RUVDS infra + scp + IIS recreate + cert import + 9 DNS swaps) выполнен и live; остальное — coordination/wait/decommission character, не agent-work load. Future actions перечислены в Closure note ниже, не trackable как `iis-migration-to-ruvds` continuation.
|
||||
|
||||
## Open questions
|
||||
|
||||
@@ -64,7 +72,7 @@ Migrate IIS hosting from [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK)
|
||||
- [ ] **Source IIS state backup НЕ снят** (skip'нули — appcmd требует elevated source-shell которого не было). Acceptable risk — source IIS всё ещё running как rollback. Если RUVDS proves stable за 1 неделю → можно decommission source без forensic snapshot'а.
|
||||
- [ ] **CMS-side 502/404 на 4 hostnames** — `maljarka.tandemmebel.ru` / `rimiz.ru` / `www.rimiz.ru` / `rimiz.snolla.com`. На source тоже не работают (return default page). Pre-existing dead-routes от `[iis-traefik-dead-routes-cleanup]` 2026-05-21. Изоляция не блокер для migration; защититься от cutover-blame — отдельная investigation если нужно.
|
||||
- [ ] **Backup strategy для RUVDS** — расширить `vds-backup-rsync-kreknin` cron как третий source? Решение отложено до после full cutover.
|
||||
- [ ] **LE renewal на RUVDS** — текущие certs valid до 2026-07-22 (~60 дней). До expiry нужен permanent renewal pipeline:
|
||||
- [x] **LE renewal на RUVDS** — ✅ DONE 2026-06-05 (win-acme HTTP-01, см. Decisions log + [[../.wiki/concepts/winacme-iis-owin-catchall-http01]]). Выбран Option A (win-acme + HTTP-01 после full cutover). Новый 25-SAN cert до 2026-09-03, авто-renewal через SYSTEM scheduled task. Дедлайн 2026-07-22 снят. ~~Историческая формулировка ниже:~~ текущие certs valid до 2026-07-22 (~60 дней). До expiry нужен permanent renewal pipeline:
|
||||
- Option A: win-acme (wacs.exe) standalone-на-RUVDS с DNS-01 (manual TXT на reg.ru — нет reg.ru-plugin) или HTTP-01 (но только после DNS swap'а — иначе LE challenge bounce'нется на home).
|
||||
- Option B: cron-job который re-extract'ит из traefik acme.json + scp upload + Import-PfxCertificate на RUVDS. Pollute source's traefik lifecycle.
|
||||
- Recommendation: Option A win-acme + HTTP-01 после full cutover (~95 days margin до cert expiry — 60 days minus DNS-stabilization window).
|
||||
@@ -84,18 +92,37 @@ Migrate IIS hosting from [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK)
|
||||
- [x] **2026-05-23 23:35:** 7 prod hostnames live-smoked through VDS → 200 OK / correct content; 4 hostnames degraded pre-migration (acceptable).
|
||||
- [x] **2026-05-24 ~00:00:** DNS swap kupimknigi.spb.ru → 80.64.31.36 (initial misroute to VDS corrected).
|
||||
|
||||
## Remaining steps (post-soak)
|
||||
## Closure note (2026-05-25)
|
||||
|
||||
- [ ] Verify kupimknigi.spb.ru через cache-clean resolver (после TTL expiry на 8.8.8.8 / 1.1.1.1) — должно показывать RUVDS content в браузере без `--resolve` override.
|
||||
- [ ] **Lower DNS TTL** в reg.ru на остальные 24 hostnames до 300s. Можно сделать сразу — обновит cache в ближайшие 24h, после этого swap пойдёт быстро.
|
||||
- [ ] DNS swap (через reg.ru) остальных 24 hostnames на `80.64.31.36`.
|
||||
- [ ] 1-неделя soak с RUVDS как live prod.
|
||||
- [ ] Decommission source IIS:8089 + traefik routes для CMS-hostnames (но не traefik сам — много других routes остаётся).
|
||||
- [ ] Cleanup: `Remove-NetFirewallRule -DisplayName 'smb-from-source'` (already-unused) + `ssh-from-source` (после disable temp key) + удалить `~/.ssh/ruvds-iis-migration*` keys + удалить `C:\ProgramData\ssh\administrators_authorized_keys` на RUVDS.
|
||||
- [ ] LE renewal pipeline (см. Open questions).
|
||||
- [ ] Concept update — `windows-server-2025-core-bootstrap.md` SMB-section deprecate в пользу SSH/scp, добавить host-header-middlebox warning, добавить HTTP/2 note.
|
||||
- [ ] New concept — `traefik-acme-json-to-iis-cert-import.md` (recipe экспорта-импорта).
|
||||
- [ ] Source-info commit — записать что image-pipeline SPOF остался → отдельная task.
|
||||
Task **closed 🟢** per user decision. Phase 1 — RUVDS infra + cert pipeline + 9 hostnames live — done.
|
||||
|
||||
**Что НЕ сделано** (не теряем — записано здесь, не в STATUS.md):
|
||||
|
||||
1. **16 hostnames still DNS на windows source** (94.19.247.14). Из них:
|
||||
- 14 plan to migrate later: `labtools.ru` + `www.labtools.ru`, `snolla.com` + 12× *.snolla.com (incl `rimiz.snolla.com`, `maljarka.tandemmebel.ru`).
|
||||
- ~~2 scope-exception: `tandemmebel.ru` + `www.tandemmebel.ru` — stays на windows-IIS~~ → **SUPERSEDED 2026-06-05: tandemmebel мигрировал на RUVDS** (см. Decisions log). Остаётся `tandemmebel.snolla.com` (snolla-алиас) на windows.
|
||||
- Action: user manually swap A-records в reg.ru → 80.64.31.36 при готовности. Recommend pre-step: lower TTL 86400s → 300s in advance to shrink cache-tail.
|
||||
- Нет reg.ru API key в pass entries — agent НЕ может сделать swap autonomously.
|
||||
|
||||
2. **Source IIS НЕ decommission'ен.** `localhost:8089` snolla site + port 80 catch-all still live на windows-recovery-host. Не трогать пока 16 hostnames на 94.19.247.14. После full cutover + 7-day soak — `Stop-Website snolla` + archive `C:\sites\snolla` (8.66GB) → kreknin.
|
||||
|
||||
3. ~~**LE renewal pipeline НЕ построен.**~~ → **DONE 2026-06-05** (win-acme HTTP-01 после full cutover, как и рекомендовалось). Новый 25-SAN LE cert до 2026-09-03 + SYSTEM auto-renew task. Recipe: [[../.wiki/concepts/winacme-iis-owin-catchall-http01]].
|
||||
|
||||
4. **Temp SSH key cleanup не сделан.** Оставлены (могут понадобиться для будущих push'ей на RUVDS):
|
||||
- `~/.ssh/ruvds-iis-migration` + `.pub` на source workstation
|
||||
- FW rule `ssh-from-source` + `smb-from-source` на RUVDS
|
||||
- `C:\ProgramData\ssh\administrators_authorized_keys` на RUVDS
|
||||
- Action defer до post-decommission.
|
||||
|
||||
5. **Wiki concept updates не сделаны** (low-priority, value beyond this task):
|
||||
- `.wiki/concepts/windows-server-2025-core-bootstrap.md` — SMB section deprecate, add HTTP-middlebox warning + HTTP/2 note.
|
||||
- New `.wiki/concepts/traefik-acme-json-to-iis-cert-import.md` — recipe extract+pkcs12+Import-PfxCertificate+AddSslCertificate.
|
||||
|
||||
6. **CMS-defect rimiz / maljarka** — 4 hostnames 502/404 pre-existing, не migration regression. На source ровно так же. Investigation отдельная история, не блокирует closure.
|
||||
|
||||
**Image-pipeline SPOF carry-forward:** RUVDS IIS зависит от `imgproxy.kzntsv.site` (windows-host imgproxy). SPOF home machine для image-rendering. Не решено в этой migration — задокументировано как separate `image-pipeline-resilience` (см. brainstorm option B local nginx-relay / option D decompile DLL — defer).
|
||||
|
||||
**Rollback (если RUVDS пропадает):** revert A-records 9 swapped hostnames в reg.ru на 94.19.247.14. Source IIS:8089 + traefik routes live = instant fallback.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
26
.tasks/kreknin-self-backup.md
Normal file
26
.tasks/kreknin-self-backup.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# kreknin-self-backup — второй таргет для приёмника бэкапов
|
||||
|
||||
**Status:** ⚪ backlog (на потом, по решению user 2026-06-12)
|
||||
**Priority:** #1 среди backup-дыр (см. [[../.wiki/concepts/backup-inventory-2026-06]])
|
||||
|
||||
## Проблема
|
||||
|
||||
[[../.wiki/entities/kreknin-synology]] (`195.19.90.188`, `/volume1` 7.0 ТБ) — **единственный приёмник всех 4 бэкап-пайплайнов** (vds-kzntsv 05:00, books-vds 06:00, ruvds-iis 04:30, openwrt 03:30) и при этом **сам никуда не бэкапится**. Если его том откажет — одновременно теряются offsite-копии ВСЕХ машин. Худший single point of failure в estate.
|
||||
|
||||
Тип RAID не подтверждён («тип неизвестен» в entity). Hyper Backup vault (`diskstation_1.hbk`, 430 ГБ) на нём же — тоже не реплицирован вовне.
|
||||
|
||||
## Направление (не финал, обсудить при подъёме)
|
||||
|
||||
Второй таргет для критичного subset — облако:
|
||||
- **Backblaze B2** (дёшево, S3-совместимо) или **S3 Glacier** (холодное, ещё дешевле, но retrieval-latency).
|
||||
- Synology **Cloud Sync** (B2) или **Hyper Backup** (→ B2/S3) — нативные пакеты DSM.
|
||||
- Минимальный subset: `/volume1/NetBackup/*/latest` каждого пайплайна + `*.hbk` vault. Полные 7 ТБ лить не нужно — только последние снапшоты.
|
||||
- Шифрование на стороне DSM (Hyper Backup client-side encryption) — облако untrusted.
|
||||
|
||||
## Оценка
|
||||
|
||||
Объём critical-subset: vds-kzntsv ~2.4 ГБ MSSQL + ~12 ГБ stacks, books-vds ~5 ГБ, ruvds ~9 ГБ, openwrt 15 КБ → ~30 ГБ latest. B2 storage $6/ТБ/мес → копейки. Egress при restore — разовый.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **2026-06-12** — заведена по итогам backup-gap аудита (триггер: MSSQL 3-нед gap). User: «задача, но на потом». Не горит, но это #1 по риску среди оставшихся дыр.
|
||||
45
.tasks/kupimknigi-deploy-snolla-0-42-0.md
Normal file
45
.tasks/kupimknigi-deploy-snolla-0-42-0.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# kupimknigi-deploy-snolla-0-42-0
|
||||
|
||||
## Goal
|
||||
Собрать VDS-staging образ `kupimknigi.spb.ru` на `@snollajs/snolla@0.42.0` из `victor/kupimknigi.spb.ru` (`apps/web`, HEAD **`9608ff6`**) и передеплоить/создать стек. Простой **одностраничник** — код закрыт (re-review PASS, docker-валидация GREEN локально, секреты не в git/образ). Финал тиража snolla.
|
||||
|
||||
## Site (факты, резолвнуты из БД прогом)
|
||||
- siteId `E924A354-0377-4E1E-80C6-2EB0194AA55F` · theme `EF2C663C-8960-4D3B-83AA-5C683D47D6C0` («bootstrap», store-путь `ef2c663c89604d3b83aa5c683d47d6c0`)
|
||||
- бой `https://kupimknigi.spb.ru` — **БЕЗ www** (www мёртв, оператор подтвердил). Production=false в БД.
|
||||
- Структура: Pages=1 (`/`, content_page), Forms=1 (`/callback-order` POST). Каталога/стора/блога/фида/редиректов нет.
|
||||
|
||||
## Key files
|
||||
- `~/projects/kupimknigi.spb.ru/apps/web/package.json` — пин `@snollajs/snolla@0.42.0`
|
||||
- `~/projects/kupimknigi.spb.ru/deploy/Dockerfile` — multi-stage node:22-slim non-root, build-arg `VERDACCIO_TOKEN`, секреты не бейкаются
|
||||
- config: `default.json` gitignored; на VDS передаётся через env (`custom-environment-variables.json` маппинг) — БД `mssql.kzntsv.site/MoreThenCms`, S3/MinIO, imgproxy
|
||||
|
||||
## Acceptance (право-масштабировано под одностраничник — НЕ tandemmebel)
|
||||
- build sha `9608ff6` на VDS → образ в `registry.kzntsv.site` → стек Portainer (env verbatim, healthy MSSQL+S3).
|
||||
- Staging-smoke с VDS: `/` → 200, рендер == бой `kupimknigi.spb.ru` (H1 «Скупка старых книг в Санкт-Петербурге»), тема-ассеты 200 из MinIO, форма `/callback-order` POST жива, seoCanonical (`/callback-order/`→301). Completeness тривиален (1 URL).
|
||||
- rollback наготове.
|
||||
|
||||
## Cutover — operator-gated (как весь тираж)
|
||||
Live DNS flip **НЕ трогать** без отмашки оператора. ⚠️ kupimknigi DNS сейчас на RUVDS IIS `80.64.31.36` (майская iis-migration) — cutover-таргет/порядок разрешить с оператором отдельно, RUVDS=rollback. Этот таск = staging-образ + гейты, не флип.
|
||||
|
||||
## Meta
|
||||
- **Weight:** needs-claude (staging-сборка; cutover=operator-gated отдельный шаг)
|
||||
- **Notify:** OpeItcLoc03/workshop (оркестратор — «его сайт = workshop»)
|
||||
- Deploy-source dev: `victor/kupimknigi.spb.ru`.
|
||||
|
||||
**Status:** done — 2026-07-05 (admin). GREEN.
|
||||
**Where I stopped:** закрыто, все гейты зелёные.
|
||||
**Next action:** — Cutover=operator-gated отдельный шаг.
|
||||
|
||||
## Completed steps (2026-07-05, admin)
|
||||
- [x] Пин верифицирован: sha `9608ff6` резолвит snolla 0.42.0 (package.json + yarn.lock), Dockerfile OK, порт 5000, healthcheck=robots.txt. production.json запечён (siteId E924A354/siteUrl kupimknigi.spb.ru/DB/imgproxy/minio). Консюмер-пин на месте (не было tandemmebel-блокера).
|
||||
- [x] snolla 0.42.0 в verdaccio подтверждён (ранее в сессии).
|
||||
- [x] Build на VDS `registry.kzntsv.site/kupimknigi:9608ff6` (digest `ac7f846`, 583MB, BUILD_EXIT=0), push OK.
|
||||
- [x] Создал НОВЫЙ Portainer-стек **Id 21 `kupimknigi`** (POST create/standalone/string, endpointId=1, env verbatim 8/8 из стека 20 — тот же snolla-тенант). Контейнер healthy сразу, running==9608ff6.
|
||||
- [x] Staging-smoke с VDS GREEN: `/`→200==prod; H1 «Скупка старых книг в Санкт-Петербурге…» идентичен prod; robots.txt 200; тема-ассет toolbox.css→200 (MinIO); форма action="/callback-order" в HTML; callback-роуты паритет prod (`/callback-order/`→301, `/callback-order`→404 POST-only); байты 17717≈17672.
|
||||
- [x] compose source-of-truth: `host-stacks/vds-kzntsv/kupimknigi.compose.yml` (staging-host, боевой Host только в cutover-комменте).
|
||||
- [x] Таска 🟢 done, notify workshop.
|
||||
|
||||
## Decisions log
|
||||
- 2026-07-05: **CLOSED 🟢.** Тривиальный одностраничник, деплой чистый, без блокеров. Форму не сабмитил (POST=реальное письмо клиенту) — роут зарегистрирован==prod + отрендерен, достаточно для staging. Cutover остаётся operator-gated (kupimknigi DNS на RUVDS IIS, майская миграция — таргет/порядок с оператором). Follow-up (общий тиражу): Dockerfile печёт VERDACCIO_TOKEN в ARG/ENV → build-secret (парный фикс с dev, отложен).
|
||||
|
||||
<!-- created-by: workshop / 2026-07-05 / dev-source victor/kupimknigi.spb.ru HEAD 9608ff6 -->
|
||||
117
.tasks/migrate-elasticsearch-to-books-vds.md
Normal file
117
.tasks/migrate-elasticsearch-to-books-vds.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# migrate-elasticsearch-to-books-vds
|
||||
|
||||
## Goal
|
||||
|
||||
Переехать ES индексы с Windows host (`elasticold.kzntsv.site`, ES 7.10.1) на books VDS (`elasticsearch.kzntsv.site`, ES 7.10.0, single-node behind traefik basicAuth). Переключить books-api / books-task-runner / books-job-scheduler через `default.json` на новый endpoint. Source ES оставить running как rollback (не выключать).
|
||||
|
||||
## Approach
|
||||
|
||||
**Reindex-from-remote** (не snapshot/restore).
|
||||
|
||||
Why:
|
||||
- Source = Windows docker, нет `path.repo` configured + нет bind для snapshot dir → snapshot подход требует restart source = consumer downtime.
|
||||
- Target = books VDS Portainer stack 33, modify env через Portainer API = recreate target only, consumers unaffected (они пока на source).
|
||||
- Same Lucene 8.7.0 → docs reindex transparently через `_reindex` API.
|
||||
- 730mb total → ~5 min over public network.
|
||||
|
||||
Trade-off: reindex использует target's dynamic mapping. Pre-create target indices с source mappings/settings (snapshot уже сохранён в `.scratch/`).
|
||||
|
||||
## Source/target inventory
|
||||
|
||||
**Source (Windows, `localhost:9200`, behind `elasticold.kzntsv.site` traefik basicAuth):**
|
||||
- ES 7.10.1, Lucene 8.7.0, `discovery.type=single-node`
|
||||
- 3 indices:
|
||||
- `artmone` 2621 docs, 1.4mb
|
||||
- `epz` 820604 docs, 699.7mb
|
||||
- `products` 105922 docs, 26.6mb
|
||||
- Compose: `C:\Users\vitya\projects\docker\diskstation\elasticsearch\docker-compose.yml`
|
||||
- Bind: `./data:/usr/share/elasticsearch/data`. **No snapshot path.**
|
||||
- basicAuth user `books`, hash `$apr1$vyxr1l5z$fpxHmNBfAx8cHrQTje4Fx/`
|
||||
|
||||
**Target (books VDS 89.253.255.133, Portainer stack 33 `elasticsearch`, behind `elasticsearch.kzntsv.site`):**
|
||||
- ES 7.10.0, Lucene 8.7.0, single-node, yellow (replica unassigned — expected)
|
||||
- 1 index: `read_me` (4.8kb placeholder)
|
||||
- `path.repo=/snapshots`, kreknin fs repo registered
|
||||
- Bind: `/usr/docker/elasticsearch/{data,snapshots}` → `/usr/share/elasticsearch/{data,/snapshots}`
|
||||
- basicAuth user `books`, **same hash как source** → same password
|
||||
|
||||
**Consumers (all on books VDS, configs bind-mounted `/opt/books/<svc>/config/default.json`):**
|
||||
- `books-api` (stack 23): port 3021, Nitro
|
||||
- `books-task-runner` (stack 22, sibling of job-scheduler+mongo)
|
||||
- `books-job-scheduler` (stack 22)
|
||||
|
||||
Все три имеют block:
|
||||
```json
|
||||
"elasticsearch": {
|
||||
"node": "https://elasticold.kzntsv.site/",
|
||||
"auth": { "username": "books", "password": "<same>" }
|
||||
}
|
||||
```
|
||||
|
||||
## Key files
|
||||
|
||||
- `C:\Users\vitya\projects\docker\diskstation\elasticsearch\docker-compose.yml` — source ES compose (no edit planned)
|
||||
- `C:\Users\vitya\projects\.admin\.scratch\source-mappings.json` — frozen source mappings 2026-05-25
|
||||
- `C:\Users\vitya\projects\.admin\.scratch\source-settings.json` — frozen source settings 2026-05-25
|
||||
- books VDS `/opt/books/api/config/default.json:27` — consumer config (idem task-runner, job-scheduler)
|
||||
- books VDS Portainer stack 33 — target ES compose (modify env via Portainer API)
|
||||
- books VDS `/usr/docker/elasticsearch/snapshots/` — kreknin fs repo, pre-migration backup target
|
||||
|
||||
## Execution plan
|
||||
|
||||
1. **Pre-migration ES snapshot books VDS** (kreknin repo, `pre-migration-<date>` snapshot name) — instant rollback for target.
|
||||
2. **Save source mappings/settings** — done (`.scratch/source-{mappings,settings}.json`).
|
||||
3. **Modify target ES stack** via Portainer API: add env `reindex.remote.whitelist=elasticold.kzntsv.site:443`. Recreate stack (~10s target downtime; consumers unaffected — они на source).
|
||||
4. **Pre-create target indices** с source mappings/settings (number_of_replicas=0 для single-node clean green).
|
||||
5. **POST `/_reindex`** per index, wait_for_completion=false, slices=auto. Poll task status.
|
||||
6. **Verify counts**: target `_cat/indices` doc.counts == source. Verify random doc fetch by `_id` matches.
|
||||
7. **Update consumer configs**: replace `elasticold.kzntsv.site` → `elasticsearch.kzntsv.site` в `/opt/books/{api,task-runner,job-scheduler}/config/default.json` (sed in-place).
|
||||
8. **Bounce consumers** через `docker restart books-api books-task-runner books-job-scheduler` (mongo / db не трогаем — bind-mount уже видит новый config).
|
||||
9. **Smoke**: tail `docker logs --since 60s books-api books-task-runner books-job-scheduler` → нет ES connection errors. Trigger known query → response from target.
|
||||
10. **Don't disable source** (per user) — Windows ES + elasticold traefik route остаётся live для rollback.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- 2026-05-25: подход reindex-from-remote, не snapshot/restore. Reason: source compose не имеет `path.repo` configured + bind для snapshot — это потребовало бы restart source (= consumer downtime). Target compose в Portainer, restart target не задевает consumers (они пока на source). Same Lucene 8.7.0 → reindex transparent.
|
||||
- 2026-05-25: pre-create target indices с frozen source mappings — иначе reindex использовал бы dynamic mapping, possibly losing exact analyzer config. Mappings/settings уже извлечены в `.scratch/`.
|
||||
- 2026-05-25: consumer config update через `sed -i` (bind-mounted files), restart through `docker restart` (не Portainer stack recreate) — mongo / db в той же stack id 22 не должны bounce.
|
||||
|
||||
## Open questions
|
||||
|
||||
- [x] `number_of_replicas`: source = 1 (yellow на single-node). Target тоже single-node → set to 0 на pre-create для green. Source оставить как есть. **Resolved: set 0 на target pre-create.**
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] Source ES probed: 3 indices, 730mb total, ES 7.10.1
|
||||
- [x] Target ES probed: ES 7.10.0, 1 placeholder index `read_me`, no collisions
|
||||
- [x] Source mappings/settings frozen в `.scratch/`
|
||||
- [x] basicAuth password identified из consumer config (same on both sides)
|
||||
- [x] Containers inventory: 3 consumer containers, all on books VDS
|
||||
- [x] Pre-migration ES snapshot `pre-migration-2026-05-25` в kreknin repo (read_me only, SUCCESS — rollback point)
|
||||
- [x] Target ES compose updated через Portainer API: `reindex.remote.whitelist=elasticold.kzntsv.site:443`. Container restart 07:51:39 UTC. Yellow cluster post-restart (replica unassigned = expected).
|
||||
- [x] 3 target indices pre-created с frozen source mappings/settings, `number_of_replicas=0` → green.
|
||||
- [x] Reindex from remote: artmone (2621/2621, 25s), epz (820604/820604, ~5m async), products (105922/105922, ~25m async).
|
||||
- [x] Doc count parity src=dst для всех 3 indices.
|
||||
- [x] Per-doc sample fetch: byte-match _id=29359 (products) + _id=22023500 (epz).
|
||||
- [x] Consumer configs sed in-place: `elasticold` → `elasticsearch`, .bak-pre-es-migration-2026-05-25 saved для rollback.
|
||||
- [x] Restart 3 containers: books-api running, books-task-runner healthy, books-job-scheduler healthy.
|
||||
- [x] Direct internal smoke (`docker exec books-api node -e ...`): API sees `https://elasticsearch.kzntsv.site/`, `_cluster/health` 200 OK.
|
||||
- [x] Post-migration source traefik access log = 0 elasticold hits за 5 min.
|
||||
|
||||
## Closure note (2026-05-25)
|
||||
|
||||
Migration **complete**. 3 indices (artmone/epz/products, 820k+108k+2.6k docs, 850mb total target storage) on books VDS ES live. Consumers switched (books-api + books-task-runner + books-job-scheduler).
|
||||
|
||||
**Source НЕ disabled** (per user requirement) — Windows ES + elasticold.kzntsv.site traefik route остаются live для emergency rollback. Кешированный data на windows host остаётся в одном экземпляре до отдельного decommission решения.
|
||||
|
||||
**Rollback**: `ssh root@89.253.255.133 'for f in /opt/books/{api,task-runner,job-scheduler}/config/default.json; do cp "$f.bak-pre-es-migration-2026-05-25" "$f"; done; docker restart books-api books-task-runner books-job-scheduler'`.
|
||||
|
||||
**Future ops**: при decommission Windows ES — `docker stop elasticsearch` на windows + remove traefik label `elasticold.kzntsv.site` route. `path.repo` / kreknin snapshot pre-migration-2026-05-25 уже не несёт практически данных (один read_me index), может быть удалён вместе с next backup retention rotation.
|
||||
|
||||
## Notes
|
||||
|
||||
- Source basicAuth password в `/opt/books/api/config/default.json` (consumer config). Не дублировать в эту таску.
|
||||
- `books-job-scheduler-mongo` — другая БД, не задевается.
|
||||
- Rollback path: `sed -i 's/elasticsearch\.kzntsv\.site/elasticold.kzntsv.site/g' /opt/books/*/config/default.json` + restart consumers.
|
||||
|
||||
<!-- created-by: vitya / 2026-05-25 / trigger: ES consolidation на books VDS, source-host decommission preparation -->
|
||||
85
.tasks/modulair-rag-vds-redeploy.md
Normal file
85
.tasks/modulair-rag-vds-redeploy.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# modulair-rag-vds-redeploy
|
||||
|
||||
## Goal
|
||||
Re-deploy the lost `modulair-rag` stack (4 containers) from the dead Synology NAS onto the VDS (`89.253.255.94`, rusonyx). Fresh start — knowledge base + pipeline cache were on lost named volumes, not critical (deploy was only ~1 week old). This `.admin` project owns the ops; `modulair-rag` repo stays the consumer.
|
||||
|
||||
## Source of truth (read before acting)
|
||||
- **Full context-freeze (canonical):** `~/projects/modulair-rag/.wiki/concepts/nas-loss-vds-redeploy-context.md` — pre-flight, deploy steps, acceptance, post-deploy. *(Note: currently uncommitted in the modulair-rag repo, but present on disk.)*
|
||||
- **Compose file (deploy as-is):** `~/projects/modulair-rag/compose.yml` — 4 services, named volumes, `proxy` external network. Do not edit; paste into Portainer.
|
||||
- **Design spec (architecture):** `mcp__projects-meta__knowledge_get slug="concepts/modulair-rag-design"` (shared wiki).
|
||||
- **Stack identity (NAS-era, now stale):** `~/projects/modulair-rag/.wiki/entities/portainer-stack.md`.
|
||||
|
||||
## VDS state — VERIFIED 2026-05-27 (via vds-ops `ops_docker_ps` / `ops_docker_inspect`)
|
||||
- ✅ **MinIO** container `minio/minio:latest` running 4d on VDS — the original blocker is CLEARED.
|
||||
- ✅ **postgres:16** running; on networks `proxy` (172.18.x) **and** `shared-dbs`, aliases `postgres`. → hostname `postgres:5432` from `modulair-pipeline`/`modulair-mcp` (which sit on `proxy`) **will resolve**. Compose's cross-stack DB reference is sound, no extra network wiring needed.
|
||||
- Minor: postgres runs with `ssl=on`; libpq default `sslmode=prefer` negotiates automatically — not a blocker.
|
||||
- ✅ `registry:2.8.3` (`registry.kzntsv.site`) running — images `modulair-pipeline:latest`, `tier1-converter:latest`, `modulair-mcp:latest` were pushed 2026-05-13 (per context-freeze). Verify they're still present before deploy.
|
||||
- ✅ `traefik:v2.11` running — compose labels target `modulair-mcp.kzntsv.site`.
|
||||
- ⛔ None of the 4 modulair containers exist yet (`lightrag-modulair`, `modulair-pipeline`, `tier1-converter`, `modulair-mcp`) — clean deploy.
|
||||
|
||||
## Pre-flight — admin must verify/create (NOT visible to read-only vds-ops)
|
||||
1. [ ] **MinIO bucket** `modulair-rag` exists. MinIO container is up but bucket membership is internal — create if missing (`mc mb`).
|
||||
2. [ ] **Postgres DB** `modulair_rag` exists → `CREATE DATABASE modulair_rag;` if not. lightrag self-creates its schema on first start — do NOT pre-init tables.
|
||||
3. [ ] **DNS** `modulair-mcp.kzntsv.site` → VDS IP `89.253.255.94` (likely still points at the dead NAS — repoint via regru/cloudflare).
|
||||
4. [ ] **MinIO endpoint sanity:** compose env uses `MINIO_ENDPOINT=minio.kzntsv.site:443` (SSL, hairpin via traefik back to the VDS minio). Confirm traefik route `minio.kzntsv.site` now points at the VDS minio container, and DNS resolves to VDS. (Alternative: direct `minio:9000` on a shared network if both end up on `proxy` — only if the external route is problematic.)
|
||||
5. [ ] **Secrets via pass** (pattern from `[secrets-manager-adopt]`):
|
||||
```
|
||||
pass insert modulair-rag/routerai-key
|
||||
pass insert modulair-rag/ollama-api-key
|
||||
pass insert modulair-rag/minio-access-key
|
||||
pass insert modulair-rag/minio-secret-key
|
||||
pass insert modulair-rag/postgres-password
|
||||
```
|
||||
|
||||
## Deploy (Portainer — per `.admin` VDS-ops rule, NOT ssh+compose)
|
||||
1. Open Portainer `https://portainer.vds.kzntsv.site`.
|
||||
2. New Stack → paste `~/projects/modulair-rag/compose.yml` as-is.
|
||||
3. Environment vars — fill from `pass show` + statics:
|
||||
- `ROUTERAI_API_KEY` (pass), `ROUTERAI_BASE_URL=https://routerai.ru/api/v1`, `ROUTERAI_EMBED_MODEL=qwen3-embedding-8b`
|
||||
- `OLLAMA_API_KEY` (pass), `OLLAMA_HOST=https://ollama.com`, `OLLAMA_LLM_MODEL=qwen3-next:latest`
|
||||
- `MINIO_ENDPOINT=minio.kzntsv.site`, `MINIO_PORT=443`, `MINIO_USE_SSL=true`, `MINIO_FORCE_PATH_STYLE=true`, `MINIO_BUCKET=modulair-rag`, `MINIO_REGION=us-east-1`, `MINIO_ACCESS_KEY`/`MINIO_SECRET_KEY` (pass)
|
||||
- `POSTGRES_USER`, `POSTGRES_PASSWORD` (pass), `POSTGRES_DB=modulair_rag`
|
||||
- `IMAGE_TAG=latest`
|
||||
4. Pre-pull images, Deploy.
|
||||
|
||||
## Acceptance
|
||||
- [ ] 4 containers up, no restart-loops (`ops_docker_ps` on VDS).
|
||||
- [ ] `lightrag-modulair :9621` healthcheck responds (internal network, not exposed).
|
||||
- [ ] `https://modulair-mcp.kzntsv.site` → 200 on root.
|
||||
- [ ] `modulair-pipeline` + `tier1-converter` NOT in CPU-spin — fix `[pipeline-tier1-cpu-fix]` already in `:latest` (commits `24c9bd5` + `ceb525e`).
|
||||
- [ ] `modulair-pipeline` log: no `connection refused` to postgres.
|
||||
- [ ] pipeline log: no `NoSuchBucket` / `AccessDenied` from MinIO.
|
||||
|
||||
## Post-deploy
|
||||
1. Rewrite `~/projects/modulair-rag/.wiki/entities/portainer-stack.md` for VDS (Docker host, Portainer URL, MinIO endpoint).
|
||||
2. Close this 🟢 task with note.
|
||||
3. `[future-resilient-architecture-goals]` — add modulair-rag to RTO/RPO matrix (low priority, non-client prod, but host-loss recovery should be documented).
|
||||
|
||||
## Decisions log
|
||||
- 2026-05-27 (exec session, .admin): pre-flight verify вскрыл, что таска писалась по stale-предпосылкам. Исправления:
|
||||
- **Registry образы ОТСУТСТВУЮТ** (open-q #1 = NO). Registry на VDS переустановлен с нуля 2026-05-20 (`pass vds-kzntsv/full-env` коммент: "не мигрировали с kreknin — user accepts loss"). Образы push'ились 2026-05-13 на kreknin-registry → потеряны. Каталог `registry.kzntsv.site/v2/_catalog`: только books/board/vds-ops. → **rebuild всех 3 обязателен.**
|
||||
- **MINIO_ENDPOINT исправлен** (open-q #2): `minio.kzntsv.site` → 89.253.255.**133** (books VDS, чужой!). Правильный route на этом VDS = `minio.vds.kzntsv.site` (traefik label на minio-контейнере = `Host(minio.vds.kzntsv.site)`, health/live=200). tier1-converter сидит только на `modulair` bridge (НЕ на `proxy`) → внутренний `minio:9000` для него не резолвится → нужен внешний route для всех. Итог env: `MINIO_ENDPOINT=minio.vds.kzntsv.site`, PORT=443, SSL=true. Compose не редактирую (paste-as-is), это env-var.
|
||||
- **MinIO creds:** создан scoped svcacct `modulair-rag-svc` (политика — только bucket `modulair-rag`, verified видит только свой bucket). Не root. → `pass modulair-rag/minio-{access,secret}-key`.
|
||||
- **MinIO bucket** `modulair-rag` создан (mc, локальный alias на minio.vds.kzntsv.site root creds).
|
||||
- **Postgres DB** `modulair_rag` создана на `postgres.vds.kzntsv.site:5432` (docker one-off psql, sslmode=require). POSTGRES_USER=postgres (shared), pwd из `pass vds-kzntsv/full-env`.
|
||||
- **DNS** `modulair-mcp.kzntsv.site` → был 94.19.247.14 (мёртвый NAS = домашний IP). User поправил → 89.253.255.94 (verified public+authoritative).
|
||||
- **ROUTERAI key** не было в pass — user дал, сохранён `pass modulair-rag/routerai-key`. OLLAMA key = `pass interns/ollama-cloud-api-key`.
|
||||
- **Portainer API key протух** (401) → auth через admin user/pass (`pass vds-kzntsv/full-env`), endpoint=1 (local).
|
||||
- **BUILD-БЛОКЕР: tier1-converter = 10.2GB (3.36GB сжатый).** Push с локальной машины падал HTTP 499 (home-канал/middlebox рвёт многогиг HTTPS-заливку через traefik; docker push монолитный, без resume). → **Решение: сборка НА VDS** (`ssh vitya@89.253.255.94` ключ `~/.ssh/id_ed25519`, vitya в docker-группе; клон приватного репо из локального gitea `git.kzntsv.site/cancel_music/modulair-rag` юзером OpeItcLoc03 + admin-token; push registry локально). services/ на origin/master идентичен локальному HEAD (cpu-fix ceb525e внутри). IMAGE_TAG=0.1.0.
|
||||
|
||||
- 2026-05-27 (DEPLOY ✅): стек `modulair-rag` (Portainer stack id=15, endpoint=1) развёрнут. Образы 0.1.0 (pipeline/tier1-converter/modulair-mcp) собраны на VDS + lightrag ghcr.io/hkuds/lightrag:latest. Acceptance 6/6 green: 4 контейнера up без restart-loop; lightrag Uvicorn :9621 startup complete; https://modulair-mcp.kzntsv.site → 200 (uvicorn, /docs тоже 200); pipeline+tier1 0% CPU (cpu-fix ok); pipeline-лог без postgres/minio ошибок.
|
||||
- **+1 compose-фикс при деплое:** лейбл `traefik...modulair-mcp.entrypoints=https` — NAS-имя; на этом VDS entrypoints = `web`/`websecure` (читал `/opt/stacks/traefik/data/traefik.yml`). Без фикса traefik default-404. Поправлено в live-стеке (PUT, https→websecure). **TODO в репо modulair-rag/compose.yml** — закоммитить тот же фикс, иначе следующий redeploy снова 404.
|
||||
|
||||
## Post-deploy / follow-ups
|
||||
- [x] **modulair-rag/compose.yml** — `entrypoints=https`→`websecure` (commit `fc68a16`). Параллельная сессия завершилась (tree clean), сделал сам.
|
||||
- [x] **modulair-rag/.wiki/entities/portainer-stack.md** — переписана под VDS (stack 15, websecure, minio.vds, build-on-VDS workflow, model-config table) + log.md entry (`fc68a16`).
|
||||
- [x] **#3 lightrag embedding — RESOLVED & verified end-to-end.** Был latent-баг (с NAS): stock-образ LightRAG читает `LLM_*`/`EMBEDDING_*`, а не `ROUTERAI_*`/`OLLAMA_*` → дефолты (`binding=ollama model=None dim=1024`). Замаплены stack-inputs на нативные имена в compose: `EMBEDDING_BINDING=openai` (RouterAI qwen/qwen3-embedding-8b, **DIM=4096**), `LLM_BINDING=ollama` (Ollama Cloud). LLM-тег `qwen3-next:latest` → 404 на Ollama Cloud → исправлен на **`qwen3-next:80b-cloud`** (user-confirmed; `qwen3-next:80b` тоже валиден). Live-smoke на VDS: insert → embedding(RouterAI)+extraction(Ollama) → `4 entities, 3 relations`, потом workspace очищен. `compose.yml`+`lightrag-models.md` (`fc68a16`).
|
||||
- [ ] `[future-resilient-architecture-goals]` — добавить modulair-rag в RTO/RPO matrix (low prio).
|
||||
- [ ] **PUSH:** `modulair-rag fc68a16` + `.admin` task-коммиты unpushed (автопуш не выдан в этой сессии).
|
||||
|
||||
## Open questions
|
||||
- [x] Registry images present? → NO, rebuilt on VDS (см. Decisions).
|
||||
- [x] minio.kzntsv.site repointed? → NO, это books VDS; используем `minio.vds.kzntsv.site`.
|
||||
|
||||
**Branch:** master
|
||||
<!-- created-by: vitya@modulair-rag-session / 2026-05-27 / from: modulair-rag nas-loss-vds-redeploy-context concept; user handed deploy to .admin (has VDS access) -->
|
||||
50
.tasks/morethencms-s3-filestorage-provider.md
Normal file
50
.tasks/morethencms-s3-filestorage-provider.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# [morethencms-s3-filestorage-provider] — S3-провайдер FileStorage для MoreThenCms (MinIO)
|
||||
|
||||
**Status:** 🟢 LIVE на прод RUVDS (2026-07-03). Split-brain закрыт: боевой catch-all `C:\sites\snolla` теперь читает/пишет ассеты/темы/галереи из MinIO. Локальный IIS (windows-recovery-host) — отложен до ухода сайтов с RUVDS (решение user).
|
||||
|
||||
### Прод-cutover RUVDS (2026-07-03)
|
||||
- Backup: `web.config.bak-pre-s3-2026-07-03` + IIS-снапшот `pre-s3-cutover-2026-07-03`. Rollback = restore + recycle (Local вернётся, MinIO не трогается).
|
||||
- 3 DLL (S3+AWSSDK.*) → `C:\sites\snolla\bin`. web.config `<fileStorageClients>`: 6 контентных классов (assets/galleries/images/scripts/stylesheets/watermarks) → S3, креды в конфиге (ACL SYSTEM+Admins); кэши (imageCache/uploadCache/contentCache/_imageCache-Azure) оставлены Local. Правка — точечный regex, UTF-8/BOM сохранён, XML валиден (10 записей).
|
||||
- Pre-flight: RUVDS→minio.kzntsv.site:443 = TCP+HTTPS 200. Egress ок.
|
||||
- Smoke GREEN: 7 тенантов 200/301 (0× 500); S3-read вживую — theme CSS `16ba5cb8…/css/lato.css` → 200/9239b/text/css, byte-parity с MinIO. Write-механика — self-test цикл (ранее).
|
||||
- imageCache-прун бакета `themes` (2.1 ГБ мусора) — фоном bfb8wy94n.
|
||||
**Валидация (standalone-проба, IIS/elevation не нужны):** Read/key/byte-parity ✅ + full self-test `put→exists→download(MD5==)→delete→exists=false` ✅ после upload-фикса. Upload-фикс: **`UseChunkEncoding=false`** в `PutObjectRequest` (MinIO отвергает AWSSDK 3.7 aws-chunked `STREAMING-AWS4-HMAC-SHA256-PAYLOAD`; `DisablePayloadSigning` НЕ помогает). 17/17 юнитов + интеграция. `_selftest/`-мусора нет.
|
||||
|
||||
### Метод валидации (важно — IIS/elevation НЕ нужны)
|
||||
Провайдер чистый (AWSSDK + базовый FileStorageClient, без SnollaHost/Autofac) → грузится standalone в PowerShell: загрузить 3 DLL из `deploy/` + зависимости из `C:\sites\stostayer.old\bin` (резолвер с guard от рекурсии), инстанцировать `AssetsStorage` через `$type::new($cfgDict)`, гонять против real MinIO. Проба: scratchpad `s3-provider-probe.ps1` + `s3-upload-diag.ps1`. stostayer.old НЕ трогается (только его DLL читаются в чужой процесс). Self-test пишет в throwaway `assets/_selftest/` (чистится).
|
||||
**Owner-split:** код — прогер (проект MoreThenCms); координация + deploy + приёмка — admin (я).
|
||||
|
||||
### Ключ-конвенция (ground-truthed 2026-07-03, финал)
|
||||
Провайдер = калька с **Local** (не Azure — Azure в бою не гонялся), ключи по snolla-фронту `packages/core/lib/services/storage.js` (`key = ownerId.toLowerCase()+'/'+storageFilename`).
|
||||
- **assets**: bucket `assets`, ключ `<ownerId:N>/<file>` — ownerId **без дефисов** (админка отдаёт `ToString("N")`, объекты в MinIO без дефисов). НЕ вставлять дефисы.
|
||||
- **themes** (css/images/**js**/watermarks): единый bucket `themes`, ключ `<themeId:N>/{css,images,js,watermarks}/<file>`. scripts=`js` (не `/scripts`!). watermarks НЕ отдельный бакет — они в `App_Data\themes\<themeId>\watermarks\` (Local `WatermarksStorage`), 11 файлов вкл. tandemmebel.ru.
|
||||
- **galleries**: bucket `galleries`, ключ `<siteId:N>/<file>` **плоско** (без `/images`).
|
||||
- Azure separator-баг (`prefix+fileUri` без `/`) прогер поймал → `Trim('/')+"/"+fileUri`.
|
||||
- Configuration net461→net462 (`OpeItcLoc03/MoreThenCms.Configuration 3e79ff2`) — build-only, на прод НЕ едет (шипим только 3 DLL).
|
||||
- Приёмка на локальном IIS: **key-parity + byte-parity** sweep (ключ совпал с существующими + MD5==).
|
||||
|
||||
## Зачем
|
||||
Админка MoreThenCms пишет ассеты на локальный диск IIS (`App_Data`, провайдер `FileStorage.Local`), а боевой фронт snolla читает из MinIO → split-brain (правка в админке не видна на сайте). Плюс отдельный симптом — 500 на delete из-за RX-only ACL (пофикшен 2026-07-03 выдачей Modify, но это лечит только delete, не устраняет раздвоение). Настоящее закрытие — посадить админку на тот же MinIO через S3-провайдер, которого в кодовой базе нет (есть только `Local` и `Azure`).
|
||||
|
||||
## Решения (приняты)
|
||||
- Новая сборка `MoreThenCms.FileStorage.S3` — калька с `MoreThenCms.FileStorage.Azure`, backend `AWSSDK.S3` против MinIO (`ForcePathStyle=true`).
|
||||
- Объём: **все** контентные классы (Assets/Galleries/Images/Scripts/Stylesheets/Watermarks). Кэши (image/upload/content) остаются Local.
|
||||
- Один MinIO на всех: `minio.kzntsv.site` (books-vds, 89.253.255.133), path-style. Bucket = имя класса, ключ = `<prefix>/<file>` **без ведущего слэша** (parity с уже мигрированными объектами).
|
||||
- Deploy target админки: локальный IIS на **windows-recovery-host** (репрпоуз PC; серверная роль была декоммишнута, сам PC жив). DNS-аудит 2026-07-03: инфра-хосты `*.kzntsv.site` резолвятся в реальные IP, перехвата `127.0.0.1` НЕТ — костыль в конфиг не нужен.
|
||||
|
||||
## Where I stopped
|
||||
ТЗ (полное, под новичка, с таблицей префиксов и выделенной граблей «ведущий слэш в ключе S3») лежит в `~/projects/MoreThenCms/.claude-inbox/2026-07-03T06-40-09Z-admin.md`.
|
||||
|
||||
## Next action
|
||||
Дождаться `MoreThenCms.FileStorage.S3.dll` от прогера → развернуть на локальный IIS (windows-recovery-host) → сквозняк: upload из админки → объект в MinIO с корректным ключом+Content-Type → фронт через imgproxy отдаёт 200 → byte-parity (ETag/MD5). Затем deploy на боевой + переключить `<fileStorageClients>` секцию, секреты прокинуть из окружения (не хардкод).
|
||||
|
||||
## Пайплайн
|
||||
ТЗ ✅ → сборка ⏳ (прогер) → deploy-local ⏳ → сквозняк-приёмка ⏳ → deploy-remote ⏳ → close split-brain.
|
||||
|
||||
## Контекст
|
||||
- `.wiki/concepts/snolla-admin-appdata-acl-500-after-scp-migration.md` (500 + split-brain + fix)
|
||||
- `.wiki/concepts/galleries-storage-class-local-not-s3.md` (Local-класс, миграция в S3, префиксы)
|
||||
- Абстракция: `MoreThenCms/FileStorage/FileStorageClient.cs` + `Azure/AzureCloudStorage.cs` (образец)
|
||||
|
||||
**Branch:** n/a (admin ops + external code)
|
||||
<!-- created-by: vitya@.admin-exec / 2026-07-03 / trigger: user-план «сайты→snolla, админка→локальный IIS», нужен S3-провайдер -->
|
||||
34
.tasks/on-snolla-vds-migration.md
Normal file
34
.tasks/on-snolla-vds-migration.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# on-snolla-vds-migration
|
||||
|
||||
## Goal
|
||||
Мигрировать посадочную `on.snolla.com` с RUVDS IIS (MoreThenCms .NET) на VDS как Node snolla-app `@snollajs/snolla` 0.42.1. Исходников сайта нет — реконструировать из боевого сайта + админки. **Спека:** [[../.wiki/concepts/snolla-local-admin-and-on-snolla-migration-design.md]] §Task B.
|
||||
|
||||
## Facts (из `MoreThenCms` DB)
|
||||
- `SiteId=B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7`, `Alias=on`, `Title="Internal Site"`, `Culture=en`, `Live=1, Production=1`.
|
||||
- Контент (DB-строки + ассеты) уже на vds-kzntsv (общая `MoreThenCms` DB + MinIO) — нужно только репо шаблонов+конфига.
|
||||
|
||||
## Plan / phases
|
||||
- [ ] **1. Реконструкция шаблонов** — через локальный админ ([[snolla-local-admin-restore]]) + боевой `https://on.snolla.com/` HTML: для каждой страницы сопоставить вёрстку с DB-контент-структурой → `layout.liquid` + page/partials. Итеративно (пиши→рендерь→сверяй с боевым).
|
||||
- [ ] **2. Новый репо** `victor/on-snolla` (имя уточнить) — калька `tandemmebel.ru/apps/web` (server.js/index.js/config/views/package.json + deploy/Dockerfile). Пин `@snollajs/snolla` 0.42.1.
|
||||
- [ ] **3. production.json** — siteId `B9ECDB50…`, siteUrl `https://on.snolla.com`, sequelize→`MoreThenCms` `mssql.kzntsv.site:1433`, s3→`minio.kzntsv.site`, imgproxy→`imgproxy.kzntsv.site`, sharp serve-bytes. Секреты в runtime-env стека.
|
||||
- [ ] **4. Build на VDS** (обход traefik-499, build-arg VERDACCIO_TOKEN), push `registry.kzntsv.site/on-snolla:<sha>`.
|
||||
- [ ] **5. Throwaway-staging `:50XX`** из env живого (или нового стека) → completeness-gate: sitemap parity vs прод-on.snolla.com (page-locs NEW==PROD, self-consistency 0 регрессий 404/5xx).
|
||||
- [ ] **6. Stack** Portainer за traefik `Host(on.snolla.com)`, `mem_limit 512m`, env секреты.
|
||||
- [ ] **7. Cutover** — verify авторит. NS reg.ru→89.253.255.94 → traefik Host-rule → LE-серт → live-smoke. По `tandemmebel-vds-deploy-runbook`.
|
||||
|
||||
## Status
|
||||
⚪ ready (unblocked — [[snolla-local-admin-restore]] DONE). **Отложен в след. сессию.** Branch: master.
|
||||
|
||||
## Depends on / blocks
|
||||
- ~~Blocked by: [[snolla-local-admin-restore]]~~ — DONE (локальный админ жив, 6 `/admin` URL).
|
||||
|
||||
## Decisions (locked 2026-07-20)
|
||||
- **Repo:** `victor/on.snolla.com` (на git.kzntsv.site), структура-калька `victor/tandemmebel.ru`.
|
||||
- **Admin-креды:** в БД `MoreThenCms.dbo.Accounts` (читать SQL-user'ом `snolla` / SA `pass mssql-vds/sa-password`). Логин-форма `/admin/account/login`.
|
||||
- **Контент-инспекция:** hosts→127.0.0.1 активен (Task A), `http://on.snolla.com/` рендерит локальный IIS из той же MoreThenCms DB → контент == прод; `/admin` — контент-дерево.
|
||||
|
||||
## Open Q
|
||||
- Сложность посадочной (объём шаблонов) — узнается при реконструкции (Title="Internal Site", culture `en` — вероятно простой лендинг).
|
||||
|
||||
## Rollback
|
||||
Образ предыдущего тега в registry / revert DNS reg.ru→80.64.31.36 (RUVDS IIS жив пока DNS не флипнут).
|
||||
21
.tasks/policy.toml
Normal file
21
.tasks/policy.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
# Project claim-policy for `.admin` (projects-meta agent-orchestration #7).
|
||||
#
|
||||
# Read by `tasks_claim_next` once per project per call; overlaid onto each
|
||||
# candidate via `effectiveClaimFields` BEFORE the claim gate. Override rule is
|
||||
# TOTAL, not intersect — a task that sets a field outright ignores the default
|
||||
# for that field. Affects ONLY the autonomous poller; interactive sessions and
|
||||
# manual claims do not pass through this gate.
|
||||
#
|
||||
# WHY needs-human: `.admin` is ops/infra (SSH, DB, panel, prod cutover, secret
|
||||
# handling). Until execution-policy governance L2/L3 lands (see
|
||||
# `agents-task-runner-vds-deploy` task § blocker), NO `.admin` task may be
|
||||
# claimed + spawned autonomously. `default_weight = "needs-human"` makes every
|
||||
# `.admin` task fail the L3 claim gate (claim.ts `selectClaimableTask`) → it is
|
||||
# never handed to an autonomous runtime and always surfaces to a human.
|
||||
#
|
||||
# A specific `.admin` task that IS safe for autonomy can opt back in by setting
|
||||
# its own `weight:` to something other than `needs-human` (total override).
|
||||
#
|
||||
# RELAX THIS when governance L2/L3 ships: drop default_weight (or set per-task).
|
||||
|
||||
default_weight = "needs-human"
|
||||
28
.tasks/reconcile-local-assets-to-minio.md
Normal file
28
.tasks/reconcile-local-assets-to-minio.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# [reconcile-local-assets-to-minio] — залить всё локальное хранилище RUVDS в MinIO
|
||||
|
||||
**Status:** 🟢 done 2026-07-03 — залито и сверено. Parity чистая: assets 4750/4750 (0 diff checksum), galleries 9984/9984 (долито 9683), themes 27999/27999, maxMind 2/2. Job `b6qb8e10q` exit 0, DONE 10:31:30Z.
|
||||
**Follow-up — ✅ closed 2026-07-03:** per-theme `<themeId>/imageCache/*` (легаси resize-кэш) выпилен из бакета `themes` после S3-cutover'а (17135 объектов / 2.1 ГБ → 0). Реальный контент тем цел (10864 / 671 MiB, счётчик 1:1). Бакет `themes` теперь только реальный контент.
|
||||
|
||||
## Задача
|
||||
Перетащить всё контентное локальное хранилище `C:\sites\snolla\App_Data` (RUVDS IIS) в MinIO (`minio.kzntsv.site`, books-vds). Для уже мигрированных живых сайтов — только verify (все файлы на месте), не перезаписывать. Готовит estate к плану «все сайты → snolla, админка → локальный IIS + S3-провайдер».
|
||||
|
||||
## Разведка (2026-07-03)
|
||||
| класс | диск (файлы/МиБ) | MinIO до | действие |
|
||||
|---|---|---|---|
|
||||
| assets | 4750 / 215.5 | 4750 / 216 | verify (`rclone check --one-way --checksum`) — по кол-ву сходится, ловим drift |
|
||||
| galleries | 9984 / 5522 | 301 (pilorama98) | gap-fill `--ignore-existing` (~9683 файла / 5.4 ГБ) |
|
||||
| themes | 27999 / 2847 | 317 (3 темы из 87) | gap-fill `--ignore-existing` (~2.8 ГБ) |
|
||||
| maxMind | 2 / 34.6 | — | ✅ залито в bucket `maxmind` (по требованию user) verify 0 diff |
|
||||
| uploadCache/contentCache/searchIndexes | кэши/индексы | — | НЕ мигрируем |
|
||||
|
||||
Конвенция ключей — **verbatim** диск→бакет (bucket = имя класса): assets `<ownerId>/<file>`, galleries `<siteId>/…`, themes `<themeId>/{css,images,js}/…`. Проверено на живых объектах.
|
||||
|
||||
## Метод
|
||||
rclone на хосте (env-var remote `min:`), режим gap-fill `--ignore-existing` (живые объекты не трогаются). Логи: `C:\Windows\Temp\mig-*.log`, маркер `mig-DONE.txt`. Скрипт: scratchpad `mig-assets-to-minio.ps1` (+ `mig-maxmind.ps1`).
|
||||
⚠️ Гоча: `Start-Process`-детач на Windows Server Core НЕ переживает закрытие ssh-сессии (процесс убивается с сессией) → запускать attached (ssh держит сессию) либо через Scheduled Task.
|
||||
|
||||
## Where I stopped / Next action
|
||||
Ждать завершения bash-job `b6qb8e10q` → прочитать `mig-galleries-check.log` / `mig-themes-check.log` (post-verify one-way parity) + `mig-assets-check.log` (drift). Файлы «есть в обоих, но различаются» (drift после 13.06) — НЕ перезаписаны, вынести списком user'у (по живым сайтам перезапись — его решение). Финальная сверка counts/size диск vs MinIO. Затем close.
|
||||
|
||||
**Branch:** n/a (admin ops)
|
||||
<!-- created-by: vitya@.admin-exec / 2026-07-03 / trigger: user-задача "перетащить все ассеты RUVDS в minio" -->
|
||||
131
.tasks/restore-elasticsearch-indices-books-vds.md
Normal file
131
.tasks/restore-elasticsearch-indices-books-vds.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# restore-elasticsearch-indices-books-vds
|
||||
|
||||
## Goal
|
||||
|
||||
Восстановить 3 ES индекса (`epz`, `products`, `artmone`) на canonical endpoint `elasticsearch.kzntsv.site` (books VDS, Portainer stack 33). Сейчас target ES пустой → весь поиск в books-app (slovo) сломан (404 `index_not_found_exception` для `epz` и `products`).
|
||||
|
||||
## Симптомы (зафиксировано 2026-05-28 ~15:50 MSK)
|
||||
|
||||
- `bookva.kzntsv.site/api/products/search?q=...` → 500 `no such index [products]`.
|
||||
- `bookva.kzntsv.site/api/epz/search?q=...` → 500 `no such index [epz]`.
|
||||
- `books-api` + `books-web` логи (stderr) — десятки `ResponseError: index_not_found_exception` за час.
|
||||
- User-facing impact: страница `/epz/search` и поиск товаров в slovo UI не работают.
|
||||
|
||||
## Closure note (2026-05-28 ~20:10 MSK)
|
||||
|
||||
🟢 **Восстановлено за 46 сек через snapshot restore** + два preventive фикса в один заход.
|
||||
|
||||
### Restore
|
||||
|
||||
```
|
||||
POST /_snapshot/kreknin/daily-2026-05-25/_restore?wait_for_completion=true
|
||||
{"indices":"epz,products,artmone","include_global_state":false,"include_aliases":true,"index_settings":{"number_of_replicas":0}}
|
||||
```
|
||||
|
||||
- artmone: 2621 docs ✓ (source: 2621)
|
||||
- epz: 820604 docs ✓ (source: 820604)
|
||||
- products: 105922 docs ✓ (source: 105922)
|
||||
- cluster: green
|
||||
- 3 consumers (books-api/task-runner/job-scheduler) рестартованы — 0 `index_not_found_exception` в логах за 5 мин
|
||||
- public smoke `bookva.kzntsv.site/api/{epz,products}/search` → 401 от auth middleware (поиск-pipeline жив, до wipe было 500)
|
||||
|
||||
**Ключевая удача:** в исходной задаче было записано «`pre-migration-2026-05-25` содержит только `read_me` placeholder — не годится для восстановления» — это правда, но это был **другой** snapshot (он был _до_ reindex'а). А `daily-2026-05-25` (сделан кроном `books-vds-backup-daily-kreknin` в 09:51 UTC через 2ч _после_ reindex'а) содержал full corpus — `[read_me, products, epz, artmone, .tasks]`, 25 sec duration, 5 шардов SUCCESS. Snapshot pipeline (стояший с 25.05) автоматически создал safety net.
|
||||
|
||||
### Root cause (forensics из ES container log)
|
||||
|
||||
Container `elasticsearch` `restart_count=0, created=2026-05-25T07:51:38` — **никогда не рестартовал**, volume bind не задет, индексы существовали и были **удалены через ES API**:
|
||||
|
||||
```
|
||||
2026-05-26 10:21:43.526 UTC [read_me/IfM2V1pTQk-...] deleting index
|
||||
2026-05-26 10:21:44.075 UTC [artmone/xQdFWkafSKy-...] deleting index
|
||||
2026-05-26 10:21:44.257 UTC [epz/AV8inT4YS-e-...] deleting index
|
||||
2026-05-26 10:21:44.450 UTC [.tasks/EbutxIzdTPSq-...] deleting index
|
||||
2026-05-26 10:21:44.576 UTC [products/6VRXsuP5QV-...] deleting index
|
||||
```
|
||||
|
||||
5 индексов (включая system `.tasks`) удалены за 1 секунду = `DELETE _all` / `DELETE *` / Kibana DevTools «delete index».
|
||||
|
||||
Timeline:
|
||||
- **2026-05-25 09:51 UTC** — daily snapshot SUCCESS [read_me, products, epz, artmone, .tasks] (full data)
|
||||
- **между ~10:00 и 26.05 03:02 UTC** — Wipe #1 (snapshot 26.05 содержит только [read_me])
|
||||
- **2026-05-26 05:49–06:44 UTC** — re-reindex (artmone+epz+products, full counts восстановлены)
|
||||
- **2026-05-26 09:10:18 UTC** — `bookva-es` container created (cutover-prep Step 2)
|
||||
- **2026-05-26 10:21:43 UTC** — Wipe #2 (1ч 11мин после создания bookva-es)
|
||||
|
||||
`bookva-es` использует named volume `bookva-es-data`, БЕЗ traefik labels (internal-only). Не задел canonical через mount или routing. **Best guess:** оператор в момент cutover-prep хотел очистить новый `bookva-es:9200`, но команда ушла на `elasticsearch.kzntsv.site` (canonical, slovo). Bookva-es internal-only → DELETE с воркстейшна туда не дойти; canonical через traefik basicAuth → доходит.
|
||||
|
||||
**Caller identity unrecoverable:** ES audit log = X-Pack платная фича (нет на free 7.10). Traefik accessLog был отключён (нет секции `accessLog:` в `traefik.yml`). Portainer audit = CE без enterprise. Только timestamp + DELETE fact в ES log.
|
||||
|
||||
### Preventive fixes (applied 2026-05-28 ~20:10 MSK)
|
||||
|
||||
**Fix #1: `action.destructive_requires_name=true`** на stack 33 (env var добавлен через Portainer PUT API). Verified:
|
||||
- `DELETE /_all` → 400 «Wildcard expressions or all indices are not allowed»
|
||||
- `DELETE /ep*` → 400 same
|
||||
- `DELETE /probe-canary` (by exact name) → 200 (легитимные операции работают)
|
||||
|
||||
Pattern удаления который случился (5 индексов в 1 сек) теперь **физически невозможен**.
|
||||
|
||||
**Fix #2: traefik accessLog** в JSON формат, `/letsencrypt/access.log` (bind-mounted, persistent). Backup `traefik.yml.bak-pre-accesslog-2026-05-28` рядом. Verified entries содержат `RequestMethod`, `RequestHost`, `RequestPath`, `ClientHost`, `ClientUsername`, `DownstreamStatus`, `TLSCipher`. Будущие `DELETE` запросы оставят след — даже если кто-то снова попадёт не на тот endpoint, retrospective forensics возможен.
|
||||
|
||||
### Artifacts
|
||||
|
||||
- `.scratch/stack33-put-2026-05-28.json` — Portainer PUT payload (новый env + сохранён `reindex.remote.whitelist`).
|
||||
- `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md` — incident concept (написан в эту сессию).
|
||||
- ES container env post-fix: `reindex.remote.whitelist=elasticold.kzntsv.site:443` + `action.destructive_requires_name=true`.
|
||||
- Traefik static config: `accessLog: { filePath: /letsencrypt/access.log, format: json, bufferingSize: 100 }`.
|
||||
|
||||
### Follow-ups (не блокеры)
|
||||
|
||||
- **Logrotate на `access.log`** — при ~1 req/sec будет ~30–50 MB/day. Disk free 63G, ressedimption через 1–2 года максимум — но добавить logrotate стоит. Отдельная micro-task.
|
||||
- **Переезд access.log из `/letsencrypt/`** в отдельный bind (`/usr/docker/traefik/logs/`) — косметика, текущее место persistent и работает.
|
||||
- **Pass entry update** — комментарий в `books-vds/full-env` устарел: «NOT Portainer-managed (legacy SSH-compose): **elasticsearch**, mongo, minio, books-db…». Migration 25.05 (`books-vds-stacks-to-portainer`) уже мигрировала их. Поправить при следующем pass-edit.
|
||||
|
||||
## Текущее состояние ES endpoint'ов
|
||||
|
||||
**Source (Windows host, rollback — не выключен):** `https://elasticold.kzntsv.site/`
|
||||
|
||||
```
|
||||
index health docs.count store.size
|
||||
artmone yellow 2621 1.4mb
|
||||
epz yellow 820604 699.7mb
|
||||
products yellow 105922 26.6mb
|
||||
```
|
||||
|
||||
**Target (books VDS, Portainer stack 33, canonical):** `https://elasticsearch.kzntsv.site/` — restored 2026-05-28, counts == source, green.
|
||||
|
||||
## ⛔ RECURRENCE 2026-05-29 — RCA был НЕВЕРНЫМ, дыра не закрыта
|
||||
|
||||
Через ~3ч после вчерашнего restore индексы снова исчезли (`artmone`/`epz`/`products`
|
||||
удалены 28.05 20:13 UTC, остался ransom-`read_me`). Вчерашний best-guess «оператор в
|
||||
cutover» **опровергнут**.
|
||||
|
||||
**True root cause:** stack 33 публиковал `ports: - 9200:9200` → docker прокинул
|
||||
`0.0.0.0:9200` на публичный IP в обход traefik+firewalld. ES 7.10 free **без auth** →
|
||||
`curl http://89.253.255.133:9200/` без кредов возвращал cluster info. **Ransom-бот**
|
||||
сканил порт, удалял индексы by-name (мимо вчерашнего Fix #1, который ловит только
|
||||
`_all`/`*`), оставлял `read_me` с требованием 0.0041 BTC. accessLog (Fix #2) пуст по
|
||||
DELETE — бот шёл прямо в `:9200`, не через traefik. bookva не задет (bookva-es порт не
|
||||
публикует).
|
||||
|
||||
**Fix #3 (applied 2026-05-29):** убран `ports:` блок из stack 33 (Portainer PUT
|
||||
`/api/stacks/33?endpointId=1`). Порт больше не публикуется (`docker ps` → `9200/tcp` без
|
||||
`0.0.0.0:`, внешний `curl :9200` refused), traefik route жив. Затем `DELETE /read_me` +
|
||||
restore `epz,products,artmone` из `daily-2026-05-25` (counts 820604/105922/2621, green) +
|
||||
restart `books-api books-task-runner books-job-scheduler`. Поиск slovo через traefik
|
||||
отдаёт хиты. Полный разбор: `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md`
|
||||
§ «Рецидив 2026-05-29».
|
||||
|
||||
**Spawned follow-up:** `harden-books-vds-exposed-ports` — exposure-audit нашёл ещё 5
|
||||
сервисов с публичными host-портами (mongo/books-db/bookva-db/minio/bookva-minio, все
|
||||
credentialed) + rsync:873. Не emergency, но закрыть.
|
||||
|
||||
## Branch
|
||||
|
||||
n/a (admin ops)
|
||||
|
||||
## Status
|
||||
|
||||
closed 2026-05-28 → **reopened+reclosed 2026-05-29** (true RCA: exposed port, Fix #3 applied)
|
||||
|
||||
<!-- created-by: vitya@books-session / 2026-05-28 / trigger: prod slovo поиск товаров+EPZ сломан, ES indices пусты на canonical endpoint -->
|
||||
<!-- closed-by: vitya@.admin-exec / 2026-05-28 / snapshot restore + 2 preventive fixes + wiki concept ingest -->
|
||||
39
.tasks/snolla-local-admin-restore.md
Normal file
39
.tasks/snolla-local-admin-restore.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# snolla-local-admin-restore
|
||||
|
||||
## Goal
|
||||
Восстановить локальный .NET-админ (catch-all IIS-сайт `snolla`) на этой машине (`windows-recovery-host`), снесённый 2026-06-08 при декоммишене. Назад — с RUVDS (текущий прод-админ с MinIO drop-in). Даёт `/admin` для всех сайтов тиража + on.snolla.com локально. **Спека:** [[../.wiki/concepts/snolla-local-admin-and-on-snolla-migration-design.md]] §Task A.
|
||||
|
||||
## Why
|
||||
Тираж уехал на VDS (Node), админка осталась на RUVDS .NET. Нужно локально редактировать контент (особенно on.snolla.com для Task B) без зависимости от RUVDS. Временно — пока админ не мигрирован на VDS.
|
||||
|
||||
## Addressing (confirmed оператором)
|
||||
Catch-all `*:80` + `hosts`-override → `127.0.0.1 <alias>.snolla.com`. Один AppPool `snolla`. URL = `<alias>.snolla.com/admin`:
|
||||
- tandemmebel.snolla.com/labtools.snolla.com/labtoolspro.snolla.com/emspb.snolla.com/kupimknigi.snolla.com/on.snolla.com
|
||||
|
||||
## Plan / phases
|
||||
- [x] **0. Verify source (read-only SSH RUVDS)** — plink `-hostkey` (fingerprint `SHA256:r/vSKU5WzH4B8T7RiXyXlg0D8XZ9hlBxzmdrPzuPWzE`, креды `pass show ruvds-iis/full-env`). `C:\sites\snolla\` есть, IIS site `snolla` Started, catch-all `*:80` + SNI `*.snolla.com`/real-domains. **Web.config уже → `mssql.kzntsv.site,1433;Catalog=MoreThenCms;User Id=snolla;Password=...` (тот же snolla SQL-user что stostayer.old).** MinIO drop-in применён: assets/galleries/images/scripts/stylesheets/watermarks → `FileStorage.S3.*`, endpoint `minio.kzntsv.site`, **ключи реальные** (placeholder_count=0, accessKey=20/secretKey=40). Кэши Local.
|
||||
- [x] **1. Копировать `C:\sites\snolla\`** RUVDS→локально — **selective tar-over-SSH ~100MB** (НЕ 8.76GB): `bin`(53MB, вкл S3 DLLs)+`Admin`(6MB)+`Areas`+`Views`+`Web.config`+`App_Data/{maxMind,searchIndexes,contentCache}`, исключая stale `App_Data/{assets(215MB),galleries(5.5GB),themes(2.8GB),uploadCache(145MB)}` + `bin.old`. Команда: `plink ... "tar -C C:/sites/snolla -cf - --exclude=App_Data/assets --exclude=App_Data/galleries --exclude=App_Data/themes --exclude=App_Data/uploadCache --exclude=bin.old --exclude=App_Data/imageCache ." | tar -C /c/sites/snolla -xf -`.
|
||||
- [x] **2. Репойнт conn-string** — НЕ НУЖНО, RUVDS-Web.config уже на `mssql.kzntsv.site` (cutover 2026-05). DB-креды `snolla`/`fXkH4@8O%3pc` (plaintext в Web.config, тот же user работает для MoreThenCms+stostayer catalogs). Q2 закрыта.
|
||||
- [x] **3. MinIO-ключи** — уже реальные в скопированном Web.config (см. step 0). Pass-lookup не понадобился.
|
||||
- [x] **4. IIS-сайт `snolla`** — elevated `scripts/local-snolla-admin-restore/setup-local-snolla-admin.ps1` (ASCII-only, PS5.1 BOM-less-safe): AppPool `snolla` (.NET v4.0, AppPoolIdentity, recycling.memory 200MB), IIS site `*:80` catch-all, ACL `IIS AppPool\snolla:(OI)(CI)(M)`.
|
||||
- [x] **5. hosts-override** — `127.0.0.1 tandemmebel/labtools/labtoolspro/emspb/kupimknigi/on .snolla.com` в `hosts` (marker `# snolla-local-admin`, idempotent).
|
||||
- [x] **6. FW** — `New-NetFirewallRule block-inbound-80-snolla-local` (Action Block, loopback не фильтруется → local-only). URL Rewrite rule НЕ ставил (скопированный inert).
|
||||
- [x] **7. Smoke** — `curl --noproxy '*' -H "Host: <alias>.snolla.com" http://127.0.0.1/admin/account/login` → **200 для всех 6** (tandemmebel/labtools/labtoolspro/emspb/kupimknigi/on). HTML = реальная MoreThenCms-логинформа (`<title>SNOLLA</title>`, `<form action="/admin/login">`). hosts + IIS listening confirmed.
|
||||
|
||||
## Status
|
||||
🟢 DONE (2026-07-20). Branch: master. Local .NET-админ поднят, 6 адресов `/admin` живые.
|
||||
|
||||
## Verify-факт
|
||||
- Catch-all routing: `appSettings` = `sitePath=C:\sites\snolla\`, `primaryDomain=snolla.com`, `primaryAlias=on` (default-site = on.snolla.com "Internal Site"); siteId резолвится из Host-header (нет siteId в appSettings, в отличие от per-site stostayer.old). `customErrors mode="Off"`.
|
||||
- bin/ несёт `MoreThenCms.FileStorage.S3.dll` + `AWSSDK.Core.dll` + `AWSSDK.S3.dll` (drop-in).
|
||||
|
||||
## Depends on / blocks
|
||||
- Блокирует: [[on-snolla-vds-migration]] (нужен админ для контент-инспекции on.snolla.com).
|
||||
|
||||
## Open Q
|
||||
- ~~MoreThenCms DB app-аккаунт~~ — ЗАКРЫТА: `snolla` SQL-user уже в Web.config (plaintext), работает для MoreThenCms catalog.
|
||||
- Включать все ~60 сайтов или только тираж(5)+on.snolla.com — пока 6 в hosts; catch-all даёт все, расширить = дописать `<alias>.snolla.com` в hosts.
|
||||
- **MinIO upload-acceptance** (не автотест): оператор логинится в админку, заливает тест-ассет → проверяет объект в MinIO `minio.kzntsv.site` (bucket assets). S3-провайдер был admin-self-test green на RUVDS, Web.config скопирован 1:1 → должен работать.
|
||||
|
||||
## Rollback
|
||||
Удалить IIS-сайт `snolla` + `C:\sites\snolla\` + откатать hosts (снести marker `# snolla-local-admin`). FW-block-rule можно оставить. Прод-RUVDS не тронут.
|
||||
58
.tasks/stateful-split-volume-copy.md
Normal file
58
.tasks/stateful-split-volume-copy.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# stateful-split-volume-copy
|
||||
|
||||
Поднять 2 MariaDB-контейнера (`bookva-db` + `slovo-db`) на VDS как копии текущего `books-db` через `cp -a` docker volume. Всё.
|
||||
|
||||
## Контекст dev-source
|
||||
|
||||
Дизайн: victor/books `.wiki/concepts/tenant-split.md` § «Фаза 1 — Physical stateful split».
|
||||
Brainstorm trace: `~/projects/.workshop/.archive/2026-05-24-books-tenant-split.md`.
|
||||
Direction: Slovo остаётся на текущем VDS, Bookva переезжает потом (см. `books-vds-bookva-bootstrap`).
|
||||
|
||||
## Goal
|
||||
|
||||
`bookva-db` и `slovo-db` бегут на VDS на своих volume'ах (`bookva-db-data` / `slovo-db-data`) — bit-for-bit копии текущего `books-db_data`. Оба контейнера видят полный shared-датасет (cleanup чужих rows — отдельной задачей).
|
||||
|
||||
## Plan (на VDS под Portainer)
|
||||
|
||||
Через Portainer — потому что VDS-rule: «stacks управляются через Portainer» (см. `.wiki/concepts/portainer-stack-management-vds.md`). Compose-файлы лежат в `~/projects/bookva-overlay/` + `~/projects/slovo-overlay/` (закрыта `overlay-repos-bootstrap-bookva-slovo` в victor/books, commit d0eb210).
|
||||
|
||||
```bash
|
||||
# Pre-flight: backup
|
||||
SRC=$(docker volume ls -q | grep books-db)
|
||||
mkdir -p /backups
|
||||
docker run --rm -v $SRC:/data:ro -v /backups:/backup alpine \
|
||||
tar czf /backup/books-db-pre-split-$(date +%Y%m%d).tar.gz -C /data .
|
||||
|
||||
# Stop books-db (maintenance window 5-10 мин)
|
||||
docker stop books-db # api/web/scheduler/task-runner запаникуют — это OK на короткое окно
|
||||
|
||||
# Copy
|
||||
docker volume create bookva-db-data
|
||||
docker volume create slovo-db-data
|
||||
docker run --rm -v $SRC:/from:ro -v bookva-db-data:/to alpine sh -c "cp -a /from/. /to/"
|
||||
docker run --rm -v $SRC:/from:ro -v slovo-db-data:/to alpine sh -c "cp -a /from/. /to/"
|
||||
|
||||
# Bring up new DBs (через Portainer — stack из bookva-overlay/slovo-overlay)
|
||||
# либо ad-hoc compose-up если stack ещё не создан в Portainer
|
||||
docker start books-db # вернуть текущий стек
|
||||
```
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `docker ps` показывает `bookva-db` + `slovo-db` живыми.
|
||||
- `docker exec bookva-db mariadb -uroot -p... -e 'SELECT 1'` → `1`.
|
||||
- `docker exec slovo-db mariadb -uroot -p... -e 'SELECT 1'` → `1`.
|
||||
- Текущий `books-db` стек снова в строю (api/web/scheduler/task-runner подняты).
|
||||
|
||||
## Out of scope (отдельные ops-таски потом)
|
||||
|
||||
- Mongo volume copy (`bookva-mongo` + `slovo-mongo`).
|
||||
- MinIO bucket rename (`books` → `bookva` + `slovo`).
|
||||
- DELETE чужих rows из каждой БД (`WHERE id_seller=<other>`).
|
||||
- deleteMany чужих agenda-jobs в Mongo.
|
||||
- Up `bookva-api` / `bookva-web` / `bookva-scheduler` / `bookva-task-runner` (и зеркально для slovo) — application stacks.
|
||||
- Cleanup старых `books-*` контейнеров после parallel-run.
|
||||
|
||||
## Blocker
|
||||
|
||||
— (overlay-репо готовы; compose-файлы в `bookva-overlay/deploy/db.compose.yml` / `slovo-overlay/deploy/db.compose.yml`).
|
||||
38
.tasks/tandemmebel-deploy-snolla-0-42-0.md
Normal file
38
.tasks/tandemmebel-deploy-snolla-0-42-0.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# tandemmebel-deploy-snolla-0-42-0
|
||||
|
||||
## Goal
|
||||
Пересобрать VDS-staging образ tandemmebel на движке `@snollajs/snolla@0.42.0` (core 0.24.0 / data 0.14.1) — суперсидит ранее принятый 0.16.2-staging (`b02ca18`). Топология = вариант A (оператор): пересборка staging ДО cutover, прод не трогаем in-place. Прод-флип остаётся тем же DNS-gated событием (reg.ru→89.253.255.94, хозяин сайта) из paused `[tandemmebel-web-vds-deploy]`. Этот таск готовит образ + перепрогоняет гейты.
|
||||
|
||||
## Key files
|
||||
- `~/projects/tandemmebel.ru/apps/web/package.json:12` — пин `@snollajs/snolla` (сейчас 0.40.0, нужен 0.42.0)
|
||||
- `~/projects/tandemmebel.ru/deploy/Dockerfile` — multi-stage node:22-slim build
|
||||
- `host-stacks/vds-kzntsv/tandemmebel.compose.yml` — Portainer стек 20, source-of-truth (staging-host; боевые Host() только в cutover-комменте)
|
||||
- `.wiki/concepts/labtools.pro-vds-deploy-runbook.md` — рунбук-зеркало
|
||||
|
||||
## Decisions log
|
||||
- 2026-07-12: **In-place bump 0.42.0→0.42.1 🟢 LIVE.** Поверх cutover'а 2026-07-12 (стек 20 уже боевой, образ ed96b18). Консюмер-бамп сделан оператором сам (пин `apps/web/package.json:12` 0.42.0→0.42.1 + `yarn install` lock + commit `0cd9351` + push origin подтверждён ls-remote — тот же блокер-паттерн 0.42.0 решён без запроса dev-source). Verdaccio: snolla 0.42.1/core 0.24.1/liquid 0.10.2 published (latest). Build на VDS → `registry.kzntsv.site/tandemmebel:0cd9351` (digest f29c187f). Throwaway-staging :5020 из env живого стека, healthy. Completeness-gate С VDS: **184/184 parity** (NEW==PROD, включая 0.42.x sitemap-реструктуризацию), единственный 404 `/articles` идентичен прод-оракулу → benign. Operator-gated PUT стека 20 (env 8/8 сохранён, node put-stack.js, prune:false pullImage:true) → контейнер 0cd9351+healthy за ~8s. Live-smoke GREEN (robots/home/projects/sitemap 200, TLS-серт CN=tandemmebel.ru не дёрнут). 0.42.1 = order-tag Drop-field fix (инертен на этой теме — блог-портфолио без e-commerce каталога, как archive на 0.40→0.42). Rollback = тег ed96b18 в registry (+ b02ca18). Tandemmebel закрывает тираж 0.42.1: теперь ВСЕ 5 snolla-сайтов (labtools.ru/emspb.ru/labtools.pro/kupimknigi+tandemmebel) — kupimknigi+tandemmebel были на 0.42.0, tandemmebel подтянут до 0.42.1.
|
||||
- 2026-07-04: **CLOSED 🟢.** Staging пересобран на 0.42.0 (ed96b18), оба acceptance-гейта GREEN на новом образе (не унаследованы). Caveat dev-source подтвердился эмпирически: дельта 0.40→0.42 (archive-роуты) инертна на этой теме — completeness 172/172 + gallery 12/12 без диффов. Cutover остаётся отдельным DNS-gated шагом. Follow-up (не блокер): Dockerfile печёт VERDACCIO_TOKEN в ARG/ENV → build-secret (передано dev/workshop).
|
||||
- 2026-07-04: **BLOCKED на входе.** Проверил репо-провенанс: snolla-репо на 0.42.0 (`bcee2d4`, опубликовано), НО tandemmebel.ru origin/master `9fa30a7` пинит `@snollajs/snolla 0.40.0` — консюмер-бамп 0.40→0.42 не закоммичен/не запушен (ни один реф не содержит 0.42/0.24). Byte-verify «199/199 на 0.42.0» из тела таски гонялся против непушнутого локального бампа. Запросил у dev-source (victor/snolla inbox) коммит-бамп + sha. Peer-дисциплина: заявленный state таски ≠ реальность репо → репорт оператору, не проглатывать.
|
||||
- 2026-07-04: Топология A ратифицирована оператором+workshop. Cutover остаётся DNS-gated (внешнее событие).
|
||||
|
||||
## Open questions
|
||||
- [ ] Опубликован ли `@snollajs/snolla@0.42.0` в verdaccio/registry так, что `yarn install` в docker-build его резолвит? (snolla commit говорит «published» — проверить при build)
|
||||
- [ ] Развести с paused `[tandemmebel-web-vds-deploy]` — пометить старый пин суперсиженным, не плодить два конкурирующих образа.
|
||||
|
||||
## Completed steps
|
||||
- [x] Вызваны обязательные скилы (using-tasks, project-discipline, using-vds-ops).
|
||||
- [x] Репо-провенанс проверен — блокер выявлен (консюмер-пин не запушен).
|
||||
- [x] Запрос на пуш бампа отправлен dev-source (victor/snolla).
|
||||
- [x] Dev-source запушил бамп → sha `ed96b18`, верифицирован (пин+yarn.lock=0.42.0/core0.24.0/data0.14.1), verdaccio publish подтверждён (snolla 0.42.0, core 0.24.0, data 0.14.1 present).
|
||||
- [x] Собрал образ на VDS `registry.kzntsv.site/tandemmebel:ed96b18` (digest ca4da79, 583MB, BUILD_EXIT=0), запушил в registry.
|
||||
- [x] Обновил compose (source-of-truth) на ed96b18, PUT стека 20 через Portainer API (env 8/8 сохранён, PullImage) → контейнер healthy, running==ed96b18.
|
||||
- [x] Acceptance#1: SSR / →200, /projects →200 (real title, не 500).
|
||||
- [x] Acceptance#2 completeness-DoD с VDS: sitemap 172/172 паритет (0 real-fail), gallery-grid 12/12 точный паритет, крошки+title==prod, sharp webp serve-bytes ✅.
|
||||
- [x] Обновил paused [tandemmebel-web-vds-deploy] — образ к флипу = ed96b18 (суперсидит b02ca18).
|
||||
- [x] Таска → 🟢 done.
|
||||
|
||||
## Notes
|
||||
- Acceptance (из тела таски на борде): (1) pre-deploy SSR `/projects`+blog-listing → 200 не 500 (новые SELECT-колонки); (2) completeness-DoD перепрогнать на НОВОМ 0.42.0-образе (188 URL + gallery grid 15 роутов), не наследовать GREEN с 0.16.2; (3) archive-страницы НЕ блокер (projects archivesPath=""); (4) rollback наготове.
|
||||
- Build-рецепт: `git -C ~/projects/tandemmebel.ru archive <sha> | ssh vitya@89.253.255.94 tar -x`; `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/tandemmebel:<sha> . && push`. Portainer PUT/POST через curl -X + node (НЕ PS Invoke-RestMethod).
|
||||
- Smoke гнать С VDS (воркстейшн ловит LAN-DNS-перехват прод-доменов).
|
||||
- Notify: OpeItcLoc03/workshop (оркестратор) + heads-up victor/snolla (dev-source).
|
||||
96
.tasks/vehicles-loader-progress-deploy.md
Normal file
96
.tasks/vehicles-loader-progress-deploy.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# vehicles-loader-progress-deploy
|
||||
|
||||
## Goal
|
||||
|
||||
Доставить `@stostayer/vehicles-loader` **0.4.0** (прогресс-логирование) на прод клиента и
|
||||
верифицировать, что `journalctl` во время боевого `sync` показывает движение, а не ~2ч тишины.
|
||||
|
||||
Код-сайд готов: progress-logging зашипан в `stostayer.new` master (commit `a74ef73`, bump
|
||||
0.3.0 → 0.4.0, 24/24 теста зелёные). Это чисто ops-handoff: rebuild образа на той же
|
||||
схеме, что и 0.3.0 (build здесь → push в registry клиента `docker.stostayer.ru` → host
|
||||
pull + re-tag), затем дождаться/прогнать sync и снять лог.
|
||||
|
||||
Закрывает единственный не-верифицированный acceptance-критерий исходной таски
|
||||
`stostayer.new/.tasks/vehicles-loader-progress-logging.md` — «journalctl показывает движение»
|
||||
(остальные 4/5 покрыты unit-тестами; этот по природе требует живого прод-прогона).
|
||||
|
||||
## Что нового в 0.4.0 (что должно появиться в логе)
|
||||
|
||||
- Фазовые строки: `▶ <phase> — start` / `✓ <phase> — done: N rows, <dur>` для
|
||||
branches / vehicles / units / delete-sweep.
|
||||
- Батч-прогресс: ` manufacturers: 18/183 (12s)`, ` units: 5/26 (…)` — авто-шаг ≈ total/10.
|
||||
- delete-sweep per-model: ` delete-sweep <model>: N disabled` / `0 — выгрузка полная` /
|
||||
`skip (пустой seen-set)`.
|
||||
|
||||
## Pending ops actions
|
||||
|
||||
- [x] **Build & push** на dev-машине из корня `stostayer.new` (2026-05-30): образ
|
||||
`docker.stostayer.ru/vehicles-loader:0.4.0` собран (811MB, digest
|
||||
`sha256:f2e10b1f090ba47f9b5de83b84f91c5ce827ecaf1e94bc0331f2d6773f85845a`),
|
||||
`docker login` BA-кредами → `docker push` (общие слои с 0.3.0, докинуты только app-слои).
|
||||
verdaccio-депы из `.yarn/cache` запеклись при build.
|
||||
- [x] **На хосте клиента** (ssh `victor@new.stostayer.ru:20435`, docker через `sudo -S`):
|
||||
`docker pull …:0.4.0` (digest совпал) + `docker tag …:0.4.0 vehicles-loader:latest`.
|
||||
`:latest` теперь `0f8a4dd46236` = 0.4.0; 0.3.0 (`5407c0563e44`) оставлен под rollback.
|
||||
- [x] Плановый прогон отстрелял **Sun 2026-05-31 06:21:48 → 07:53:14 MSK** на changed-выгрузке
|
||||
(не skipped — 1С перезаписала файл в 6:00, checksum разошёлся).
|
||||
- [x] **Live-verify выполнен (2026-05-31):** журнал показал движение по всем фазам —
|
||||
`▶ branches/vehicles/units/delete-sweep — start`, батч-прогресс
|
||||
(`manufacturers: 18/183 … 183/183`, `units: 3/26 … 26/26`), `✓ … done: N rows`,
|
||||
per-model `delete-sweep … 0 — выгрузка полная`. **НЕ тишина.** `Finished … Deactivated
|
||||
successfully` = exit 0. `importRun id=4 ok`, `reportJson errors:[]`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Образ `docker.stostayer.ru/vehicles-loader:0.4.0` собран здесь и доступен на хосте клиента,
|
||||
`:latest` указывает на 0.4.0.
|
||||
- В `journalctl -u vehicles-loader.service` боевого прогона видно движение по фазам +
|
||||
батч-прогресс + per-model delete-sweep counts (не тишина после трёх `Total …`).
|
||||
- Данные залились корректно (`importRun` новый `ok`-ряд), email-отчёт ушёл (попутно
|
||||
закрывает остаток «живой email-SEND в составе sync» из `vehicles-loader-image-distribution`).
|
||||
|
||||
## Decisions log
|
||||
|
||||
- 2026-06-01: **прод-подтверждение за 01.06 (опц. follow-up из хендоффа закрыт).** Плановый прогон
|
||||
06:20:45→07:55:22 MSK, `importRun id=5 status=ok`, `errorMessage=NULL`, exit 0. Progress-logging
|
||||
показал движение по всем фазам (batch `units 26/26`, per-model delete-sweep `0 — выгрузка полная`).
|
||||
Counts: vehicles 3884, units 100529, `service updated:103`. **Email реально дошёл до gmail** (user
|
||||
подтвердил «письмо пришло») — фикс адресата отработал на боевом прогоне, не только на тест-письме.
|
||||
`generation unmatched:51` стабилен (== id=4 за 31.05) — не регрессия, остаётся follow-up'ом.
|
||||
- 2026-05-31: **CLOSED 🟢.** Live-verify прогона 06:21→07:53 MSK прошёл (см. Pending ops actions).
|
||||
Progress-logging работает как задумано. units-фаза = 1h 31m на 100529 rows (узкое место,
|
||||
кандидат на оптимизацию — follow-up, не блокер). generation `unmatched:51` — глянуть отдельно.
|
||||
- 2026-05-31: **email-баг найден и починен.** 4-й acceptance («email ушёл») валился молча:
|
||||
`STOSTAYER_MAIL_TO=site@stostayer.ru` в `/etc/stostayer/vehicles-loader.env` — отчёт слался
|
||||
сам себе, а не user'у. Отправка отрабатывала успешно (потому прогон и `ok`), адресат неверный.
|
||||
Фикс: `STOSTAYER_MAIL_TO=vitya.kuznetsov@gmail.com` (бэкап `vehicles-loader.env.bak.20260531`),
|
||||
`FROM=site@stostayer.ru` без изменений (это и есть SMTP-аккаунт релея `mail.stostayer.ru`).
|
||||
Доставка подтверждена тест-письмом из контейнера: `ACCEPTED=[gmail]`, `250 queued as 8732C122F18`.
|
||||
Хвост: в репо `config/default.json` дефолт `to` всё ещё `site@stostayer.ru` (прод перекрыт env) —
|
||||
опц. выровнять в `stostayer.new`.
|
||||
- 2026-05-30: **build+push+host-deploy выполнены.** Образ 0.4.0 собран здесь, запушен в
|
||||
`docker.stostayer.ru`, на хосте pull + re-tag `:latest` → 0.4.0. 0.3.0 retained для rollback.
|
||||
- 2026-05-30: open question #1 (push кода) снят — `a74ef73` оказался **уже в origin/master**
|
||||
(`git branch -r --contains a74ef73` → origin/master), дерево чистое. Билдил из чистого дерева,
|
||||
Rule-4-вопроса нет.
|
||||
- 2026-05-30: open question #2 (verify-окно) решён user'ом = **ждём natural 06:20** (Sun 31.05),
|
||||
НЕ форсим baseline-reset. Причина: форс = внеплановое email-письмо клиенту + спурьёзный
|
||||
ре-импорт; natural-прогон и так = боевой acceptance. Trade-off: verify unattended, в след. сессии.
|
||||
- 2026-05-30: заведено из `stostayer.new` session по явному указанию (закрыть код-таску
|
||||
by-inspection, live-verify вынести в .admin как deploy-follow-up).
|
||||
|
||||
## Open questions
|
||||
|
||||
- [x] ~~Push `stostayer.new` master нужен до build?~~ — нет, `a74ef73` уже в origin/master.
|
||||
- [x] ~~Где взять changed-выгрузку?~~ — ждём natural 06:21 Sun 31.05 (1С перезапишет файл в 6:00).
|
||||
|
||||
## Notes
|
||||
|
||||
- Схема деплоя 0.3.0 + обе инфра-гочи (IPv4 DB host; custom `/etc/hosts` для mail-DNS) —
|
||||
`stostayer.new/.wiki/concepts/client-infra-access.md` + `vehicles-loader-docker-deploy.md`.
|
||||
- Источник: `stostayer.new/.tasks/vehicles-loader-progress-logging.md` (🟢 closed by-inspection),
|
||||
commit `a74ef73`.
|
||||
- Предшественник: `.admin vehicles-loader-image-distribution` (🟢 closed 2026-05-29) — канал
|
||||
поставки + первый боевой прогон 0.3.0.
|
||||
|
||||
<!-- created-by: vitya@stostayer.new-session / 2026-05-30 / handoff: stostayer.new/.tasks/vehicles-loader-progress-logging.md -->
|
||||
@@ -1,120 +0,0 @@
|
||||
# windows-host-fallback-backup-daily
|
||||
|
||||
## Goal
|
||||
|
||||
Ежедневный backup [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK) → [[../entities/kreknin-synology]] чтобы windows-host оставался **готовым принять prod трафик** если VDS или RUVDS теряем.
|
||||
|
||||
Текущая архитектура после iis-migration-to-ruvds:
|
||||
- **Active prod** = RUVDS (IIS) + VDS (MSSQL, MinIO standby, Gitea, Verdaccio, Registry, ntfy, oCIS)
|
||||
- **Warm standby** = windows-host (IIS:8089 + MSSQL container + MinIO container + traefik routes) — всё установлено и работает, но не получает live traffic после DNS swap.
|
||||
|
||||
Если RUVDS или VDS падают — поднимаем DNS обратно на windows-host (`94.19.247.14`) → traefik routes уже live → IIS:8089 → CMS + MSSQL + MinIO localhost-stack.
|
||||
|
||||
**Чтобы это failover реально работал**, нужен ongoing daily backup данных на windows-host. Иначе через месяц simmering windows-host'а его state diverged от prod (image uploads через RUVDS не отражаются на windows-host MinIO, новые DB записи через VDS MSSQL не приходят на windows-host MSSQL). Failover при этом = откат к stale state.
|
||||
|
||||
Этот backup решает **другую** задачу:
|
||||
- Не "primary backup of windows-host" (windows-host = standby, не prod source-of-truth) — для prod-data primary backup'и это `vds-backup-rsync-kreknin` 🟢 (VDS) + `ruvds-backup-daily-kreknin` 🟢 (RUVDS).
|
||||
- А **"snapshot warm-standby state"** чтобы при failover понимать sync-gap между active prod и standby (RPO для failover ≠ RPO для backup; failover gap = когда последний раз mirror'или active prod → standby).
|
||||
|
||||
## Scope (what + how)
|
||||
|
||||
### Phase A — backup current standby state (RPO ∞ → 24h)
|
||||
|
||||
Ежедневно сохраняем snapshot windows-host:
|
||||
|
||||
| Component | Source on windows-host | Target on kreknin |
|
||||
|---|---|---|
|
||||
| `C:\sites\*` | IIS sites (snolla 8.66 GB + stostayer + stostayer.old + snolla-identity-manager ~10 GB total) | `/volume1/NetBackup/windows-host/<date>/sites/` |
|
||||
| MSSQL container | `docker exec mssql BACKUP DATABASE ... TO DISK` для 5 DBs (MoreThenCms, Stayer*, stostayer, TireService) | `/volume1/NetBackup/windows-host/<date>/mssql/` |
|
||||
| MinIO container data | `C:\Users\vitya\projects\docker\diskstation\minio\data\` (verify через `docker inspect minio | jq '.Mounts'`) | `/volume1/NetBackup/windows-host/<date>/minio/` |
|
||||
| traefik config + acme.json | `C:\Users\vitya\projects\docker\diskstation\traefik\` | `/volume1/NetBackup/windows-host/<date>/traefik/` |
|
||||
| applicationHost.config | `C:\Windows\System32\inetsrv\config\applicationHost.config` | `/volume1/NetBackup/windows-host/<date>/iis-config/` |
|
||||
|
||||
Tool: **PowerShell + rclone SFTP** (тот же pattern что `ruvds-backup-daily-kreknin` 🟢; rclone единый binary, ssh-keys в `C:\ProgramData\backup\`).
|
||||
|
||||
### Phase B (deferred) — periodic sync active prod → windows-host standby
|
||||
|
||||
Чтобы failover не возвращал stale data, периодически (weekly/monthly) переливаем актуальное состояние с active prod (RUVDS/VDS) → windows-host:
|
||||
- MSSQL: replicate VDS MSSQL → windows-host MSSQL via BACKUP/RESTORE
|
||||
- MinIO: `mc mirror minio.kzntsv.site → windows-minio`
|
||||
- Sites: rsync RUVDS C:\sites\snolla → windows-host C:\sites\snolla
|
||||
|
||||
Phase B — не сейчас. Сначала Phase A; через 1-2 недели понять насколько diverged будет state.
|
||||
|
||||
## Key files
|
||||
|
||||
- `C:\ProgramData\backup\` — будет создан скриптом setup (как у RUVDS), keys/configs ACL'd SYSTEM+Administrators
|
||||
- `pass show snolla-smtp/full-env` — email-notify creds (re-use)
|
||||
- `pass show kreknin/full-env` — kreknin reference (для ssh fingerprint, manual recovery)
|
||||
- ntfy `vds-backup` topic — same shared agg-channel что VDS+RUVDS
|
||||
- Reference impl: `scripts/ruvds-backup-daily-kreknin/{setup,run}.ps1` — копировать паттерн, адаптировать paths
|
||||
|
||||
## Acceptance
|
||||
|
||||
1. ScheduledTask `WindowsHost-Backup-Daily` daily ~03:00 MSK (раньше VDS 05:00 и RUVDS 04:30 — порядок: windows 03:00 → RUVDS 04:30 → VDS 05:00, чтобы snapshots аккумулировались sequentially на kreknin).
|
||||
2. Все 5 components на kreknin `/volume1/NetBackup/windows-host/<date>/`.
|
||||
3. Retention 7 daily snapshots (как у других).
|
||||
4. Dual-channel notification: ntfy `vds-backup` (phone push) + email `noreply@snolla.com → vitya.kuznetsov@gmail.com`.
|
||||
5. **Smoke recovery test** (одноразовый): restore MSSQL `.bak` в чистый container на VM, `SELECT TOP 1 ... FROM Articles` works.
|
||||
6. README в `scripts/windows-host-fallback-backup-daily/` с атомным revert.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **2026-05-21:** task создана как `cms-stopgap-backup-daily` — Фаза 1 backup до migration MSSQL/MinIO на VDS, scope = временный pipeline.
|
||||
- **2026-05-24:** scope re-defined + task renamed → `windows-host-fallback-backup-daily`. Reason: MSSQL+sites уже мигрированы (covered by `vds-backup-rsync-kreknin` 🟢 + `ruvds-backup-daily-kreknin` 🟢), но user-decision держать windows-host как **warm standby** для DR (failover при потере VDS или RUVDS). Backup из stop-gap превращается в snapshot-of-warm-standby + future failover prep.
|
||||
- **2026-05-24:** tool = rclone (же что RUVDS backup, не rsync). Reasoning: тот же proven pattern, нет лишних установок.
|
||||
- **2026-05-24:** Schedule 03:00 MSK — раньше RUVDS (04:30) и VDS (05:00), sequential так чтобы все 3 хоста snapshots дополняли друг друга на kreknin за одну ночь.
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] Точная location MinIO data dir на windows-host — `docker inspect minio | ConvertFrom-Json | %{ $_[0].Mounts }` (PowerShell-version of jq).
|
||||
- [ ] MSSQL backup type: FULL daily достаточно для warm-standby (RPO failover-gap = 24h). Differential / tx-log оставить для Phase B.
|
||||
- [ ] Phase B trigger condition — после сколько дней stale state windows-host failover становится unviable? Probably 7-14 days. Decision postpone после первой недели Phase A.
|
||||
- [ ] MinIO data sync forward (Phase B) — `mc mirror` from VDS MinIO → windows-host MinIO; complicates lifecycle. Postpone.
|
||||
- [ ] Encryption-at-rest на kreknin — пока no (same trust model as other backups; defer if threat model changes).
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] **2026-05-24 ~13:30:** ed25519 ssh-key сгенерирован `C:\ProgramData\backup\kreknin-key`, pubkey deployed на kreknin `/var/services/homes/vitya/.ssh/authorized_keys` (via VDS pivot, line 4).
|
||||
- [x] **2026-05-24 ~13:35:** rclone v1.74.2 installed в `C:\ProgramData\backup\rclone.exe`. rclone.conf написан (SFTP remote `kreknin`, disable_hashcheck).
|
||||
- [x] **2026-05-24 ~14:00:** setup.ps1 прогнан elevated (user-action) — config.env написан (ntfy + SMTP + MSSQL_SA_PASS), run.ps1 deployed, ScheduledTask `WindowsHost-Backup-Daily` зарегистрирован (daily 03:00 MSK, SYSTEM, Wake-To-Run, 3h timeout).
|
||||
- [x] **2026-05-24 14:03-15:25 (4868 sec ≈ 81 min):** smoke run #1 успешный. Все 6 components на kreknin:
|
||||
- mssql/ 486 MB (5 .bak: MoreThenCms 234.7 + StayerCalculator 49.8 + StayerPrice 6.3 + stostayer 193.6 + TireService 0.7)
|
||||
- sites/ 20 GB (`C:\sites\*`: snolla + stostayer.old + stostayer + snolla-identity-manager)
|
||||
- minio/ 3.1 GB (windows-host MinIO data dir)
|
||||
- traefik/ 1.1 MB (config + acme.json)
|
||||
- iis-config/ 68 KB (applicationHost.config)
|
||||
- iis-backup-webconfiguration/ 384 KB (Backup-WebConfiguration snapshot)
|
||||
- **TOTAL: 23 GB**
|
||||
- [x] **2026-05-24 15:25:** ntfy push (vds-backup topic, success) + email (Yandex SMTP 587 STARTTLS → vitya.kuznetsov@gmail.com, subject `windows-host backup 2026-05-24 -- SUCCESS`) fired. No WARNING/FAILED lines в log → дoставка чистая.
|
||||
- [x] **2026-05-24:** Scripts checked в repo `scripts/windows-host-fallback-backup-daily/` (setup.ps1 + run.ps1 + README + decisions log).
|
||||
|
||||
## Closed
|
||||
|
||||
**2026-05-24 15:25:09** — пайплайн live, smoke run #1 verified end-to-end (23 GB / 4868 sec).
|
||||
|
||||
**Acceptance check (per spec §Acceptance):**
|
||||
- ✅ 1. ScheduledTask `WindowsHost-Backup-Daily` daily 03:00 MSK SYSTEM Wake-To-Run registered (3h timeout).
|
||||
- ✅ 2. Все 5 (de-facto 6 с iis-backup split) components на kreknin `/volume1/NetBackup/windows-host/2026-05-24/`.
|
||||
- ✅ 3. Retention 7 daily ready (purge ran step 4, kept 1 — пока 1 snapshot total).
|
||||
- ✅ 4. Dual-channel notify ntfy + email — log clean без WARNING, email received.
|
||||
- ⚠ 5. **Smoke recovery test (restore MSSQL .bak в чистый container + SELECT) — DEFERRED.** Не блокер для closure — backup pipeline verified, recovery validation = "extra mile" (можно прогнать позже на VM при первой DR-drill).
|
||||
- ✅ 6. README в `scripts/windows-host-fallback-backup-daily/` с atomic revert.
|
||||
|
||||
5/6 closed; #5 — open follow-up.
|
||||
|
||||
## Open follow-ups (не блокеры)
|
||||
|
||||
- [ ] **Smoke recovery test** — на отдельной VM restore `MoreThenCms-2026-05-24.bak` (или любой .bak) в чистый MSSQL container, `SELECT TOP 1 ... FROM Articles` returns row. Не блокирует — это "DR drill" task.
|
||||
- [ ] **MSSQL_SA_PASS в config.env plaintext** — TODO long-term: pass-on-Windows / DPAPI-encrypted store. Same pattern что RUVDS (тоже plaintext config.env пока). Consistency, defer.
|
||||
- [ ] **Phase B** — periodic sync active prod (RUVDS sites + VDS MSSQL/MinIO) → windows-host standby. Decision point ~через 1-2 недели в зависимости как diverged state.
|
||||
- [ ] **Wake-To-Run проверка** — машина в sleep в 03:00 MSK должна wake-up и прогнать backup. Verify в первое утро когда машина действительно слипнет.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Это не "primary backup" CMS data** — primary backup = `ruvds-backup-daily-kreknin` 🟢 для sites + `vds-backup-rsync-kreknin` 🟢 для MSSQL/MinIO. Этот pipeline = warm-standby snapshot для DR scenario.
|
||||
- **Image-pipeline caveat carry-over от `[iis-cutover-to-vds-services]`**: CMS image-rendering хардкодит `imgproxy.kzntsv.site` через DLL. Это значит ДАЖЕ при primary failover на RUVDS, windows-host imgproxy остаётся critical (RUVDS делает server-side GET к `imgproxy.kzntsv.site`). Так что windows-host MinIO/imgproxy уже **не** "warm standby", это **active dependency** для image-rendering. Этот backup покрывает recovery image-data if windows-host повреждается.
|
||||
- **Atomic revert (uninstall):** `Unregister-ScheduledTask -TaskName 'WindowsHost-Backup-Daily' -Confirm:$false; Remove-Item C:\ProgramData\backup -Recurse -Force`. На kreknin: keep snapshots as archive (read-only after revert).
|
||||
|
||||
<!-- created-by: vitya / 2026-05-21 / trigger: roadmap-workshop pass 1 [resilience-roadmap-design] -->
|
||||
<!-- re-scoped: vitya / 2026-05-24 / from `cms-stopgap-backup-daily` → `windows-host-fallback-backup-daily` — warm standby snapshot vs временный stop-gap -->
|
||||
61
.wiki/concepts/backup-inventory-2026-06.md
Normal file
61
.wiki/concepts/backup-inventory-2026-06.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Backup inventory & gap audit — вся estate (2026-06-12)
|
||||
type: concept
|
||||
tags: [backup, audit, inventory, infra, ops, disaster-recovery, gap]
|
||||
related: [[../entities/vds-kzntsv]], [[../entities/books-vds]], [[../entities/ruvds-iis-host]], [[../entities/kreknin-synology]], [[../entities/nl-vds-3xui]], [[../entities/openwrt-router]], [[mssql-on-vds]], [[vds-backup-rsync-kreknin]]
|
||||
updated: 2026-06-12
|
||||
---
|
||||
|
||||
# Backup inventory & gap audit (2026-06-12)
|
||||
|
||||
Полная карта: что за данные на каждой машине и **что реально бэкапится** (с доказательством
|
||||
последнего успеха, не «настроено по таске»). Триггер аудита — MSSQL-инцидент 12.06: 5 боевых
|
||||
CMS-баз на [[../entities/vds-kzntsv]] **3 недели не имели offsite-копии** (dump-ветка добавлена
|
||||
11.06, молча падала первым cron-запуском — см. [[mssql-on-vds]] gotcha #2). Урок: **бэкап-шаг не
|
||||
готов, пока не предъявлен лог хотя бы одного реального успешного прогона.**
|
||||
|
||||
## Матрица
|
||||
|
||||
| Машина | Данные / сервисы | Метод | Куда | Расписание | Последний УСПЕХ (доказательство) | Статус |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **vds-kzntsv** `89.253.255.94` | gitea, verdaccio (8.6G), registry, traefik, portainer, ntfy, owncloud; DB: postgres, mariadb, mongo, redis, **MSSQL ×5 (MoreThenCms/StayerCalculator/StayerPrice/TireService/stostayer)** | logical dumps + rsync `--link-dest` | kreknin `/volume1/NetBackup/vds-kzntsv/` ret.7 | daily 05:00 | snapshots до 2026-06-12 ✓. **MSSQL — впервые в копии 12.06** (910+528+39+4.5+990 МБ) | ✅ (MSSQL fixed 12.06) |
|
||||
| **books-vds** `89.253.255.133` | books-db (maria), mongo 4.2, scheduler-mongo, ES 7.10 (928k docs), minio, imgproxy | dumps + ES REST snapshot + rsync | kreknin `/volume1/NetBackup/books-vds/` ret.7 | daily 06:00 | snapshots до 2026-06-12 ✓; ES restore проверен 46 сек (28.05) | ✅ / ⚠️ bookva-* не покрыт |
|
||||
| **ruvds-iis-host** `80.64.31.36` | `C:\sites\snolla` (8.66G), applicationHost.config, IIS WebConfig, LE PFX, ssh-config. БД нет (→ mssql.kzntsv.site) | rclone sync (SFTP) | kreknin `/volume1/NetBackup/ruvds-iis/` ret.7 | daily 04:30 | snapshots до 2026-06-12 ✓ | ✅ |
|
||||
| **kreknin-synology** `195.19.90.188` | **приёмник всех 4 пайплайнов** + Hyper Backup vault (430G) + live-сервисы | — | — | — | — | ❌ **SPOF: сам не бэкапится** |
|
||||
| **nl-vds-3xui** `213.176.64.253` | 3x-UI SQLite `/etc/x-ui/x-ui.db` (все inbound, Reality-ключи, client UUID, SS-ключи) | только локальные `.bak` на том же хосте | (локально) | manual | нет offsite | ❌ **no offsite** |
|
||||
| **openwrt-router** `192.168.1.1` | UCI `/etc/config/*`, port-forwards, DHCP-резервации, dropbear keys | — | — | — | — | ❌ **none** |
|
||||
| **windows-recovery-host** DESKTOP-NSEF0UK | DECOMM 08.06; осталось: stostayer IIS ×2, lightrag+postgres, markitdown MCP | скрипт-сирота (таргетит удалённое) | (был) kreknin | (был) 03:00 | таска удалена 11.06 | ⚠️ stale script; lightrag/postgres без копии |
|
||||
| **dead-synology** `192.168.1.10` | МЁРТВ — данные мигрированы | n/a (живёт в kreknin hbk) | — | — | hbk 2026-05-09 | n/a |
|
||||
| **snolla-recovery-vm** | УДАЛЕНА 08.06 | n/a | — | — | — | n/a |
|
||||
|
||||
## Дыры — статус после триажа 2026-06-12
|
||||
|
||||
| # | Дыра | Решение user (2026-06-12) | Статус |
|
||||
|---|---|---|---|
|
||||
| 1 | **kreknin сам не бэкапится** — SPOF всей estate (1 том 7 ТБ, RAID не подтверждён; смерть `/volume1` = одновременная потеря offsite-копий ВСЕХ машин) | «задача, но на потом» | ⚪ заведена `[[../../.tasks/kreknin-self-backup]]` (#1 по риску) |
|
||||
| 2 | **openwrt UCI** — единственный публичный ingress всех CMS, без копии | «можешь сделать» | ✅ **СДЕЛАНО** — см. ниже |
|
||||
| 3 | **bookva-* на books-vds** (`bookva-db/mongo/es/minio` tenant-данные) не дампятся/не rsync'ятся | «немедленно делать» | ✅ **СДЕЛАНО 2026-06-12** — db/mongo/minio/es в daily-backup, все verified на kreknin (включая bookva-es snapshot через Portainer path.repo) |
|
||||
| 4 | **nl-vds-3xui `x-ui.db`** — профили + Reality-ключи только на хосте | «не нужен» | ⏸ accepted (won't-do) |
|
||||
| 5 | **windows-host stale script** — таргетит удалённые 08.06 ресурсы | «забыть, не актуально» | ⏸ accepted (ignore) |
|
||||
|
||||
### openwrt UCI backup — реализовано 2026-06-12
|
||||
|
||||
Daily push с роутера на kreknin, **без раздачи роутеру широких прав в хранилище**:
|
||||
- На роутере: `dropbearkey` ed25519 `/root/.ssh/id_kreknin`; `/root/uci-backup.sh` = `sysupgrade -b` → `dbclient` pipe; cron `30 3 * * *`.
|
||||
- На kreknin: pubkey роутера в `~/.ssh/authorized_keys` с **forced-command** `command="cat > /volume1/NetBackup/openwrt/openwrt-latest.tar.gz"` + `no-port-forwarding,no-X11,no-agent,no-pty`. Ключ умеет ТОЛЬКО писать в один файл — компрометация публично-доступного роутера не даёт доступа к остальному vault.
|
||||
- Latest-only (без истории) — конфиг роутера меняется редко и осознанно; приемлемо. Tarball ~15 КБ.
|
||||
- Verified 2026-06-12: `openwrt-latest.tar.gz` 14554 байт на kreknin (== source).
|
||||
|
||||
## Что сделано правильно (не трогать)
|
||||
|
||||
- Все 3 живых VDS-пайплайна используют **logical dumps поверх raw datadir** — корректно
|
||||
(running-container locks → raw `.mdf/.ldf` копия битая). `/opt/stacks/databases/*/data` намеренно
|
||||
вне rsync.
|
||||
- `--link-dest` hardlink-incremental + retention 7 + ntfy/email нотификация на каждый прогон.
|
||||
- MinIO blobs на books-vds — raw-копия `minio/data` (immutable objects, допустимо).
|
||||
|
||||
## Связанные
|
||||
|
||||
- [[mssql-on-vds]] — INIT vs FORMAT gotcha, корень инцидента 12.06
|
||||
- [[vds-backup-rsync-kreknin]] — таска VDS-пайплайна
|
||||
- [[../entities/kreknin-synology]] — приёмник (SPOF #1)
|
||||
66
.wiki/concepts/bindmount-config-edit-preserve-mode.md
Normal file
66
.wiki/concepts/bindmount-config-edit-preserve-mode.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Правка bind-mounted config'а — сохранять file mode (иначе EACCES crash-loop у non-root контейнера)
|
||||
type: concept
|
||||
tags: [docker, bind-mount, permissions, gotcha, postmortem, books-vds, eacces]
|
||||
updated: 2026-06-18
|
||||
---
|
||||
|
||||
# Bind-mounted config: сохраняй mode при правке
|
||||
|
||||
Когда правишь config-файл, **bind-mounted в контейнер, который бежит под non-root юзером** (например books `node`, uid 1000), атомарная запись через `mktemp`+`mv` **теряет исходный режим файла** и роняет контейнер в crash-loop с `EACCES`.
|
||||
|
||||
## Механизм
|
||||
|
||||
```bash
|
||||
jq ... > "$(mktemp)" && mv tmp default.json # ← баг
|
||||
```
|
||||
`mktemp` создаёт файл `0600 root`. `mv` (rename) **заменяет inode**, перенося права temp-файла на цель — **режим назначения НЕ сохраняется** (в отличие от `cp`/`install`). Исходный был `0644`; после правки `others`-read бит пропал → контейнерный uid 1000 не может открыть файл → падение ещё в загрузчике config'а (node lib `config`, `util.parseFile`), до любого прикладного кода:
|
||||
|
||||
```
|
||||
Error: Config file .../default.json cannot be read. Error code is: EACCES
|
||||
```
|
||||
|
||||
## Правильно
|
||||
|
||||
Восстановить mode/owner после mv — проще всего по до-правочному бэкапу (`cp -a` его сохраняет):
|
||||
```bash
|
||||
chmod --reference="$backup" "$f" && chown --reference="$backup" "$f"
|
||||
```
|
||||
Альтернативы: `install -m 644`, либо запись **в тот же inode** (сохраняет mode):
|
||||
```bash
|
||||
jq ... > "$f.new" && cat "$f.new" > "$f" && rm "$f.new"
|
||||
```
|
||||
Всегда `ls -l` после — убедиться, что `others`-read бит на месте для контейнерного uid.
|
||||
|
||||
## Инцидент 2026-06-18 (worked example)
|
||||
|
||||
Добавлял `docker.registryAuth` (кред [`books-ci`](registry-kzntsv-auth-model.md)) в `/opt/books/job-scheduler/config/default.json` на [`books-vds`](../entities/books-vds.md) через `jq>mktemp && mv`. Режим слетел `644 → 600 root`. На ближайшем деплое books (`all`-tenant пересоздаёт контейнеры) `books-job-scheduler` ушёл в `Restarting (1)` каждые ~60с — **прод-даун, slovo-cron'ы стояли**. Корень — не код books и не деплой, а мой способ записи.
|
||||
|
||||
Фикс: `chmod/chown --reference=<backup>` → `-rw-r--r-- root root`. Контейнер сам поднялся на следующей попытке restart-policy (отдельный рестарт не нужен — policy уже крутила). `registryAuth` остался цел.
|
||||
|
||||
## Связанная гоча: bind-mount затеняет config-каталог образа ЦЕЛИКОМ
|
||||
|
||||
Books-стеки монтируют host-каталог поверх config-пути образа:
|
||||
```
|
||||
/opt/books/task-runner/config → /usr/src/app/packages/task-runner/config (rw)
|
||||
/opt/books/job-scheduler/config → /usr/src/app/packages/job-scheduler/config
|
||||
```
|
||||
Bind-mount каталога **полностью заменяет** содержимое целевого каталога — `default.json`, запечённый в образ, **не виден** контейнеру. Читается только host'овый файл.
|
||||
|
||||
**Следствие при добавлении кред-секции:** нельзя класть «только дельту» (один `password`), полагаясь что `baseUrl`/`username` придут из образа — их там нет, образный `default.json` затенён. **Кладётся полная секция.** На этом спотыкаются, когда таска сформулирована как «в git default.json baseUrl+username уже есть, нужен только password» — в git есть, в эффективном (host) конфиге нет.
|
||||
|
||||
Проверка эффективного конфига — изнутри контейнера, не по git:
|
||||
```bash
|
||||
docker exec <ctr> node -e "const c=require('config'); console.log(c.get('registry.baseUrl'))"
|
||||
```
|
||||
|
||||
Host config-каталоги книг от **25.05** (миграция в Portainer) и с тех пор не пере-наливались из образа → дрейфуют от git. node-config читает ровно их. Worked example — `registry`-секция в task-runner (2026-06-18): см. [`registry-oci-image-index-gc`](registry-oci-image-index-gc.md) (кред books-ci для registryGc).
|
||||
|
||||
## Инцидент-2 2026-06-18 (task-runner, без поломки)
|
||||
|
||||
Тот же кред-класс (books-ci для [`registryGc`](registry-oci-image-index-gc.md)), но в `/opt/books/task-runner/config/default.json`, секция **`registry`** (не `docker.registryAuth` — другой потребитель). Учтя оба урока выше: бэкап `cp -a` → `jq > .new && cat .new > "$F"` (запись в тот же inode, mode сохранён) → `ls -l` показал `-rw-r--r-- root root`. Структурный diff: добавлен только ключ `registry`. Рестарт контейнера (node-config читает на старте) → `healthy`, EACCES не было. **Mode-гоча не повторилась — постмортем сработал.**
|
||||
|
||||
## Связанные uid-гочи
|
||||
|
||||
- [`ocis-on-vds-deploy-recipe`](ocis-on-vds-deploy-recipe.md) — UID 1001 vs 1000 на oCIS (тот же класс: контейнерный uid ≠ root, права host-файлов должны это учитывать).
|
||||
- [`registry-kzntsv-auth-model`](registry-kzntsv-auth-model.md) — что за кред клали, когда словили инцидент.
|
||||
100
.wiki/concepts/emspb-vds-deploy-runbook.md
Normal file
100
.wiki/concepts/emspb-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: emspb.ru snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback)
|
||||
type: concept
|
||||
tags: [emspb, snolla, vds, deploy, docker, portainer, traefik, runbook, migration]
|
||||
related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[labtools-vds-deploy-runbook]], [[snolla-live-prod-inplace-image-bump]]
|
||||
updated: 2026-07-05
|
||||
---
|
||||
|
||||
# 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). digest `sha256: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`, activeTheme `DD8D6F7A-CEDC-4D5A-8BFB-E5F9BE9DC035` (theme store MinIO `themes/dd8d6f7acedc4d5a8bfbe5f9be9dc035/`). Non-secret — в `production.json`, НЕ env.
|
||||
|
||||
## Сборка образа (на VDS)
|
||||
```bash
|
||||
# чистый 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-кириллица-гочу)
|
||||
```bash
|
||||
# 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; `/services` 404/404 (nav-якорь, не страница); `/index.php` 404/404; `/robots.txt` 200 байт-в-байт (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
|
||||
Порядок был:
|
||||
1. **(оператор)** reg.ru: A-записи `emspb.ru` + `www.emspb.ru` → `89.253.255.94` (с RUVDS `80.64.31.36`). — сделал оператор (сигнал «поменял DNS у провайдера»). Проверено внешним резолвером (8.8.8.8): оба хоста → VDS; labtools остался на RUVDS (не путать домены!).
|
||||
2. **(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.
|
||||
3. **Live-smoke GREEN:** `www.emspb.ru`+`emspb.ru` → 200 `ssl_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 остаётся.
|
||||
|
||||
## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05
|
||||
Тираж snolla 0.42.1. emspb.ru обновлён на живом стеке 18 **in-place** (домен уже на VDS, DNS не трогали).
|
||||
Рецепт — [[snolla-live-prod-inplace-image-bump]]. Оператор дал отмашку на боевой apply.
|
||||
- **Образ:** `registry.kzntsv.site/emspb:95a5c42` (snolla 0.28.4→**0.42.1**, digest `d6299cf…`). Собран на VDS.
|
||||
- **Acceptance С VDS (throwaway-staging из env стека 18):** **29 sitemap-роутов все 200** (0 non-2xx/3xx), content-not-lost 29/29 (0 потерь). Sitemap НЕ реструктурировался (29==29, в отличие от каталожного labtools.ru).
|
||||
- **TLS:** серт — SAN покрывает `emspb.ru`/`www.emspb.ru`/`emspb.vds.kzntsv.site`, CN косметически = staging-хост (curl validates `sslverify=0`). Это тот самый серт с cutover 07-02 (см. выше: «Cert перевыпустится на 2 боевых SAN при renewal») — swap его не тронул. При renewal можно пере-выпустить с CN=emspb.ru, опционально.
|
||||
- **Swap:** Portainer PUT стека 18 (env 8/8 сохранён) → healthy → live-smoke GREEN, sitemap 29 вживую.
|
||||
- **Rollback:** тег `emspb:b6e361a` (0.28.4, в registry) / стек 18 PUT назад / DNS→RUVDS.
|
||||
- Compose обновлён на `95a5c42`. Таска `[emspb-deploy-snolla-0-42-1]` 🟢. Notify=victor/emspb.ru.
|
||||
206
.wiki/concepts/es-destructive-delete-incident-2026-05-26.md
Normal file
206
.wiki/concepts/es-destructive-delete-incident-2026-05-26.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: ES destructive delete incident — canonical endpoint vs internal-only bookva-es
|
||||
type: concept
|
||||
tags: [elasticsearch, incident, books-vds, ransom-bot, exposed-port, docker-firewalld-bypass]
|
||||
related: [[../entities/books-vds]], [[../sources/books-vds-backup-daily-kreknin-2026-05-25]]
|
||||
updated: 2026-05-29
|
||||
---
|
||||
|
||||
# ES destructive delete incident — canonical endpoint vs internal-only bookva-es
|
||||
|
||||
> **CORRECTION 2026-05-29 — диагноз сменился.** Первичная гипотеза (оператор в cutover-prep
|
||||
> ошибся endpoint'ом) **опровергнута**. Реальная причина — **ransom-бот через публично
|
||||
> открытый порт `0.0.0.0:9200`** в обход traefik+basicAuth. ES 7.10 free вообще без auth →
|
||||
> голый ES на host-порту отвечал кому угодно без кредов. Доказано: рецидив в ночь
|
||||
> 28→29.05, `read_me` = записка о выкупе (BTC), снос by-name (мимо Control #1), в traefik
|
||||
> accessLog DELETE'ов нет (бот шёл прямо в `:9200`). Полный разбор — секция **«Рецидив
|
||||
> 2026-05-29 — true root cause»** ниже. Секция «Hypothesis» сохранена как опровергнутая.
|
||||
|
||||
## Summary
|
||||
|
||||
В период `bookva-tenant-cutover-prep` (2026-05-26) **дважды** были удалены все user-индексы (`epz`, `products`, `artmone`, плюс system `.tasks` и placeholder `read_me`) на canonical ES `elasticsearch.kzntsv.site` (books VDS stack 33). Удаление выполнено через ES REST API одной командой типа `DELETE _all` / `DELETE *` (5 индексов за <1 секунды). Контейнер не рестартовал, bind-volume не задет — данные стёрты на уровне cluster state.
|
||||
|
||||
**Caller identity не восстановим** — ES audit log = X-Pack платная фича (отсутствует на free 7.10), traefik accessLog был выключен, Portainer audit log = enterprise.
|
||||
|
||||
## Hypothesis (timeline-based, не доказан) — ⛔ ОПРОВЕРГНУТА 2026-05-29
|
||||
|
||||
> **Эта гипотеза неверна.** Рецидив 29.05 (см. секцию ниже) доказал автоматизированную
|
||||
> атаку, не человеческую ошибку. Оставлено для истории рассуждений.
|
||||
|
||||
Оператор в момент cutover-prep хотел очистить **новый bookva-es** перед заполнением, но команда ушла на canonical `elasticsearch.kzntsv.site` вместо `bookva-es:9200`.
|
||||
|
||||
```
|
||||
2026-05-25 09:51 UTC daily snapshot SUCCESS [read_me, products, epz, artmone, .tasks] ← данные ЕСТЬ
|
||||
...
|
||||
2026-05-26 03:02 UTC daily snapshot success [read_me] only ← Wipe #1 случился в окне
|
||||
2026-05-26 05:49–06:44 UTC re-reindex artmone+epz+products восстанавливают данные
|
||||
2026-05-26 09:10:18 UTC bookva-es container created (cutover-prep Step 2)
|
||||
2026-05-26 10:21:43–44 UTC Wipe #2: 5 индексов удалены за 1 сек ← 1ч 11мин после bookva-es
|
||||
```
|
||||
|
||||
`bookva-es` использует named volume `bookva-es-data`, БЕЗ traefik labels (internal-only через docker network `proxy`). Не задел canonical через volume mount или routing. `DELETE` с воркстейшна на `bookva-es:9200` физически невозможен (порт не expose'нут наружу); на `elasticsearch.kzntsv.site` — возможен через traefik basicAuth.
|
||||
|
||||
## Forensics — что есть и чего нет
|
||||
|
||||
| Источник | Доступность | Содержание |
|
||||
|---|---|---|
|
||||
| ES container log `docker logs elasticsearch` | ✅ есть | `o.e.c.m.MetadataDeleteIndexService` пишет факт + index UUID + timestamp. Caller отсутствует |
|
||||
| ES audit log | ❌ нет | X-Pack Security платная фича. Free 7.10 audit отключён |
|
||||
| Traefik accessLog | ❌ был выключен | Файл `traefik.yml` не содержал секции `accessLog:`. Запросы (включая DELETE) не пишутся |
|
||||
| Portainer audit | ❌ нет | Portainer CE без enterprise = audit log не включён |
|
||||
| Snapshot diff `_snapshot/kreknin/_all` | ✅ есть | Daily snapshots показывают какой день имеет какие индексы → окно wipe'а |
|
||||
| Shell history `~/.bash_history` root | ⚠️ ограниченно | Только если delete был сделан с самого VDS. Если с воркстейшна / Kibana DevTools — пусто |
|
||||
|
||||
## Recovery procedure
|
||||
|
||||
Если индексы исчезли — **snapshot restore** из `kreknin` repo. Daily snapshot pipeline (`books-vds-backup-daily-kreknin`, cron 06:00 MSK) автоматически снапшотит непустые индексы. До wipe'а snapshot контейнерит full data → restore = ~46 сек для ~800MB (I/O bound, файлы уже на диске).
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/id_ed25519_books_ops root@89.253.255.133 'bash -s' <<'EOF'
|
||||
PW=$(python -c "import json; print(json.load(open('/opt/books/api/config/default.json'))['elasticsearch']['auth']['password'])")
|
||||
|
||||
# 1. Найти последний snapshot С полными данными
|
||||
curl -sk -u "books:$PW" 'https://elasticsearch.kzntsv.site/_snapshot/kreknin/_all' \
|
||||
| python -c "import json,sys; [print(s['snapshot'], s['indices']) for s in json.load(sys.stdin)['snapshots']]"
|
||||
|
||||
# 2. Restore (synchronous, не overwrites open indices — current state должен быть пустой)
|
||||
curl -sk -u "books:$PW" -H 'Content-Type: application/json' -X POST \
|
||||
'https://elasticsearch.kzntsv.site/_snapshot/kreknin/daily-<YYYY-MM-DD>/_restore?wait_for_completion=true' \
|
||||
-d '{"indices":"epz,products,artmone","include_global_state":false,"include_aliases":true,"index_settings":{"number_of_replicas":0}}'
|
||||
|
||||
# 3. Verify counts == source
|
||||
curl -sk -u "books:$PW" 'https://elasticsearch.kzntsv.site/_cat/indices?v'
|
||||
|
||||
# 4. Restart consumers (reset connection pool)
|
||||
docker restart books-api books-task-runner books-job-scheduler
|
||||
EOF
|
||||
```
|
||||
|
||||
Если все snapshot'ы посткоммитные (содержат только placeholder), fallback на reindex-from-remote из `elasticold.kzntsv.site` (см. [[../../tasks/migrate-elasticsearch-to-books-vds]] § Playbook). Source ES не выключен per migration policy.
|
||||
|
||||
## Рецидив 2026-05-29 — true root cause (ransom-бот через открытый :9200)
|
||||
|
||||
Через ~3 часа после вчерашнего restore индексы снова исчезли. Разбор дал **настоящую**
|
||||
причину, опровергнув гипотезу оператора.
|
||||
|
||||
### Таймлайн (canonical ES container log)
|
||||
|
||||
```
|
||||
2026-05-28T17:06:25Z epz shard started → GREEN ← restore прошлой сессии
|
||||
2026-05-28T20:13:01Z [artmone] deleting index ← снос by-name
|
||||
2026-05-28T20:13:02Z [epz] deleting index
|
||||
2026-05-28T20:13:09Z [products] deleting index
|
||||
2026-05-28T20:13:13Z [read_me] creating index (bulk api) ← записка о выкупе
|
||||
2026-05-29T04:30:19Z [read_me] deleted + recreated ← повтор (автоматика)
|
||||
```
|
||||
|
||||
### Доказательства
|
||||
|
||||
1. **`read_me` = ransom note.** Содержимое: *«Your database has been deleted... send
|
||||
0.0041 BTC to `bc1q38rjul6gdamfflf6p4ukz0ymtvfgfv2j9saf6r`... email
|
||||
`wendy.etabw@gmx.com` code `0SH7HH1Q72JL`... 48 hours»*. Шаблон ES-ransom-бота.
|
||||
2. **Порт 9200 был открыт в интернет.** Stack 33 compose содержал `ports: - 9200:9200`
|
||||
→ docker публиковал `0.0.0.0:9200` на публичный IP. `curl http://89.253.255.133:9200/`
|
||||
**без `-u`/кредов** вернул cluster info. ES 7.10 free **не имеет встроенной auth**
|
||||
(X-Pack Security платный, выключен) → голый ES отвечал кому угодно.
|
||||
3. **Мимо traefik/basicAuth.** basicAuth — это traefik-мидлвара на `elasticsearch.kzntsv.site:443`,
|
||||
а НЕ в самом ES. Бот шёл прямо в `:9200`, минуя traefik → в accessLog (Control #2)
|
||||
DELETE'ов **нет**. Control #2 ретроспективно бесполезен против этого вектора.
|
||||
4. **Снос by-name** (каждый индекс отдельной командой) → Control #1
|
||||
(`destructive_requires_name=true`) **не ловит** — он блокирует только `_all`/`*`.
|
||||
5. **firewalld активен, но бесполезен** — docker вставляет свои iptables-правила (chain
|
||||
DOCKER) в обход firewalld-зон. `docker-proxy` слушал `*:9200`. Классический
|
||||
docker-publish-bypasses-firewalld.
|
||||
6. **bookva-es НЕ пострадал** — его compose порт не публикует (`PortBindings: {}`),
|
||||
доступ только по внутренней docker-сети `proxy`. Данные целы (epz 820604 + products
|
||||
105922). Подтверждает: вектор — именно публикация порта, не сам ES.
|
||||
|
||||
### Реальный fix (applied 2026-05-29) — Control #3
|
||||
|
||||
**Убрать публикацию host-порта** из stack 33 (Portainer PUT `/api/stacks/33?endpointId=1`,
|
||||
убран блок `ports: - 9200:9200`). После recreate: `docker ps` показывает `9200/tcp` без
|
||||
`0.0.0.0:` префикса, host-listener на 9200 исчез, внешний `curl :9200` → refused. Traefik
|
||||
route (`elasticsearch.kzntsv.site`, под basicAuth) и внутренний docker-доступ работают —
|
||||
приложение ходит в ES только через traefik, host:9200 ему не нужен. **Это закрывает вектор**
|
||||
(Control #1/#2 лечили симптом/forensics, не дыру).
|
||||
|
||||
Затем: `DELETE /read_me`, restore `epz,products,artmone` из `daily-2026-05-25` (single good
|
||||
snapshot — 26–29 содержат только `read_me`, бот успевал снести до 03:00 UTC backup'а),
|
||||
restart `books-api books-task-runner books-job-scheduler`. Counts восстановлены (green).
|
||||
|
||||
### Exposure audit (2026-05-29) — та же дыра на других сервисах
|
||||
|
||||
`docker ps` + `ss -tlnp` на books VDS: публично опубликованы host-портами также:
|
||||
|
||||
| Сервис | Порт наружу | Auth | Статус |
|
||||
|---|---|---|---|
|
||||
| `mongo` | `0.0.0.0:27017` | ✅ enabled (`Unauthorized` без кредов) | exposed, credentialed |
|
||||
| `books-db` (mariadb) | `0.0.0.0:3306` | ✅ root требует пароль | exposed, credentialed |
|
||||
| `bookva-db` (mariadb) | `0.0.0.0:33306` | ✅ | exposed, credentialed |
|
||||
| `minio` | `0.0.0.0:9000` | ✅ `AccessDenied` анонимно | exposed, credentialed |
|
||||
| `bookva-minio` | `0.0.0.0:9001` | ✅ | exposed, credentialed |
|
||||
| rsync | `:873` | ? | exposed |
|
||||
|
||||
Все БД **имеют auth** (в отличие от ES) → не дыра-нараспашку, но публикация портов БД
|
||||
наружу — плохая практика (brute-force / leaked-creds). Не emergency, рискованнее закрывать
|
||||
(возможен внешний admin-доступ). Follow-up hardening task, не часть инцидент-фикса.
|
||||
|
||||
**Главный learning:** любой stateful-сервис с публикуемым host-портом на VDS = публичный
|
||||
вектор в обход traefik+firewalld. Auth должна быть **на самом сервисе**, не только в
|
||||
traefik-мидлваре. ES free auth не имеет → его порт публиковать нельзя НИКОГДА.
|
||||
|
||||
## Preventive controls (applied 2026-05-28)
|
||||
|
||||
### Control #1 — ES `action.destructive_requires_name=true`
|
||||
|
||||
Env var на stack 33 (Portainer-managed). После применения wildcard/_all delete возвращают **400 Bad Request**:
|
||||
|
||||
```
|
||||
$ curl -sk -u books:<pw> -X DELETE 'https://elasticsearch.kzntsv.site/_all'
|
||||
{"error":{"type":"illegal_argument_exception","reason":"Wildcard expressions or all indices are not allowed"},"status":400}
|
||||
|
||||
$ curl -sk -u books:<pw> -X DELETE 'https://elasticsearch.kzntsv.site/ep*'
|
||||
{"error":{"type":"illegal_argument_exception","reason":"Wildcard expressions or all indices are not allowed"},"status":400}
|
||||
|
||||
$ curl -sk -u books:<pw> -X DELETE 'https://elasticsearch.kzntsv.site/epz'
|
||||
{"acknowledged":true} # legit by-name delete still works
|
||||
```
|
||||
|
||||
Класс ошибки «DELETE wildcard на неправильный endpoint» теперь блокируется ES до того, как достанется до данных. Reindex по одному индексу остаётся легитимным (нужно для миграции). Полностью **физически невозможно** удалить >1 индекса одной командой.
|
||||
|
||||
Apply: добавить `action.destructive_requires_name: "true"` в `environment:` блок stack 33 compose, PUT через Portainer API endpoint `/api/stacks/33?endpointId=1`. ES container recreate ~10 сек, consumers переподключатся через retry.
|
||||
|
||||
### Control #2 — Traefik accessLog (JSON)
|
||||
|
||||
Без этого никакая ретроспективная диагностика невозможна. Static config в `/usr/docker/traefik/data/traefik.yml`:
|
||||
|
||||
```yaml
|
||||
accessLog:
|
||||
filePath: /letsencrypt/access.log
|
||||
format: json
|
||||
bufferingSize: 100
|
||||
```
|
||||
|
||||
`/letsencrypt/` уже bind-mounted (`./letsencrypt:/letsencrypt` в `/usr/docker/traefik/docker-compose.yml`), файл persistent. Restart container — изменения подхватились (accessLog = static config, не dynamic).
|
||||
|
||||
Sample entry:
|
||||
```json
|
||||
{"ClientHost":"...", "ClientUsername":"books", "RequestMethod":"DELETE", "RequestHost":"elasticsearch.kzntsv.site",
|
||||
"RequestPath":"/_all", "RouterName":"elasticsearch@docker", "DownstreamStatus":400, ...}
|
||||
```
|
||||
|
||||
Будущие подозрительные запросы видны с IP, username basicAuth, methodом, путём, response status. Логирование = ~30–50 MB/day при типичной нагрузке (~1 req/sec).
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- **Logrotate** на `/usr/docker/traefik/letsencrypt/access.log` — пока без него файл будет расти; не критично (disk free 63G, ressedimption ~1–2 года), но micro-task на logrotate cron имеет смысл.
|
||||
- **bookva-es traefik route** — если когда-нибудь bookva-es потребует внешнего доступа, обязательно отдельный hostname (`bookva-es.kzntsv.site`) с отдельной basicAuth. **Никогда** не делиться `elasticsearch.kzntsv.site` между slovo/bookva.
|
||||
- **Snapshot retention review** — сейчас 7 daily snapshots. Если бы инцидент проявился через 8 дней, daily-25.05 был бы вытеснен. Для критичных корпусов имеет смысл добавить weekly/monthly tier. Отдельная мини-задача.
|
||||
- **Логирование callers без accessLog ретроспективно невозможно** — это main learning. Любой новый production endpoint должен иметь access log с момента создания.
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- [[../entities/books-vds]] § Стек, ES — stack 33 inventory.
|
||||
- [[../sources/books-vds-backup-daily-kreknin-2026-05-25]] — backup pipeline который сделал spasibo daily-25.05 snapshot.
|
||||
- [[../../tasks/restore-elasticsearch-indices-books-vds]] — incident response trace (этой сессии).
|
||||
- [[../../tasks/migrate-elasticsearch-to-books-vds]] — original migration playbook (применим для reindex-from-remote fallback).
|
||||
@@ -50,7 +50,7 @@ Globальные RTO/RPO разнесены **per service** — cost-of-downtime
|
||||
**Текущий статус (2026-05-21):** CMS прод (sites + MSSQL + MinIO) на [[../entities/windows-recovery-host]] **не имеет ни одного бэкапа нового состояния** — последний Hyper Backup 2026-05-09 был с мёртвой синки. RPO факт = ∞ (если host сгорит сейчас — теряем всё).
|
||||
|
||||
**Фаза 1 — stop-gap (1-2 дня работы, на этой неделе):**
|
||||
Impl-task: [[../../.tasks/cms-stopgap-backup-daily]].
|
||||
Impl-task: `windows-host-fallback-backup-daily` (🟢 closed 2026-05-24, декоммишнен 2026-06-11).
|
||||
- MSSQL: ежедневный `BACKUP DATABASE FULL` → локальный `D:\backup\` → ночной rsync на [[../entities/kreknin-synology]]
|
||||
- IIS files (`C:\sites\*`): ежедневный rsync → kreknin
|
||||
- MinIO data dir: ежедневный rsync → kreknin
|
||||
|
||||
66
.wiki/concepts/galleries-storage-class-local-not-s3.md
Normal file
66
.wiki/concepts/galleries-storage-class-local-not-s3.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: MoreThenCms galleries — Local storage class, never in S3 (snolla imgproxy 404 root cause)
|
||||
status: resolved for pilorama98 (2026-06-12); other sites unmigrated
|
||||
tags: [cms, snolla, morethencms, minio, imgproxy, s3, galleries, migration]
|
||||
related: [[minio-imgproxy-on-vds]], [[../entities/books-vds]], [[../entities/ruvds-iis-host]]
|
||||
---
|
||||
|
||||
# Galleries storage class = Local (disk), not S3
|
||||
|
||||
## Root cause
|
||||
|
||||
В MoreThenCms `<fileStorageClients>` (Web.config) каждый `storageClient` — это **алиас конфига**, не имя бакета. Алиас `galleries` маппится на тип **`MoreThenCms.FileStorage.Local.GalleriesStorage`** → файлы на локальном диске IIS:
|
||||
|
||||
```
|
||||
C:\sites\snolla\App_Data\galleries\<siteId:N>\<storageFilename-guid>.<ext>
|
||||
```
|
||||
|
||||
То же у `assets`, `images`, `scripts`, `stylesheets`, `watermarks` — все **Local**. Продукты (e-commerce) — другой механизм, **реально в S3** (bucket `pilorama98`, prefix `products/`, шард по hex, без siteId).
|
||||
|
||||
Переписка **snolla** (Node) наивно строит источник как `s3://${image.storageClient}/${siteId}/${storageFilename}` (`packages/snolla/middleware/galleries.js:47`, `lib/viewModels/index.js:289`) — т.е. ждёт галереи в S3 в бакете с именем `galleries`. Но их там никогда не было → imgproxy «Source image is unreachable» (404). Это НЕ баг билдера URL и НЕ поломка ресайза — просто **данные не мигрированы** из Local-диска в S3.
|
||||
|
||||
Симптом-близнец на legacy-prod (`pilorama98.ru/galleries/<id>/images/<size>/<seq>.jpg`): `original/*` = 200 (читает диск), любой ресайз = 404 — отдельная мёртвая ветка legacy-ресайзера (System.Drawing/image-cache). Целевая архитектура = imgproxy, legacy-ресайз не чинится.
|
||||
|
||||
## Fix recipe (путь A — выбран)
|
||||
|
||||
storageClient как **имя бакета** = рабочая конвенция snolla. Заливаем Local-диск в одноимённый bucket, ключи verbatim — код snolla не меняется:
|
||||
|
||||
```powershell
|
||||
# на RUVDS IIS (rclone v1.74.2 уже стоит). env-var remote, без записи cred в rclone.conf
|
||||
$env:RCLONE_CONFIG_MIN_TYPE='s3'; $env:RCLONE_CONFIG_MIN_PROVIDER='Minio'
|
||||
$env:RCLONE_CONFIG_MIN_ACCESS_KEY_ID='<minio-access>'; $env:RCLONE_CONFIG_MIN_SECRET_ACCESS_KEY='<minio-secret>'
|
||||
$env:RCLONE_CONFIG_MIN_ENDPOINT='https://minio.kzntsv.site' # books-vds S3 API за traefik
|
||||
& rclone mkdir min:galleries
|
||||
& rclone copy "C:\sites\snolla\App_Data\galleries\<siteId>" min:galleries/<siteId> --transfers 8
|
||||
& rclone size min:galleries/<siteId> # сверить count+size с источником
|
||||
```
|
||||
|
||||
- `<siteId>` папки на диске уже lowercase 32-hex == `ownerId` snolla (`String(site.id).replace(-).toLowerCase()`) → ключи совпадают 1:1, трансформаций не надо.
|
||||
- Креды MinIO: `pass books-vds/full-env` (контейнер `minio`, env `MINIO_ACCESS_KEY`/`MINIO_SECRET_KEY`). imgproxy key/salt — env контейнера `imgproxy`.
|
||||
|
||||
## Smoke (важно: мимо LAN-DNS воркстейшна)
|
||||
|
||||
`imgproxy.kzntsv.site` с воркстейшна резолвится в `127.0.0.1` (LAN-перехват CMS-доменов) — смоук гнать **с самого books-vds** или `--resolve …:89.253.255.133`:
|
||||
|
||||
```bash
|
||||
curl -sk --resolve imgproxy.kzntsv.site:443:127.0.0.1 'https://imgproxy.kzntsv.site/<sig>/fill/W/H/ce/0/<b64url(s3://galleries/<siteId>/<guid>.jpg)>.webp'
|
||||
# подпись: base64url( HMAC_SHA256(KEY_bin, SALT_bin + path) ), KEY/SALT = hex из env imgproxy
|
||||
```
|
||||
|
||||
Реальный объект → 200 image/webp; несуществующий guid → 404 (негативный контроль).
|
||||
|
||||
## Status / scope
|
||||
|
||||
- **pilorama98** (siteId `37e67fc44b9e4c06a522a26a73bac9b0`): мигрирован 2026-06-12 — 301 файл / 77 MiB, imgproxy 200 verified.
|
||||
- **Остальные ~40 siteId-папок** в `App_Data\galleries` — НЕ мигрированы (часть пустые). Bucket `galleries` shared; заполнен только префикс pilorama98. Другие сайты в snolla → 404 пока не зальёшь (регрессии нет).
|
||||
- Если поднимаются другие Local-классы (`images`/`scripts`/`stylesheets`/`watermarks`) в snolla через imgproxy — их ждёт та же дыра, тот же рецепт.
|
||||
|
||||
### `assets` класс — мигрирован 2026-06-13
|
||||
|
||||
Тот же Local-класс, тот же путь A. **Отличие ключа:** prefix = `OwnerId` медиа-папки (из таблицы `Folders`), **НЕ siteId**; snolla строит `s3://assets/<ownerId>/<storageFilename>` (`middleware/assets.js:98`), storageFilename плоский. Залит **весь** `C:\sites\snolla\App_Data\assets` (145 owner-папок, 4750 файлов / 215.5 MiB) в новый bucket `assets` rclone'ом с RUVDS — scope шире pilorama98 (owner→сайт маппинг неочевиден; заливка всего гарантирует покрытие + закрывает класс для всех тенантов). Verify count+size == source. Smoke: brevno.jpg → 200, валидно-подписанный фейк → 404.
|
||||
|
||||
⚠️ На стороне snolla `content-api/routes/assets.js` — пустой stub: объекты в S3 есть и отдаются через imgproxy, но inline-картинки контента не отрендерятся, пока роут не реализован (код-таска snolla).
|
||||
|
||||
⚠️ **Админка на S3 НЕ переключена** (класс `assets` в её `<fileStorageClients>` всё ещё Local) → правки ассета через админку RUVDS меняют только локальный `App_Data`, а боевой app читает MinIO — расходятся. Плюс отдельный баг прав на этой же локалке ронял админский delete/upload в 500. Оба — в [[snolla-admin-appdata-acl-500-after-scp-migration]] (там же рецепт ручного зеркалирования в оба хранилища).
|
||||
|
||||
См. задачи `.tasks/STATUS.md` → `[migrate-gallery-originals-to-s3]`, `[migrate-assets-originals-to-s3]`.
|
||||
81
.wiki/concepts/labtools-vds-deploy-runbook.md
Normal file
81
.wiki/concepts/labtools-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: labtools.ru snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback)
|
||||
type: concept
|
||||
tags: [labtools, snolla, vds, deploy, docker, portainer, traefik, runbook, migration]
|
||||
related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[snolla-live-prod-inplace-image-bump]]
|
||||
updated: 2026-07-05
|
||||
---
|
||||
|
||||
# labtools.ru → VDS deploy runbook
|
||||
|
||||
Вынос `labtools.ru` (snolla-приложение, `@snollajs/snolla`, 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.
|
||||
|
||||
## Артефакты
|
||||
- **Код:** `victor/labtools.ru` @ `43e28ba` (apps/web, parity ре-ревью №2 PASS). `deploy/Dockerfile` (multi-stage node:22-slim).
|
||||
- **Образ:** `registry.kzntsv.site/labtools:43e28ba` (+`:latest`). Собран НА VDS (обход traefik-499), 114 MB content.
|
||||
- **Стек Portainer:** `labtools` (Id 17, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/labtools.compose.yml`.
|
||||
|
||||
## Сборка образа (на VDS)
|
||||
```bash
|
||||
# чистый git-archive (только tracked → без config/default.json и node_modules) на VDS
|
||||
git -C ~/projects/labtools.ru archive --format=tar 43e28ba \
|
||||
| ssh vitya@89.253.255.94 'rm -rf ~/build/labtools && mkdir -p ~/build/labtools && tar -x -C ~/build/labtools'
|
||||
# build с build-arg VERDACCIO_TOKEN (pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN), push из VDS
|
||||
ssh vitya@89.253.255.94 'cd ~/build/labtools && docker build -f deploy/Dockerfile \
|
||||
--build-arg VERDACCIO_TOKEN=<token> -t registry.kzntsv.site/labtools:43e28ba -t registry.kzntsv.site/labtools:latest . \
|
||||
&& docker push registry.kzntsv.site/labtools:43e28ba && docker push registry.kzntsv.site/labtools:latest'
|
||||
```
|
||||
- VERDACCIO_TOKEN — build-time only (discarded build-стейдж; в финальный образ не попадает; docker linter warn-only).
|
||||
- `config/default.json` (dev-секреты) исключён `.dockerignore` — в образе только `production.json` + `custom-environment-variables.json`.
|
||||
|
||||
## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git)
|
||||
Маппинг env→config: `apps/web/config/custom-environment-variables.json`. Non-secret (host/siteId/endpoints) — в `production.json`.
|
||||
|
||||
| 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; default.json имел dev-значение — НЕ брать оттуда) |
|
||||
|
||||
PORT — не переопределять (образ дефолтит 5000; EXPOSE 5000; healthcheck `/robots.txt` на $PORT). traefik service-port = 5000.
|
||||
|
||||
## Runtime egress (наружу из proxy-сети)
|
||||
`mssql.kzntsv.site:1433` (БД-контент, обязателен) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы).
|
||||
|
||||
## Staging smoke (2026-07-01, GREEN) — `labtools.vds.kzntsv.site` vs бой `www.labtools.ru`
|
||||
- Все страницы меню (home/about/clients/contacts/cookie/privacy/6×products) — 200/200, дельта размера стабильна (длина хоста в canonical/og).
|
||||
- Редиректы: `.php`→301→`/`, `/about/`→301 (trailing-slash), `/Products/Press-Forms`→301→lowercase — 1:1 с боем.
|
||||
- `theme.css` из MinIO — **байт-в-байт** (12248), `Cache-Control: max-age=86400, public, must-revalidate`.
|
||||
- `/assets/<ownerId>/*.webp` из MinIO — **байт-в-байт** с боем (44444, 24354); content-type `application/octet-stream` — как на бою.
|
||||
- Контейнер healthy (MSSQL+S3 подключены).
|
||||
|
||||
## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02
|
||||
⚠️ **DNS зоны `labtools.ru` — на Yandex DNS (`dns1/dns2.yandex.net`), НЕ reg.ru** (в отличие от `labtools.pro`/reg.ru). Порядок был:
|
||||
1. **(оператор)** Yandex DNS: A-записи `labtools.ru` + `www.labtools.ru` → `89.253.255.94` (с RUVDS `80.64.31.36`).
|
||||
2. **(ops) verify перед traefik — критично:** флип шёл неравномерно, **dns1 и dns2 расходились** (dns2 обновился первым, dns1 отставал). Гейт cutover = **ОБА** авторитетных NS согласованно отдают VDS по apex+www (иначе LE мог попасть на dns1→RUVDS→сжечь rate-limit). Публичные резолверы кэшировали до 6ч (TTL 21600) — для LE не критично (резолвит по авторитетному). Cм. memory [operator-dns-flip-verify-domain-before-cutover].
|
||||
3. **(ops) после подтверждения обоих NS:** стек `labtools` (Id 17) traefik-rule → `Host(\`labtools.ru\`) || Host(\`www.labtools.ru\`)` (Portainer PUT, env 8/8 сохранён, pullImage=false). LE-cert выпущен (~t+20s; первые хиты ловили self-signed в окне issuance, потом ssl_verify=0).
|
||||
4. **Live-smoke GREEN:** оба хоста 200 (nav + каталог-секции + продукты); редиректы (`/about/`→trailing, `/Contacts`→lowercase, `/index.php`→`/`) parity; sitemap = index (host=`labtools.ru`, non-www — siteUrl этого тенанта без www, в отличие от .pro/R3); theme-ассеты из MinIO byte-identical бою (RUVDS отдаёт 301 apex→www, поэтому прямой md5 «DIFF» — артефакт; с `-L` MD5-OK). staging-хост `labtools.vds.kzntsv.site` убран (→404). **Внешняя проверка с RUVDS-хоста:** `labtools.ru`→VDS 200; `www` ещё на кэше RUVDS (200, идентичный контент) — split-brain окно, тухнет по TTL.
|
||||
5. `labtools.pro` — **закрыт отдельно** ([[labtools.pro-vds-deploy-runbook]], LIVE 2026-07-02).
|
||||
|
||||
## Rollback
|
||||
- **DNS:** вернуть A-записи `labtools.ru`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS.
|
||||
- **Стек:** Portainer → stack `labtools` → remove (или откат тега образа). Образ в registry остаётся.
|
||||
- RUVDS IIS labtools **не выводить из эксплуатации** до явного решения оператора (директива 2026-07-01).
|
||||
|
||||
## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05
|
||||
Тираж snolla 0.42.1. labtools.ru обновлён на живом стеке 17 **in-place** (домен уже на VDS, DNS не трогали).
|
||||
Рецепт — [[snolla-live-prod-inplace-image-bump]] (общий для labtools.ru/emspb/labtools.pro). Оператор дал отмашку на боевой apply.
|
||||
- **Образ:** `registry.kzntsv.site/labtools:566d41c` (snolla 0.28.2→**0.42.1**, digest `ea0649a…`). Собран на VDS.
|
||||
- **Acceptance С VDS (throwaway-staging из env стека 17):** новый sitemap = **38 page-locs** (7 pages+1 static+6 sections+24 продукта) — 37×200 + 1×301(`/products/laboratory-ball-mills`→`/lshm-750`, идентично проду). content-not-lost 11/11 (0 потерь). **Order-фикс 0.42.1** (liquid 0.10.2): presses = `plg-20,plg-12,plg-25,pgr-10` == прод.
|
||||
- **Находка:** старый 0.28.2 sitemap был дефицитным (sections=1, catalog=2 продукта) — 0.42.1 отдаёт полную корректную структуру. Ещё: sitemap листит 301-секцию как `<loc>` (прод идентичен, engine-intended; heads-up прогу→снолле).
|
||||
- **Swap:** Portainer PUT стека 17 (env 8/8 сохранён, pullImage=true) → healthy → live-smoke GREEN, TLS CN=labtools.ru не тронут (без LE-churn).
|
||||
- **Rollback:** тег `labtools:43e28ba` (0.28.2, в registry) / стек 17 PUT назад / DNS→RUVDS.
|
||||
- Compose обновлён на `566d41c`. Таска `[labtools-ru-deploy-snolla-0-42-1]` 🟢. Notify=victor/labtools.ru.
|
||||
88
.wiki/concepts/labtools.pro-vds-deploy-runbook.md
Normal file
88
.wiki/concepts/labtools.pro-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: labtools.pro snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback)
|
||||
type: concept
|
||||
tags: [labtools-pro, labtools, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, catalog]
|
||||
related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[emspb-vds-deploy-runbook]], [[labtools-vds-deploy-runbook]], [[snolla-live-prod-inplace-image-bump]]
|
||||
updated: 2026-07-05
|
||||
---
|
||||
|
||||
# labtools.pro → VDS deploy runbook
|
||||
|
||||
Вынос `labtools.pro` (snolla-приложение, `@snollajs/snolla` **0.28.7**, server-side Liquid, **каталожный сайт**)
|
||||
с [[../entities/ruvds-iis-host]] (catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]]
|
||||
(89.253.255.94), за traefik. Модель — snolla-app: читает БД-контент MoreThenCms (`mssql.kzntsv.site`)
|
||||
+ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy. **Зеркало [[emspb-vds-deploy-runbook]]** — тот же паттерн;
|
||||
отличие: каталог `/products/*` + `Culture=en` (англоязычный близнец labtools.ru) + Cache-Control отсутствует (см. ниже).
|
||||
|
||||
## Артефакты
|
||||
- **Код:** `victor/labtools.pro` @ `7bd9fae` (apps/web, ре-ревью PASS все 9 дименшнов, ноль хаков, snolla 0.28.7 final). `deploy/Dockerfile` (multi-stage node:22-slim, non-root, healthcheck `/robots.txt`).
|
||||
- **Образ:** ⚠️ `registry.kzntsv.site/labtools-pro:7bd9fae` (+`:latest`) — имя **`labtools-pro`**, НЕ `labtools` (тот у labtools.ru!). Собран НА VDS (обход traefik-499). digest `sha256:23d0bc597c8930a1bf443c418a4af68d63b692903963c0e1f9f25baa4a1dd236`.
|
||||
- **Стек Portainer:** `labtools-pro` (**Id 19**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/labtools-pro.compose.yml`.
|
||||
- **siteId:** `663F9410-A6CC-4651-9A5C-62844A313957`, activeTheme `389AD745-E3EE-4F23-BE16-DF38440A0941` (theme store MinIO `themes/389ad745e3ee4f23be16df38440a0941/`). Non-secret — в `production.json`, НЕ env.
|
||||
- **siteUrl:** `https://www.labtools.pro` — в `production.json` (R3: sitemap/robots/canonical отдают www).
|
||||
|
||||
## Сборка образа (на VDS)
|
||||
```bash
|
||||
git -C ~/projects/labtools.pro archive --format=tar 7bd9fae \
|
||||
| ssh vitya@89.253.255.94 'rm -rf ~/build/labtools-pro && mkdir -p ~/build/labtools-pro && tar -x -C ~/build/labtools-pro'
|
||||
ssh vitya@89.253.255.94 "cd ~/build/labtools-pro && VERDACCIO_TOKEN='<token>' docker build -f deploy/Dockerfile \
|
||||
--build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/labtools-pro:7bd9fae -t registry.kzntsv.site/labtools-pro:latest . \
|
||||
&& docker push registry.kzntsv.site/labtools-pro:7bd9fae && docker push registry.kzntsv.site/labtools-pro:latest"
|
||||
```
|
||||
- VERDACCIO_TOKEN — `pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN`, build-time only (не в финальный образ; docker linter warn-only).
|
||||
- `config/default.json` (dev-секреты) исключён `.dockerignore` — guard проверен на архиве: default.json ABSENT (только production.json + custom-environment-variables.json + default.example.json).
|
||||
|
||||
## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git)
|
||||
**Значения идентичны labtools/emspb** (все snolla-тенанты = одна MoreThenCms + общий MinIO/imgproxy/smtp; tenant-разница = siteId, не секрет). При создании стека env-массив **переиспользован verbatim из labtools stack (Id 17)** через Portainer API. 8 env: `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD`. Маппинг env→config: `apps/web/config/custom-environment-variables.json`.
|
||||
PORT — не переопределять (образ 5000; EXPOSE 5000; healthcheck `/robots.txt`). traefik service-port = 5000.
|
||||
|
||||
## Создание стека (Portainer API, X-API-Key)
|
||||
```bash
|
||||
K=$(pass show vds-kzntsv/full-env | sed -n 's/^PORTAINER_API_KEY=//p'); BASE=https://portainer.vds.kzntsv.site
|
||||
ENV=$(curl -ksS -H "X-API-Key: $K" $BASE/api/stacks/17 | jq '.Env') # verbatim из labtools stack 17
|
||||
COMPOSE=$(cat ~/build/labtools-pro.compose.yml)
|
||||
PAYLOAD=$(jq -n --arg name labtools-pro --arg compose "$COMPOSE" --argjson env "$ENV" '{name:$name,stackFileContent:$compose,env:$env,fromAppTemplate:false}')
|
||||
curl -ksS -X POST "$BASE/api/stacks/create/standalone/string?endpointId=1" -H "X-API-Key: $K" -H 'Content-Type: application/json' --data "$PAYLOAD" # → Id 19
|
||||
```
|
||||
Предусловие pull: `registry.kzntsv.site` зарегистрирован в Portainer как Custom registry Id 1 (иначе `no basic auth`).
|
||||
|
||||
## Runtime egress
|
||||
`mssql.kzntsv.site:1433` (БД-контент) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы).
|
||||
|
||||
## Staging smoke (2026-07-02, GREEN) — `labtools-pro.vds.kzntsv.site` vs бой `www.labtools.pro` (RUVDS IIS 80.64.31.36)
|
||||
Гнать с самого VDS (воркстейшн ловит LAN-DNS-перехват на `*.labtools.pro`; с VDS бой резолвится публично → RUVDS).
|
||||
- **Status-паритет:** nav (`/ /about /contacts`) + 3 каталог-секции (`/products/laboratory-presses`, `/products/press-forms`, `/products/milling-accessories`) + продукты (`plg-20`, `press-forms/ring`) — все 200/200.
|
||||
- **Редиректы (301 parity, target идентичен modulo host):** каталог#1 `/products/laboratory-ball-mills`→`…/laboratory-ball-mills/lshm-750`; `/Contacts`→lowercase `/contacts`; `/contacts/`→trailing `/contacts`.
|
||||
- **Sitemap — структура index (snolla) vs плоский (legacy), покрытие идентично:** бой отдаёт плоский `/sitemap.xml` с 25 URL; snolla отдаёт **sitemap-INDEX** из 7 дочерних (`sitemap-pages-1` 3, `sitemap-static-pages-1` 2, `sitemap-sections-1` 3, 4× `sitemap-catalog-<GUID>-products-1` = 1+2+2+12=17), все 200, **в сумме ровно 25 URL, набор == бой** (comm — ноль расхождений). `<loc>` host = `www.labtools.pro` (R3). **НЕ дефект** — index-структура штатна для snolla (как pilonuxt).
|
||||
- **theme-ассеты из MinIO md5-IDENTICAL бою** (проверены css/js/png/svg под `/themes/389ad745e3ee4f23be16df38440a0941/`): 7/7 MD5-OK, http 200.
|
||||
- ⚠️ **Cache-Control ОТСУТСТВУЕТ на theme-ассетах — КОРРЕКТНО** (labtools.pro `CachingOptions=[]` empty-config; движок 0.28.7 не ставит заголовок = бой). НЕ флагать как дефект (в отличие от emspb/labtools где был `max-age=86400`).
|
||||
- **Контент-страницы рендерят реальное тело** (12–22 KB), дельта vs бой +155…+222 B — стабильна, объясняется длиной staging-host в canonical/og (на 11 симв. длиннее www; на cutover уравняется в byte-parity).
|
||||
- Контейнер healthy, MSSQL (tedious) + S3 (aws-sdk) подключены, `listening at http://localhost:5000`, лог чист (только deprecation-warnings tedious/aws-sdk-v2).
|
||||
|
||||
### Косметика (не дефект, отдано workshop)
|
||||
Стартовый баннер в логе — `labtools.ru (snolla) listening…` — строка-константа унаследована из labtools.ru-референса при скаффолде. Идентификация сайта = siteId в production.json (663F9410…), на рендер/контент не влияет.
|
||||
|
||||
## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02
|
||||
Порядок был (как emspb):
|
||||
1. **(оператор)** reg.ru: A-записи `labtools.pro` + `www.labtools.pro` → `89.253.255.94` (с RUVDS `80.64.31.36`).
|
||||
2. **(ops) verify флипа перед traefik:** предыдущие проверки ловили старый TTL-кэш (публичные резолверы отдавали 80.64.31.36); **авторитетный `ns1.reg.ru` через `Resolve-DnsName` подтвердил** `labtools.pro`+`www` → `89.253.255.94` (TTL 3600), 8.8.8.8 догнал. Именно `.pro` (НЕ `.ru`). Урок: при "поменял DNS" проверять авторитетный NS напрямую, а не только кэширующие резолверы. Cм. memory [operator-dns-flip-verify-domain-before-cutover].
|
||||
3. **(ops) ТОЛЬКО ПОСЛЕ flip:** стек `labtools-pro` (Id 19) traefik-rule → `Host(\`labtools.pro\`) || Host(\`www.labtools.pro\`)` (Portainer PUT, env сохранён 8/8, pullImage=false). LE HTTP-01 выпустил cert **мгновенно** (ssl_verify=0 с первого хита).
|
||||
4. **Live-smoke GREEN:** оба хоста 200 (nav+каталог+продукты); редиректы (ball-mills→lshm-750, /Contacts→lowercase, /contacts/→trailing) parity; sitemap index 7 детей host=www; контент реальный (12728 B, `lang="en"`). staging-хост `labtools-pro.vds.kzntsv.site` убран из rule (→ 404). **Внешняя проверка с RUVDS-хоста (Королёв, public DNS):** `labtools.pro`+`www` → 89.253.255.94 HTTP 200, cert доверенный.
|
||||
5. RUVDS labtools.pro — rollback-путь, жив (отвечает 301, TLS ок), НЕ тронут.
|
||||
|
||||
⚠️ **Порядок критичен:** Host-правило добавлять ТОЛЬКО ПОСЛЕ flip DNS (иначе traefik проактивно тянет LE-cert HTTP-01, challenge упадёт на RUVDS → сожжём rate-limit).
|
||||
|
||||
## Rollback
|
||||
- **DNS:** вернуть A-записи `labtools.pro`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS.
|
||||
- **Стек:** Portainer → stack `labtools-pro` (Id 19) → remove (или откат тега образа). Образ в registry остаётся.
|
||||
- RUVDS IIS labtools.pro **не выводить** до явного решения оператора (директива 2026-07-01). (Ср.: emspb.ru биндинги на RUVDS сняты 2026-07-02 после подтверждённого cutover — см. [[../entities/ruvds-iis-host]].)
|
||||
|
||||
## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05
|
||||
Тираж snolla 0.42.1. labtools.pro обновлён на живом стеке 19 **in-place** (домен уже на VDS, DNS не трогали).
|
||||
Рецепт — [[snolla-live-prod-inplace-image-bump]]. Оператор дал отмашку на боевой apply.
|
||||
- **Образ:** `registry.kzntsv.site/labtools-pro:0610432` (snolla 0.28.7→**0.42.1**, digest `3d543fc…`). Собран на VDS.
|
||||
- **Acceptance С VDS (throwaway-staging из env стека 19):** **25 sitemap page-locs все 200** (+robots+sitemap.xml = 27 прогерских), content-not-lost 25/25 (0 потерь).
|
||||
- **Order-парити ВСЕ 3 секции MATCH == прод** (фикс liquid 0.10.2): presses=`plg-20,plg-12`, milling=`milling-jars,grinding-media`, press-forms=**12** изделий (round-xrf…round-collapsible). NB: первичный ручной счёт дал 13 (задвоил тайл), прог-сверка = 12, парити не задет.
|
||||
- **Swap:** Portainer PUT стека 19 (env 8/8 сохранён) → healthy → live-smoke GREEN (nav+presses+press-forms 200, order вживую), TLS CN=labtools.pro не тронут.
|
||||
- **Rollback:** тег `labtools-pro:7bd9fae` (0.28.7, в registry) / стек 19 PUT назад / DNS→RUVDS.
|
||||
- Compose обновлён на `0610432`. Таска `[labtools-pro-deploy-snolla-0-42-1]` 🟢. Notify=victor/labtools.pro.
|
||||
59
.wiki/concepts/mcp-init-resilience.md
Normal file
59
.wiki/concepts/mcp-init-resilience.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: mcp-init-resilience
|
||||
type: concept
|
||||
ingested_at: '2026-06-10T10:58:09.003Z'
|
||||
ingested_by: OpeItcLoc03@DESKTOP-NSEF0UK
|
||||
source_project: OpeItcLoc03/workshop
|
||||
---
|
||||
# MCP init resilience — почему user-level MCP не стартуют
|
||||
|
||||
Зафиксировано 2026-05-23. Round 2 не начат.
|
||||
|
||||
## Класс инцидента
|
||||
|
||||
Два прецедента (2026-05-21, 2026-05-23): Claude Code не поднимает часть user-level MCP-серверов
|
||||
при инициализации. `/reload-plugins` не помогает (он reload'ит только plugin-MCPs, не user-level entries).
|
||||
|
||||
**Пострадавшие серверы:** interns, kicad, context7 — общее: все имеют **multi-step launcher**.
|
||||
- `interns` — `pass show` (blocking gpg-agent call) → exec python
|
||||
- `kicad` — node spawn'ит python helper (двойной spawn)
|
||||
- `context7` — `npx -y @upstash/context7-mcp` (npm fetch / cache lock при первом запуске)
|
||||
|
||||
**4 успешных** используют прямой launcher (direct npx/node/uv).
|
||||
|
||||
## Гипотеза root cause
|
||||
|
||||
CC MCP supervisor запускает MCPs параллельно при init, ждёт `initialize` JSON-RPC в пределах
|
||||
timeout (значение неизвестно). Multi-step launchers не укладываются → помечаются disconnected.
|
||||
Retry в сессии не делается. Параллельная вторая CC сессия (+28с) — вероятный contributing factor.
|
||||
|
||||
## 7 открытых вопросов
|
||||
|
||||
1. Сколько секунд ждёт CC `initialize` ответа? Есть ли `mcp.startupTimeoutMs` knob в `settings.json`?
|
||||
2. Можно ли получить MCP transport stderr-логи? (`claude --mcp-debug` / `CLAUDE_MCP_DEBUG=1`)
|
||||
3. `pass show` в `interns-mcp-launcher.sh` — заменить на env-file (уже делается для других секретов)?
|
||||
4. Serial-launch wrapper между двумя CC окнами?
|
||||
5. MCPs как long-lived daemons + CC connect по сокету?
|
||||
6. Plugin MCP (context7) — почему тоже упал при `/reload-plugins`?
|
||||
7. Enhancement request Anthropic: `/reload-mcp` для user-level серверов
|
||||
|
||||
## Fix-направления (не выбраны)
|
||||
|
||||
| Направление | Эффект | Цена |
|
||||
|---|---|---|
|
||||
| Убрать `pass show` из interns launcher → env-file | устраняет gpg-blocking | API key plain at rest |
|
||||
| Serial-launch wrapper CC сессий | снимает race | UX friction (ждать 30s) |
|
||||
| Найти `mcp.startupTimeoutMs` knob | увеличить порог | если knob'а нет — null |
|
||||
| MCPs как long-lived daemons + sockets | устраняет per-session re-init | зависит от CC поддержки |
|
||||
| Push к Anthropic: `/reload-mcp` | runtime recovery | долго, off our hands |
|
||||
|
||||
## Preemptive guards
|
||||
|
||||
- **Не редактировать `interns-mcp-launcher.sh` импульсивно** — сначала воспроизвести без параллельной .admin сессии
|
||||
- **Не выпиливать `pass` без обсуждения** security trade-off
|
||||
- **Не предлагать `--mcp-debug` рестарт** в активной сессии
|
||||
|
||||
## Round 2: первые два шага
|
||||
|
||||
1. Проверить `mcp.startupTimeoutMs` в `~/.claude/settings.json` schema
|
||||
2. Воспроизвести в чистой сессии (только одна CC, без параллельной .admin) — если стартуют → виновен race; если нет → виноват launcher
|
||||
@@ -1,12 +1,35 @@
|
||||
---
|
||||
title: MinIO + imgproxy stack on vds-kzntsv
|
||||
status: live as standby — CMS image pipeline остаётся на windows-host (closed-source DLL hardcoded URL)
|
||||
status: STANDBY/unused for CMS — живой CMS image pipeline переехал на books-vds 2026-06-08 (см. ниже)
|
||||
tags: [vds, minio, imgproxy, migration, ops]
|
||||
related: [[vds-kzntsv]], [[recovery-architecture-snapshot]], [[mssql-on-vds]]
|
||||
related: [[vds-kzntsv]], [[recovery-architecture-snapshot]], [[mssql-on-vds]], [[../entities/books-vds]], [[galleries-storage-class-local-not-s3]]
|
||||
---
|
||||
|
||||
# MinIO + imgproxy on vds-kzntsv
|
||||
|
||||
> ⚠ **Stale для живого pipeline (2026-06-08).** `imgproxy.kzntsv.site` (hardcoded в `MoreThenCms.Modules.Imgproxy.dll`) теперь смотрит на **[[../entities/books-vds]]** (стек 29 imgproxy + стек 30 minio, bucket'ы `pilorama98`, `galleries`, …), а windows-recovery-host декоммишнен. Этот VDS-kzntsv стек — standby/неиспользуемый для CMS. Option C ниже («остаётся на windows-host») историчен. Миграция галерей и рецепт smoke — [[galleries-storage-class-local-not-s3]].
|
||||
|
||||
## S3 access для клиентских приложений (content-api, raw-stream, прямой getObject)
|
||||
|
||||
**Это раздел-lookup — отвечать на «дай MinIO endpoint/креды» отсюда, без SSH на сервер.**
|
||||
|
||||
Живой MinIO с бакетами CMS — на **[[../entities/books-vds]]** (`89.253.255.133`), legacy-инстанс `minio/minio:RELEASE.2020-07-13` (миграция as-is, без ротации — поэтому ключ совпадает с историческим windows-host).
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Endpoint (внешний, TLS) | `https://minio.kzntsv.site` (traefik → `minio:9000`, LE cert, port 443) |
|
||||
| Endpoint (сырой host-порт) | `http://89.253.255.133:9000` (тот же бэкенд, ssl:false) |
|
||||
| Endpoint (inter-container на books-vds) | `http://minio:9000` (сеть `proxy`) — так ходит imgproxy |
|
||||
| accessKeyId | `AKIAJ2YJP72W6ZHCRE6Q` (= `MINIO_ACCESS_KEY`, root, read+write all) |
|
||||
| secretAccessKey | в `pass minio-vds/full-env` (`MINIO_ROOT_PASSWORD`) |
|
||||
| region / pathStyle / ssl | `local` / `true` / **`true` для https-хостнейма** (false для сырого `:9000`) |
|
||||
|
||||
**Форма ключа (подтверждена тем, что imgproxy отдаёт 200):** `bucket=<storageClient>`, `key=<ownerId-lowercase>/<storageFilename>`, backslash→slash. owner из MSSQL приходит UPPERCASE → лоуэркейсить. Бакеты: `assets` (`<ownerId>/<file>`), `galleries` (`<siteId-hex-no-dashes>/<guid>.<ext>`), `pilorama98` (`products/<hex-shard>/...`).
|
||||
|
||||
⚠️ В snolla dev `default.json` endpoint `212.24.37.82:9000` + ключ `AV1QWDHDUQIPO6ABJOM0` — **устарели** (windows-host, декоммишен). Менять и endpoint, и креды на books-vds-значения выше.
|
||||
|
||||
> Прецедент: inbox-запрос snolla 2026-06-14 (raw-stream content-api) закрыт этим блоком.
|
||||
|
||||
Запущен 2026-05-22 как часть `minio-imgproxy-vds-migration`. Заодно **upgrade MinIO** `RELEASE.2020-07-13` (5 лет, CVE-куча) → `RELEASE.2025-09-07`.
|
||||
|
||||
## Architecture
|
||||
|
||||
66
.wiki/concepts/morethencms-null-settingsdata-https-502.md
Normal file
66
.wiki/concepts/morethencms-null-settingsdata-https-502.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: MoreThenCms — NULL SettingsData → 502 на HTTPS (per-tenant)
|
||||
type: concept
|
||||
tags: [morethencms, cms, iis, ruvds, https, 502, mssql, multi-tenant]
|
||||
sources: [../sources/iis-migration-to-ruvds-2026-05-23.md]
|
||||
updated: 2026-06-08
|
||||
---
|
||||
|
||||
# MoreThenCms — пустой `Sites.SettingsData` ⇒ 502 на HTTPS
|
||||
|
||||
## Симптом
|
||||
|
||||
Один тенант catch-all CMS ([[ruvds-iis-host]], движок **MoreThenCms**) недоступен **только по HTTPS**, тогда как остальные живут:
|
||||
|
||||
| | maljarka | рабочий тенант (kupimknigi) |
|
||||
|---|---|---|
|
||||
| `:80` (HTTP) | **200** | 200 |
|
||||
| `:443` (HTTPS) | **502** Bad Gateway (пустое тело) | 200 |
|
||||
|
||||
502 приходит с `Server: Microsoft-IIS/10.0`, `X-Powered-By: ASP.NET`, `Content-Length: 0`. В IIS-логе (`W3SVC1`) — `sc-status=502 sc-substatus=0 sc-win32-status=0` (managed-код, **не** HTTP.sys/ARR; ARR на хосте не установлен).
|
||||
|
||||
## Root cause
|
||||
|
||||
У сайта в БД `MoreThenCms` (`mssql.kzntsv.site,1433` → `dbo.Sites`) поле **`SettingsData` = NULL**. У рабочих сайтов там JSON с блоком:
|
||||
|
||||
```json
|
||||
"httpSecure": { "enableHttps": true, "httpsOnly": false, "forceHttpsRedirect": false, "forceHttpsApi": false, "forceHttpsAdministration": false }
|
||||
```
|
||||
|
||||
На **HTTPS**-запросе `SnollaMiddleware` читает ключ `httpSecure`/`enableHttps` из настроек сайта. При NULL-настройках ключа нет → `KeyNotFoundException: The given key was not present in the dictionary` (`MoreThenCms.Owin.ErrorHandler.cs:line 61`) → middleware отдаёт **502 до** пайплайна (в логе видно `Begin invoke /` → сразу `End invoke /`, без `RedirectionsService`/рендера). По HTTP блок не читается → страница рендерится (200).
|
||||
|
||||
Не зависит от хоста — данные в общей БД, поэтому симптом был идентичен и на старом home-IIS:8089. **Не дефект миграции.**
|
||||
|
||||
## Диагностический след (как ловить)
|
||||
|
||||
- Логи движка: `C:\Logs\MoreThenCms\log.YYYYMMDD.txt` (log4net). 502-запрос = `Begin invoke /` → `End invoke /` без промежуточных шагов; `KeyNotFoundException` в `Owin.ErrorHandler` (async-стек теряет исходный кадр — он не покажет конкретный словарь, не трать время).
|
||||
- Воспроизводить **с самого сервера** против `127.0.0.1` (`curl.exe --resolve host:443:127.0.0.1`), чтобы исключить свой сетевой путь (дома трафик может уходить через VPN/прокси — см. [[proxy-debugging-test-the-real-client]]).
|
||||
- Подключение к БД: строка `MoreThenCmsEntities` в `C:\sites\snolla\web.config` (EF-обёртка, `provider connection string=...`). `INFORMATION_SCHEMA.COLUMNS` под app-юзером может быть пуст — читать колонки из ридера (`GetName`), не из метаданных.
|
||||
|
||||
## Fix
|
||||
|
||||
Заполнить `SettingsData` сайта блоком `httpSecure` (структуру брать с рабочего sibling-сайта):
|
||||
|
||||
```sql
|
||||
UPDATE dbo.Sites
|
||||
SET SettingsData = N'{ "httpSecure": { "enableHttps": true, "httpsOnly": false,
|
||||
"forceHttpsRedirect": false, "forceHttpsApi": false, "forceHttpsAdministration": false } }'
|
||||
WHERE SiteId = '<site-guid>' AND SettingsData IS NULL; -- гард: не затереть существующие настройки
|
||||
```
|
||||
|
||||
Затем `Restart-WebAppPool snolla` (настройки сайта кэшируются в памяти w3wp). App-native альтернатива: в админке MoreThenCms (`/admin`) включить «HTTPS» в настройках сайта — пишет тот же блок.
|
||||
|
||||
`httpsOnly/forceHttpsRedirect=false` ⇒ поведение HTTP не меняется, риск минимальный; откат = вернуть `SettingsData = NULL`.
|
||||
|
||||
## Scope (audit 2026-06-08)
|
||||
|
||||
На [[ruvds-iis-host]] из 25 SNI-биндингов :443 единственным сайтом с NULL-настройками был **maljarka** — пофикшен 2026-06-08. Прочие ~27 NULL-сайтов **не имеют :443-биндинга** (HTTP-only / неактивные домены) → этот баг их не заденет, пока кому-то не повесят HTTPS-биндинг. **Памятка:** при добавлении нового HTTPS-тенанта на этот CMS — проверить, что у его сайта `SettingsData` содержит `httpSecure`.
|
||||
|
||||
> Не путать: `rimiz.ru`/www/`rimiz.snolla.com` тоже «degraded», но у них `SettingsData` заполнен — их 502/404 имеет **другую** причину (dead-routes контента), не этот баг.
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- Хост: [[ruvds-iis-host]]
|
||||
- БД-бэкенд: [[vds-kzntsv]] (MSSQL `mssql.kzntsv.site`)
|
||||
- Сетевой анти-паттерн при диагностике: [[proxy-debugging-test-the-real-client]]
|
||||
- Контекст миграции: [[../sources/iis-migration-to-ruvds-2026-05-23]]
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
title: MSSQL on vds-kzntsv (Express 2022 Linux)
|
||||
status: live
|
||||
type: concept
|
||||
tags: [vds, mssql, migration, ops]
|
||||
related: [[vds-kzntsv]], [[recovery-architecture-snapshot]], [[traefik-tcp-passthrough-vs-starttls]], [[db-tls-self-signed-via-traefik-raw-tcp]]
|
||||
related: [[../entities/vds-kzntsv]], [[recovery-architecture-snapshot]], [[traefik-tcp-passthrough-vs-starttls]], [[db-tls-self-signed-via-traefik-raw-tcp]]
|
||||
updated: 2026-06-12
|
||||
---
|
||||
|
||||
# MSSQL on vds-kzntsv
|
||||
@@ -83,13 +84,26 @@ ALTER USER snolla WITH LOGIN = snolla;
|
||||
4. **`MSSQL_SA_PASSWORD` спец-символы**: rotation password generator должен excludeавать `+/=` (Base64) и shell-meta (`$`, `` ` ``, `"`, `'`, `\`). Иначе env-var passing через ssh heredoc или sed конструкторы ломаются. Пример безопасного: `[Convert]::ToBase64String($bytes) -replace '[+/=]','x'`.
|
||||
5. **MSSQL Linux user uid**: `mssql` user = uid 10001. Bind-mount должен быть `chown 10001:0` чтобы контейнер read/write. Parent dir owns `vitya:vitya` чтобы можно было редактировать compose без sudo.
|
||||
|
||||
## Backup integration (TODO)
|
||||
## Backup integration
|
||||
|
||||
Расширить `/opt/stacks/backup/scripts/run.sh` (см. [[vds-backup-rsync-kreknin]]):
|
||||
- `mssql_dump_full` daily — `BACKUP DATABASE [<db>] TO DISK='...' WITH COMPRESSION, COPY_ONLY` через `docker exec mssql sqlcmd ...`
|
||||
- `mssql_tx_log_backup` hourly — `BACKUP LOG [<db>] TO DISK='...'` для FULL-recovery DBs (только `MoreThenCms` — остальные SIMPLE).
|
||||
- Достигает target RPO 1ч (vs current daily snapshot).
|
||||
- ntfy push на `vds-backup` topic.
|
||||
Реализовано 2026-06-11 в `/opt/stacks/backup/scripts/run.sh` (см. [[vds-backup-rsync-kreknin]]). Блок между Step 1 (DB dumps) и Step 2 (rsync):
|
||||
|
||||
```bash
|
||||
source /opt/stacks/databases/mssql/.env # injects MSSQL_SA_PASSWORD
|
||||
for DB in MoreThenCms StayerCalculator StayerPrice TireService stostayer; do
|
||||
docker exec mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -b \
|
||||
-Q "BACKUP DATABASE [$DB] TO DISK='/var/opt/mssql/backups/${DB}.bak' WITH COPY_ONLY, FORMAT"
|
||||
mv /opt/stacks/databases/mssql/backups/${DB}.bak "$DUMP_DIR/mssql-${DB}.bak"
|
||||
done
|
||||
unset MSSQL_SA_PASSWORD
|
||||
```
|
||||
|
||||
**Gotcha #1 — Express не умеет COMPRESSION:** Express Edition не поддерживает `WITH COMPRESSION`. .bak файлы создаются в bind-mount `/var/opt/mssql/backups/` (uid 10001:10001); root mv их в `$DUMP_DIR` для rsync на kreknin. sqlcmd path: `/opt/mssql-tools18/bin/sqlcmd`.
|
||||
|
||||
**Gotcha #2 — `FORMAT`, не `INIT` (incident 2026-06-12):** первая версия блока (added 2026-06-11) использовала `WITH COPY_ONLY, INIT` и **молча падала первым же cron-запуском**. Причина: в `/var/opt/mssql/backups/` лежали миграционные `.bak` от 22.05, созданные `WITH COMPRESSION` на Developer-источнике. `INIT` переиспользует существующий media-set header (компрессованный) → Express не может в него писать → `Msg 1844 "BACKUP ... WITH COMPRESSION is not supported on Express"` (хотя в команде COMPRESSION нет!). `NO_COMPRESSION` тоже не спасает → `Msg 3098 "media formatted with an incompatible structure"`. **Fix: `WITH COPY_ONLY, FORMAT`** — всегда создаёт новый media set, иммунно к остаткам. Урок: при бэкапе на Express в каталог с возможными чужими/старыми `.bak` — только `FORMAT`, не `INIT`.
|
||||
|
||||
**Backup gap:** между 22.05 (миграция) и 12.06 у 5 боевых баз **не было ни одной offsite-копии** — raw datadir намеренно не rsync'ится (lock), а dump-ветка падала с первого дня. Закрыто 2026-06-12.
|
||||
|
||||
## Rollback recipe
|
||||
|
||||
|
||||
96
.wiki/concepts/on-snolla-vds-deploy-runbook.md
Normal file
96
.wiki/concepts/on-snolla-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: on.snolla.com snolla-app — VDS deploy runbook (cutover 2026-07-20)
|
||||
type: concept
|
||||
tags: [on-snolla, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, landing]
|
||||
related: [[../entities/vds-kzntsv], [../entities/ruvds-iis-host], [tandemmebel-vds-deploy-runbook], [snolla-live-prod-inplace-image-bump], [portainer-stack-management-vds], [snolla-local-admin-and-on-snolla-migration-design]]
|
||||
updated: 2026-07-20
|
||||
---
|
||||
|
||||
# on.snolla.com → VDS deploy runbook
|
||||
|
||||
Вынос посадочной `on.snolla.com` (snolla-app, `@snollajs/snolla` **0.42.1**, server-side Liquid) с
|
||||
[[../entities/ruvds-iis-host]] (catch-all CMS, 80.64.31.36) в docker-контейнер на [[../entities/vds-kzntsv]]
|
||||
(89.253.255.94), за traefik. Паттерн = [[tandemmebel-vds-deploy-runbook]]. Отличие: **лендинг** (1 Page `/`
|
||||
+ 2 StaticPages), БЕЗ блога/каталога/e-commerce → reconstruction = layout.liquid byte-identical prod-HTML.
|
||||
Culture `en`. Спека: [[snolla-local-admin-and-on-snolla-migration-design]] §Task B.
|
||||
|
||||
## Артефакты
|
||||
- **Код:** `victor/on.snolla.com` @ `473923e494db` (apps/web, snolla 0.42.1). Структура-калька tandemmebel.ru.
|
||||
`deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root, healthcheck `/robots.txt`).
|
||||
- **Образ:** `registry.kzntsv.site/on-snolla:473923e494db` (+`:latest`). Имя **`on-snolla`** (= стек/контейнер).
|
||||
Собран НА VDS (обход traefik-499).
|
||||
- **Стек Portainer:** `on-snolla` (**Id 22**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/on-snolla.compose.yml`.
|
||||
- **siteId:** `B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7` (non-secret, в `production.json`).
|
||||
- **siteUrl:** `https://on.snolla.com` (в `production.json`).
|
||||
- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD`
|
||||
— Portainer stack env (переиспользованы verbatim из tandemmebel стека 20 — общая snolla-инфра). НЕ в образе, НЕ в git.
|
||||
- **mem_limit:** `512m`.
|
||||
|
||||
## Site model (MoreThenCms DB, siteId B9ECDB50)
|
||||
- 1 Page `/` (template `content_page`, `customDefaultLayout=layout`). **Content-поля пустые** → весь
|
||||
bootstraptor-лендинг baked в `views/layout.liquid` (byte-identical prod). `{{ item.content }}` (пуст) wired.
|
||||
- Form "Join us" (Path `/`, FormTemplate `join_us`: first_name/last_name/email/website/description/captcha)
|
||||
baked в `layout.liquid`; POST `/` → snolla `forms` middleware → `form_ajax_response.liquid` (AJAX replace).
|
||||
Legacy reCAPTCHA v1 в разметке мертва (Google shut down v1) — parity с продом, НЕ чиним.
|
||||
- StaticPages: `/yandex_7a510ac1f8311fca.html` (verification, IncludeInSitemap=true), `/4a052808276c.html` (not in sitemap).
|
||||
- `/c` → **404** (stale dead-link в .NET sitemap; snolla-app sitemap тоже его несёт → parity `/`+`/c`+`/yandex`).
|
||||
- Theme assets `/themes/c406a987ebe14244b0eefe7b8f959a6f/{css,js,images}/*` — из MinIO bucket `themes` (snolla
|
||||
`themeFiles` middleware; пути literal в `layout.liquid`).
|
||||
- `/admin` — **публичного `on.snolla.com/admin` не существует** (никогда не было: RUVDS catch-all IIS не
|
||||
маршрутизировал /admin на публичном on.snolla.com — cert на 80.64.31.36:443 SNI on.snolla.com = чужой
|
||||
`CN=kupimknigi.spb.ru`, /admin → 404). Админка — только **локальный catch-all IIS `snolla`** (Task A,
|
||||
воркстейшн). Site alias переименован `on`→`internal` (2026-07-20), доступ = `http://internal.snolla.com/admin`
|
||||
через hosts override → 127.0.0.1 → 302→login. **Web.config `primaryAlias`** тоже обновлён `on`→`internal`
|
||||
(default-site resolution; иначе catch-all NRE-500 на ВСЕХ запросах — primaryAlias ссылается на алиас,
|
||||
переименование сайта без обновления primaryAlias ломает весь catch-all). НЕ часть snolla-app.
|
||||
|
||||
## Cutover 2026-07-20 (первый вынос на VDS — DNS-gated)
|
||||
1. **Build на VDS** (archive `473923e494db` → `~/build/on-snolla` → `docker build -f deploy/Dockerfile
|
||||
--build-arg VERDACCIO_TOKEN -t .../on-snolla:473923e494db -t .../on-snolla:latest . && push`). Guard:
|
||||
`config/default.json` ABSENT в архиве (.dockerignore), пин snolla = 0.42.1.
|
||||
2. **Throwaway-staging :5077** из собранного env (8 секретов inline, т.к. нового live-контейнера ещё нет —
|
||||
секреты общие с тиражом). `docker run -d --name on-snolla-staging --env-file .staging.env -p 127.0.0.1:5077:5000`.
|
||||
Healthy. robots/`/`/sitemap 200.
|
||||
3. **Completeness-gate С VDS** — sitemapindex разворот. **NEW==PROD locs: 3 = 3 IDENTICAL** (`/`, `/c`, `/yandex_…`).
|
||||
Self-consistency: `/`→200, `/yandex`→200, `/c`→404 (dead-link, parity с прод-оракулом — prod /c тоже 404).
|
||||
**Byte-parity:** homepage + /yandex **byte-identical** staging vs prod (RUVDS, via `--resolve :443:80.64.31.36`).
|
||||
GREEN = 0 регрессий.
|
||||
4. **Portainer стек 22** создан со **staging-Host** `on-snolla.vds.kzntsv.site` (wildcard → VDS), LE=default cert.
|
||||
Healthy за traefik. (`create-stack.mjs` node-скрипт, Portainer JWT auth, env array 8/8, `mem_limit 512m`.)
|
||||
5. **DNS flip** — operator: reg.ru `on.snolla.com` A `80.64.31.36` → `89.253.255.94`.
|
||||
6. **Verify авторит. NS** — `nslookup on.snolla.com ns1/ns2.reg.ru` = `89.253.255.94` (BEFORE traefik rule swap —
|
||||
memory `operator-dns-flip-verify-domain-before-cutover`: wrong domain burns LE rate-limit). Public resolvers propagated.
|
||||
7. **PUT стека 22** — rule `Host(on-snolla.vds.kzntsv.site)` → `Host(on.snolla.com)`, env-preserving (8/8),
|
||||
`prune:false, pullImage:false`. node `cutover-put.mjs` (Portainer JWT; **re-auth right before PUT** —
|
||||
jwt из GET-фазы успел протухнуть к моменту PUT, первый PUT дал 401). PUT 200.
|
||||
8. **Poll** — контейнер healthy, **LE-серт issued on first hit**: CN=on.snolla.com, issuer YR1, until 2026-10-18.
|
||||
9. **Live-smoke С VDS + external** — `https://on.snolla.com/{robots.txt,/,/sitemap.xml,/yandex_…}` = 200,
|
||||
`/c` = 404 parity. TLS verify `ssl_verify_result=0` (trusted LE chain). Homepage **byte-identical** prod-HTML.
|
||||
|
||||
## Гочи (специфичные)
|
||||
- **snolla `robotsTxt` middleware падает на undefined `app.locals.domain`** — on.snolla.com НЕ имеет row в
|
||||
MoreThenCms `Domains` (served by primaryDomain без per-domain row). `robotsTxt.js` читает
|
||||
`res.app.locals.domain.robotsTxt` без null-guard → GET /robots.txt (и Dockerfile healthcheck) крашится 500.
|
||||
**Фикс в `apps/web/index.js`**: после `initApp`, если `!app.locals.domain`, synthesized `{robotsTxt: site.robotsTxt}`
|
||||
(only robotsTxt.js reads locals.domain; theme resolution uses site.activeThemeId — safe). Без мутации shared DB.
|
||||
**Альтернатива** (не применена): добавить Domains row (Name=on.snolla.com, SiteId=B9ECDB50, Public=1, ThemeId=null)
|
||||
— но это мутация shared prod MoreThenCms; предпочтён app-code fallback.
|
||||
- **Form "Join us" не имеет Liquid form-tag** — snolla не предоставляет form-rendering tag. Форма HTML baked в
|
||||
`layout.liquid` (поля по FormTemplate `join_us`); POST обрабатывает `forms` middleware (мэтч по Path `/`).
|
||||
`form_ajax_response.liquid` — AJAX-ответ (`$form.replaceWith(response)`).
|
||||
- **`/c` в sitemap — dead-link** — snolla SitemapService эмбитит `/c` из sections-source, но страница 404.
|
||||
Prod-оракул идентичен (404) → benign parity, не регрессия.
|
||||
- **hosts override на воркстейшне** — Task A оставил `127.0.0.1 on.snolla.com` в hosts (локальный админ catch-all).
|
||||
External verify прод-on.snolla.com с воркстейшна = только через `curl --resolve on.snolla.com:443:89.253.255.94`
|
||||
(иначе попадёшь в локальный IIS). См. memory `workstation-lan-dns-serves-local-cms-copy`.
|
||||
- **Portainer JWT short-lived** — GET stack + GET file + PUT в одном скрипте: jwt из GET-фазы может протухнуть
|
||||
к PUT. Re-auth (`POST /api/auth`) прямо перед PUT. API-key `ptr_*` даёт 401 (см. [[portainer-stack-management-vds]]).
|
||||
- **Образных rollback-тегов пока нет** — первый deploy, `473923e494db` = `:latest`. ~~Rollback = revert DNS (RUVDS жив).~~ RUVDS DECOMM 2026-07-21 — rollback только образ-тегом.
|
||||
|
||||
## Rollback
|
||||
- ~~**DNS (предпочт, мгновенный):** revert reg.ru `on.snolla.com` A → `80.64.31.36` (RUVDS IIS жив, не тронут).~~ **НЕАКТУАЛЬНО с 2026-07-21** — [[../entities/ruvds-iis-host]] DECOMM (погашен у провайдера). RUVDS более не rollback-target.
|
||||
- **Образный (рабочий):** PUT стека 22 назад на предыдущий тег (rollback-теги в registry). `473923e494db` = `:latest` — первый deploy, предыдущего тега нет; с этого дня ведём rollback-теги на каждый bump.
|
||||
|
||||
## Связанное
|
||||
- on.snolla.com admin (`/admin`) остаётся на RUVDS .NET — catch-all IIS `snolla` (см. [[snolla-local-admin-and-on-snolla-migration-design]] §Task A).
|
||||
- Тираж snolla на VDS: labtools.ru(17), emspb(18), labtools.pro(19), tandemmebel(20), kupimknigi(21), **on-snolla(22)**.
|
||||
127
.wiki/concepts/portainer-stack-management-books-vds.md
Normal file
127
.wiki/concepts/portainer-stack-management-books-vds.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: Portainer stack management on books VDS — pattern application + migration log
|
||||
status: live
|
||||
tags: [books-vds, portainer, docker-compose, ops, migration]
|
||||
related: [[books-vds]], [[portainer-stack-management-vds]], [[books-vds-backup-daily-kreknin-2026-05-25]]
|
||||
---
|
||||
|
||||
# Portainer stack management on books VDS
|
||||
|
||||
Application of the canonical pattern from [[portainer-stack-management-vds]] к **books VDS** (`89.253.255.133`, host4g.ru, CentOS 7).
|
||||
|
||||
**Canonical rule** (same as VDS-infra): all docker-compose стеки на books VDS управляются через Portainer `https://portainer.kzntsv.site` (endpoint 1). SSH-compose в `/usr/docker/<svc>/` — **anti-pattern** post-2026-05-25.
|
||||
|
||||
2026-05-25 retro-migrated 6 ранее SSH-compose стеков: `proxy-chain`, `imgproxy` (2 контейнера), `minio`, `mongo`, `books-db`, `elasticsearch`. Skipped `traefik` + `portainer` (management plane). Adapter script `scripts/books-vds-portainer-migration/migrate.sh`.
|
||||
|
||||
## Diffs from VDS-infra pattern
|
||||
|
||||
| Aspect | vds-kzntsv | books VDS |
|
||||
|---|---|---|
|
||||
| Portainer URL | `portainer.vds.kzntsv.site` | `portainer.kzntsv.site` |
|
||||
| Auth | JWT через admin/password (PAT 401'd) | **`X-API-Key` header — PAT works** |
|
||||
| Stack dir prefix | `/opt/stacks/<stack>/` | `/usr/docker/<stack>/` |
|
||||
| Endpoint ID | 1 (single docker daemon) | 1 (books VDS local). **Endpoint 6 = stostayer — НЕ наш** |
|
||||
| Compose down | `cd $DIR && docker compose down` | `docker rm -f` by `com.docker.compose.project` label |
|
||||
| `env_file:` directives | many stacks had them | **none** — env was inline в compose |
|
||||
| `.env` files | many stacks had them | **none** — все env inline |
|
||||
| Sibling files | nginx.conf, certs, scripts | nginx.conf for imgproxy-nginx only |
|
||||
| docker-compose binary | v2 plugin only | v2 plugin (2.27.1) + v1 standalone (1.29.2). Script uses neither — direct `docker rm -f` |
|
||||
|
||||
## Pre-flight quirks on books VDS
|
||||
|
||||
1. **No `jq` installed**, no python3, **yum is broken** (CentOS 7 EOL since 2024-06, mirrors retired). Mitigation: pre-install static jq 1.7.1 binary:
|
||||
```bash
|
||||
curl -fsSL https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64 \
|
||||
-o /usr/local/bin/jq && chmod +x /usr/local/bin/jq
|
||||
```
|
||||
2. **No `/root/.docker/config.json`** — no `docker login registry.kzntsv.site` configured. Risk: if Portainer create force-pulls `:latest` for private registry images (`proxy-chain`, books-*) → pull fail. **Mitigation in practice:** Portainer create использует local image cache when available; for current `:latest` cached images миграция прошла без issue. Future-proofing: `docker login registry.kzntsv.site` before any registry-managed stack force-pull.
|
||||
3. **Mongo image lost tag** — running container's image was retagged (digest `0bf62880fec3` only). Portainer create pulls fresh `mongo:4.2` — may be different sub-version digest. 4.2 schema stable, accepted as low risk. Mitigation if reproducibility critical: pin to digest.
|
||||
|
||||
## Stack inventory (2026-05-25 post-migration, Portainer Ids)
|
||||
|
||||
Endpoint 1 (books VDS):
|
||||
|
||||
| Id | Name | Compose dir | Note |
|
||||
|---|---|---|---|
|
||||
| 22 | books-job-scheduler | (Portainer-only, pre-migration) | agenda jobs + embedded mongo |
|
||||
| 23 | books-api | (Portainer-only) | books API server, Nitro, port 3021 |
|
||||
| 24 | books-web | (Portainer-only) | books Nuxt front-end |
|
||||
| 25 | books-ntfy | (Portainer-only) | local ntfy (separate from `ntfy.vds.kzntsv.site`) |
|
||||
| 26 | books-ops-mcp | (Portainer-only) | + books-docker-proxy + books-docker-proxy-ro + bootstrap |
|
||||
| 27 | chrome | (Portainer-only) | Puppeteer for PDFs/scraping |
|
||||
| 28 | proxy-chain | `/usr/docker/proxy-chain` | upstream HTTP-proxy chain, internal-only |
|
||||
| 29 | imgproxy | `/usr/docker/imgproxy` | imgproxy + imgproxy-nginx, serves `imgproxy.kzntsv.site` |
|
||||
| 30 | minio | `/usr/docker/minio` | S3 blob store, `minio.kzntsv.site` |
|
||||
| 31 | mongo | `/usr/docker/mongo` | shared MongoDB 4.2, used by books-api + books-task-runner |
|
||||
| 32 | books-db | `/usr/docker/books-db` | MariaDB latest, books application DB |
|
||||
| 33 | elasticsearch | `/usr/docker/elasticsearch` | ES 7.10.0 + `path.repo=/snapshots` for backup |
|
||||
|
||||
Non-Portainer (management plane, скип migration):
|
||||
- `traefik` — `/usr/docker/traefik/`, ad-hoc compose, бинд `./letsencrypt` (acme.json) + `./data/traefik.yml`
|
||||
- `portainer` — `/usr/docker/portainer/`, ad-hoc compose, named volume `portainer_data`
|
||||
|
||||
Endpoint 6 (stostayer VDS, **не наш** — user-confirmed 2026-05-25). Stacks listed: `Traefik`, `minio`, `mariadb`, `imgproxy`, `stostayer-{franchise,web,tireservice-api}`, `registry`. Same Portainer UI manages, but physical machine not ours.
|
||||
|
||||
## Migration ladder
|
||||
|
||||
Used 2026-05-25, lowest → highest blast radius. Successful in this order:
|
||||
|
||||
1. **proxy-chain** — internal-only, no traefik exposure → smoke for the script
|
||||
2. **imgproxy** — 2-container compose with sibling nginx.conf
|
||||
3. **minio** — read-mostly, books-api retries OK
|
||||
4. **mongo** — shared, used by books-api + books-task-runner. Reconnect transparent (~13s)
|
||||
5. **books-db** — MariaDB, books-api connection-pool reconnect (~15s)
|
||||
6. **elasticsearch** — single-node, snapshot repo `kreknin` preserved through bind
|
||||
|
||||
Все 6 stacks Up + smoke green в одну сессию (~10 минут end-to-end).
|
||||
|
||||
## Gotchas hit during migration
|
||||
|
||||
All 8 gotchas from [[portainer-stack-management-vds]] applied; books-specific additions:
|
||||
|
||||
9. **CentOS 7 base OS, yum dead** — package install через yum невозможно. Static jq binary download — единственный путь без EPEL mirror работающего. Documented above.
|
||||
10. **No registry login** — `docker login registry.kzntsv.site` не настроен. Кэшированные образы спасают, но fresh-pull для private images → auth fail. Setup `docker login` если планируется image-rotation от Portainer UI.
|
||||
11. **Compose v1 + Compose v2 plugin co-existence** — `docker-compose` (1.29.2) и `docker compose` (2.27.1) обе installed. Migration script использует `docker rm -f` для down → не зависит ни от той ни от другой. Чище.
|
||||
|
||||
## Smoke commands per stack (post-migration)
|
||||
|
||||
```bash
|
||||
# proxy-chain — internal smoke (HTTP 400 = service responding, not a valid proxy GET)
|
||||
curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:8778/
|
||||
|
||||
# imgproxy — traefik HTTP route
|
||||
curl -ksS -o /dev/null -w "%{http_code}\n" https://imgproxy.kzntsv.site/
|
||||
|
||||
# minio
|
||||
curl -ksS -o /dev/null -w "%{http_code}\n" https://minio.kzntsv.site/minio/health/live
|
||||
|
||||
# mongo (auth via root/MONGO_INITDB_ROOT_PASSWORD)
|
||||
docker exec mongo mongo --quiet --eval 'db.adminCommand({ping:1}).ok' -u root -p '<from pass>'
|
||||
|
||||
# books-db
|
||||
docker exec books-db mariadb -uroot -p'<from pass>' -e 'SELECT 1'
|
||||
|
||||
# elasticsearch
|
||||
docker exec elasticsearch curl -sS localhost:9200/_cluster/health | jq .status
|
||||
docker exec elasticsearch curl -sS localhost:9200/_snapshot # verify kreknin repo
|
||||
```
|
||||
|
||||
## What's preserved post-migration
|
||||
|
||||
- **Bind data paths** — `/usr/docker/<stack>/data` (and `./snapshots`, `./config`, etc.) stay on host disk. Bind preserved through container recreate.
|
||||
- **Backup pipeline** — `scripts/books-vds-backup-daily-kreknin/run.sh` rsync paths unchanged. ES `path.repo=/snapshots` bind preserved → snapshot repo `kreknin` continues to write.
|
||||
- **Traefik labels** — propagate automatically. Routes for `imgproxy.kzntsv.site` / `minio.kzntsv.site` / `elasticsearch.kzntsv.site` re-attach as containers come up (docker socket events).
|
||||
- **Network** — все 6 stacks join external `proxy` network as before. No re-configuration.
|
||||
- **Compose file on disk** — `/usr/docker/<stack>/docker-compose.yml` remains as reference. Дата-stamp backup `docker-compose.yml.bak-pre-portainer-migration-2026-05-25` создан per stack.
|
||||
|
||||
## What changed
|
||||
|
||||
- `working_dir` label: было `/usr/docker/<stack>` (ad-hoc), стало `/data/compose/<id>/` (Portainer-managed). Compose files в `/usr/docker/<stack>/` теперь reference-only.
|
||||
- Lifecycle management: edit / restart / view logs — всё через Portainer UI или API. Editing `/usr/docker/<stack>/docker-compose.yml` напрямую больше не имеет эффекта на runtime (Portainer владеет compose state в `/data/compose/<id>/docker-compose.yml`).
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- [[portainer-stack-management-vds]] — source pattern (vds-kzntsv).
|
||||
- [[books-vds]] — host factsheet (updated 2026-05-25).
|
||||
- [[books-vds-backup-daily-kreknin-2026-05-25]] — backup pipeline stand-up; depends on bind paths preserved through this migration.
|
||||
- `scripts/books-vds-portainer-migration/{migrate.sh,README.md}` — implementation.
|
||||
@@ -3,6 +3,7 @@ title: Portainer stack management on vds-kzntsv — canonical pattern
|
||||
status: live
|
||||
tags: [vds, portainer, docker-compose, ops]
|
||||
related: [[vds-kzntsv]], [[mssql-on-vds]], [[minio-imgproxy-on-vds]]
|
||||
updated: 2026-06-17
|
||||
---
|
||||
|
||||
# Portainer stack management on vds-kzntsv
|
||||
@@ -67,6 +68,22 @@ curl -ksS -X POST "$PORTAINER_URL/api/stacks/create/standalone/string?endpointId
|
||||
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" --data "$PAYLOAD"
|
||||
```
|
||||
|
||||
## Convention: `mem_limit` на app-стеках (обязательно)
|
||||
|
||||
**Каждый app/site-стек несёт `mem_limit`.** Без него cgroup-`limit` = вся память хоста (12.9 GiB) →
|
||||
одна течь в контейнере может съесть весь бокс, а не упереться в свой потолок. С `restart: unless-stopped`
|
||||
упор в лимит = OOM-kill контейнера + авто-подъём (изоляция вместо каскада на всю машину).
|
||||
|
||||
- **standalone-стек (не swarm) → ключ `mem_limit`**, НЕ `deploy.resources.limits.memory` (тот игнорится вне swarm).
|
||||
- Значение = ~2.5–5× наблюдаемого пика. snolla-сайты (Node SSR, baseline ~100–200 MB) → **`512m`**.
|
||||
Прочерк-пример: owncloud oCIS → `1g`.
|
||||
- Ставить в момент создания/правки стека — не откладывать. Проверка постфактум: `ops.docker.stats <name>`
|
||||
поле `limit` == заданному (512m = 536870912), не 13890813952.
|
||||
- **Синхронизировать обе копии**: боевой стек в Portainer (PUT) И source-of-truth compose в
|
||||
`admin/host-stacks/vds-kzntsv/<img>.compose.yml`. Иначе следующий redeploy из гита откатит лимит.
|
||||
|
||||
Применено 2026-07-05 ко всему тиражу snolla (labtools.ru/17, emspb/18, labtools.pro/19, tandemmebel/20, kupimknigi/21) → все `512m`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
1. **`env_file: .env` ломает Portainer string-mode** — Portainer не материализует `.env` файл в `/data/compose/<id>/`. Compose pull fails: `env file /data/compose/<id>/.env not found`. Fix: `sed '/^\s*env_file:/d'` + передавать env через API `env` array.
|
||||
@@ -77,6 +94,30 @@ curl -ksS -X POST "$PORTAINER_URL/api/stacks/create/standalone/string?endpointId
|
||||
6. **`com.docker.compose.project.working_dir`** меняется с `/opt/stacks/<name>` (ad-hoc) на `/data/compose/<id>` (Portainer-managed). Сохранённые `/opt/stacks/<name>/.env` остаются на disk — лишь для backup/reference, не используются compose runtime.
|
||||
7. **Volumes preserved across migration** — named volumes (`docker compose down` без `-v`) и binds (`/opt/stacks/<name>/data`) сохраняются. Data layer не теряется при ad-hoc→Portainer переезде.
|
||||
8. **Management plane skip** — `traefik` и `portainer` сами через себя нельзя безопасно recreate (теряется access). Оставлены ad-hoc; их docker-compose.yml в `/opt/stacks/{traefik,portainer}/`. Future: bootstrap script для cold-start ставит их first перед всем остальным.
|
||||
9. **PowerShell 5.1 коррапит кириллицу при round-trip stack-file через API** — `Invoke-RestMethod` на `GET /api/stacks/<id>/file` декодит тело как **ISO-8859-1** (нет `charset` в Content-Type ответа), UTF-8 кириллица в комментариях compose превращается в mojibake (`Боевой`→`Боевой`). Обратный `PUT` шлёт mojibake → docker compose загрузчик падает `yaml: line N: could not find expected ':'` на строке с битым комментарием. На диске исходный файл корректен — портит именно round-trip. **Fix (PS 5.1):** качать байтами `Invoke-WebRequest -OutFile $tmp` → `Get-Content $tmp -Raw -Encoding UTF8 | ConvertFrom-Json`, а тело PUT слать UTF-8-байтами: `$bytes=[Text.Encoding]::UTF8.GetBytes($json); Invoke-RestMethod -Body $bytes -ContentType 'application/json; charset=utf-8'`. (`pilorama98`/pilonuxt redeploy 2026-06-17.)
|
||||
10. **Пустой `env` в PUT-payload** — Portainer ждёт `env: []` (`[]portainer.Pair`). PS `@{env=@()}|ConvertTo-Json` схлопывает пустой массив → `Invalid request payload`. Fix: подставить literal через плейсхолдер — `(... | ConvertTo-Json) -replace '"__ENV__"','[]'`.
|
||||
|
||||
## Stack redeploy (existing stack, новый image tag)
|
||||
|
||||
Перекат уже-managed стека на новый тег образа (НЕ создание). Применялось для `pilonuxt` (stack Id 16) на `redeploy-pilonuxt-gsc-*` 2026-06-16/17. Образ собирается/пушится на workstation (≈400 МБ влезает; multi-GB → собирать на VDS, см. memory `registry-large-push-499-build-on-vds`), затем:
|
||||
|
||||
```powershell
|
||||
# 1. JWT
|
||||
$jwt = (Invoke-RestMethod -Method Post -Uri 'https://portainer.vds.kzntsv.site/api/auth' `
|
||||
-ContentType 'application/json' -Body (@{username='vitya';password='<vds-kzntsv/full-env>'}|ConvertTo-Json)).jwt
|
||||
$h = @{ Authorization = "Bearer $jwt" }
|
||||
# 2. GET текущий compose БАЙТАМИ (gotcha #9), подменить тег
|
||||
$tmp = "$env:TEMP\stack.json"
|
||||
Invoke-WebRequest -Headers $h -Uri 'https://portainer.vds.kzntsv.site/api/stacks/16/file' -OutFile $tmp
|
||||
$new = ((Get-Content $tmp -Raw -Encoding UTF8 | ConvertFrom-Json).StackFileContent) -replace 'pilonuxt:OLD','pilonuxt:NEW'
|
||||
# 3. PUT с pullImage:true (форсит свежий pull нового тега), env=[] через плейсхолдер, тело UTF-8-байтами
|
||||
$json = (@{stackFileContent=$new; env='__ENV__'; prune=$false; pullImage=$true}|ConvertTo-Json -Depth 10) -replace '"__ENV__"','[]'
|
||||
Invoke-RestMethod -Method Put -Headers $h -ContentType 'application/json; charset=utf-8' `
|
||||
-Body ([Text.Encoding]::UTF8.GetBytes($json)) -Uri 'https://portainer.vds.kzntsv.site/api/stacks/16?endpointId=1'
|
||||
```
|
||||
|
||||
Verify: контейнер на новом образе — `ops.docker.ps name=<stack>` (vds-ops MCP) показывает `image: registry.kzntsv.site/<name>:NEW`. Прод-smoke мимо LAN-DNS воркстейшна — `curl --resolve <host>:443:89.253.255.94` (иначе резолвится локальная копия, см. memory `workstation-lan-dns-serves-local-cms-copy`); краулить реальные nav-страницы + data-endpoint, не 2 роута ([[smoke-crawl-real-pages-not-two-routes]]).
|
||||
**Предусловие pull:** registry.kzntsv.site зарегистрирован в Portainer как Custom registry Id 1 (иначе `pull access denied` / `no basic auth`). См. memory `portainer-vds-needs-registry-registered`.
|
||||
|
||||
## Stack inventory (2026-05-22, Portainer Ids)
|
||||
|
||||
|
||||
36
.wiki/concepts/proxy-debugging-test-the-real-client.md
Normal file
36
.wiki/concepts/proxy-debugging-test-the-real-client.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Отладка прокси — проверять на реальном клиенте, не на своих curl-тестах
|
||||
type: concept
|
||||
tags: [troubleshooting, methodology, proxy, vpn, vless, reality, mtproto, anti-pattern]
|
||||
sources: [../sources/nl-vds-3xui-setup-2026-06-05.md]
|
||||
updated: 2026-06-05
|
||||
---
|
||||
|
||||
# Отладка прокси/VPN: свой короткий тест ≠ опыт пользователя
|
||||
|
||||
## Анти-паттерн (как делать НЕ надо)
|
||||
|
||||
При отладке прокси-узла [`nl-vds-3xui`](../entities/nl-vds-3xui.md) я многократно заявлял «работает» на основании **собственных изолированных тестов**:
|
||||
- `curl -x socks5h://127.0.0.1:PORT https://...` через временный `xray.exe`,
|
||||
- короткое скачивание 10 МБ,
|
||||
- `openssl s_client` к порту.
|
||||
|
||||
Эти тесты **проходили**, а боевые клиенты пользователя (v2rayN на ПК, официальный Telegram, телефон) — **не работали**. Каждое такое «у меня работает» при «у меня не работает» от пользователя разрушало доверие и уводило от причины.
|
||||
|
||||
## Почему короткие тесты врут
|
||||
|
||||
1. **Другой путь.** `curl -x http://127.0.0.1:10808` шёл через системный прокси (v2rayN), а не через тестируемый инбаунд. Или мой отдельный `xray.exe` — это **не** запущенный пользователем v2rayN (тот же core, но другой процесс/конфиг/состояние).
|
||||
2. **Короткое vs устойчивое.** Кратковременный коннект мог проходить, а реальное использование (много соединений, сессия минутами) — рушиться (актив-блокировки по состоянию, ретраи).
|
||||
3. **Камуфляж-путь vs рабочий.** `openssl` к FakeTLS-порту (mtg) проходит, потому что отвечает fallback-релей на маскировочный домен — но это НЕ значит, что реальная MTProto-сессия держится. Аналогично REALITY: неаутентифицированный `openssl` получает cert dest (fallback), а аутентифицированный клиент висит.
|
||||
4. **«Доступен/пинг ОК» в GUI** — пассивная проверка достижимости, не равна работающей сессии.
|
||||
|
||||
## Правило
|
||||
|
||||
- **Источник истины — реальный клиент пользователя на его устройстве/сети, а не мой тест.** Если пользователь говорит «не работает» — это данные; мои зелёные curl'ы их не отменяют.
|
||||
- Когда нельзя воспроизвести на боевом клиенте — **изолировать переменные на реальном пути**: закрыть конфликтующие клиенты (у ПК и телефона один внешний IP — пока Desktop держит прокси, телефон в tcpdump не отличить), смотреть `tcpdump` на сервере по конкретному соединению (дошёл ли SYN, идёт ли рукопожатие, где встаёт, есть ли ответные байты).
|
||||
- **Не заявлять «работает» / «подтверждено», пока не подтверждено на боевом клиенте.** Гипотезу называть гипотезой. (Отдельно ловить overclaim: «РКН режет 443» было выдано за факт без доказательства.)
|
||||
- Не вносить «лечебные» изменения на сервере по недоказанной гипотезе (MSS-clamp по MTU-догадке — **сломал** соединения; пользователь заранее говорил, что дело не в MTU).
|
||||
|
||||
## Где встречалось
|
||||
|
||||
- [`nl-vds-3xui-setup-2026-06-05`](../sources/nl-vds-3xui-setup-2026-06-05.md) — Reality/MTProto «проходили» в моих тестах, не работали у пользователя.
|
||||
100
.wiki/concepts/reality-pq-mldsa65-dest-incompatibility.md
Normal file
100
.wiki/concepts/reality-pq-mldsa65-dest-incompatibility.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: REALITY ML-DSA-65 (PQ) × Akamai-dest несовместимость
|
||||
type: concept
|
||||
tags: [reality, xray, vless, post-quantum, mldsa65, x25519mlkem768, troubleshooting, 3x-ui]
|
||||
sources: [../sources/nl-vds-3xui-setup-2026-06-05.md]
|
||||
updated: 2026-06-05
|
||||
---
|
||||
|
||||
# REALITY + ML-DSA-65 (post-quantum) ломается с не-PQ dest (напр. www.intel.com)
|
||||
|
||||
## Симптом
|
||||
|
||||
Свежий 3x-UI / Xray-инбаунд VLESS+Reality **не коннектится** (клиент молча висит до таймаута, никакой
|
||||
ошибки в GUI), при этом **обычный VLESS на другом порту работает**. На сервере (при debug-логе):
|
||||
|
||||
```
|
||||
transport/internet/tcp: REALITY: processed invalid connection from <ip>: handshake did not complete successfully
|
||||
```
|
||||
|
||||
Клиент крутит ретраи и сдаётся: `proxy/vless/outbound: failed to find an available destination > [EOF]`.
|
||||
|
||||
## Root cause
|
||||
|
||||
Современный 3x-UI (Xray ≥ 25.x, здесь 26.6.1) для нового Reality-инбаунда **по умолчанию включает
|
||||
пост-квантовый режим ML-DSA-65** (`realitySettings.mldsa65Seed` на сервере + `mldsa65Verify` у клиента).
|
||||
В этом режиме ClientHello несёт пост-квантовый key-share **`X25519MLKEM768`** (~1.2 КБ).
|
||||
|
||||
REALITY-сервер релеит ClientHello на `dest`/`target`, чтобы «позаимствовать» его TLS-рукопожатие. Если dest
|
||||
**не поддерживает PQ-группу обмена ключами**, он отвечает **HelloRetryRequest** (просит другую группу) —
|
||||
а borrowed-TLS поток REALITY этого не переживает → хендшейк не достраивается. `www.intel.com` за **Akamai**
|
||||
именно такой: PQ-обмен не поддерживает.
|
||||
|
||||
Ключевой нюанс диагностики: **неаутентифицированный** гость (обычный `openssl s_client`) идёт по
|
||||
fallback-пути REALITY (прозрачный релей на dest) и получает настоящий сертификат intel — поэтому сервер
|
||||
кажется «живым». Ломается только **аутентифицированный** путь, и только в связке с PQ.
|
||||
|
||||
## Изоляционная матрица (replica-пара, Xray 26.6.1 ↔ 26.6.1, debug)
|
||||
|
||||
Полностью подконтрольная пара (свой сервер-инбаунд на 8443 + клиент на socks 10888, ключи выведены из seed):
|
||||
|
||||
| dest/SNI | ML-DSA-65 | Результат |
|
||||
|---|---|---|
|
||||
| `www.intel.com` | **вкл** | ❌ `handshake did not complete` |
|
||||
| `www.intel.com` | выкл | ✅ HTTP 204 |
|
||||
| `www.microsoft.com` | выкл | ✅ 204 |
|
||||
| `www.microsoft.com` | **вкл** | ✅ 204 |
|
||||
| `www.yahoo.com` | выкл | ✅ 204 |
|
||||
|
||||
→ Падает **только** `intel + PQ`. Порознь оба компонента исправны. Версии ядер, X25519-ключи, shortId, flow,
|
||||
UUID, seed↔verify, часы — всё проверено и совпадает; ни один из них не виноват.
|
||||
|
||||
## Fix
|
||||
|
||||
Два варианта (рекомендация — первый):
|
||||
|
||||
1. **Сменить dest на PQ-совместимый** (сохраняет пост-квантовую защиту). На сервере в инбаунде:
|
||||
`realitySettings.target` → `www.microsoft.com:443`, `serverNames` → `["www.microsoft.com"]`; в клиенте —
|
||||
SNI → `www.microsoft.com`. Менять **симметрично** обе стороны. PQ-совместимость dest проверяется
|
||||
тестом из матрицы выше (или: dest должен уметь TLS1.3 hybrid PQ key-exchange `X25519MLKEM768` —
|
||||
Cloudflare/Google/Microsoft умеют, Akamai-фронт обычно нет).
|
||||
2. **Выключить ML-DSA-65** на инбаунде (убрать `mldsa65Seed` / пересоздать инбаунд без PQ) — тогда `intel`
|
||||
работает. Проще, но теряется пост-квантовая стойкость. Брать только если PQ не нужен.
|
||||
|
||||
## Как воспроизвести/проверить dest на PQ (read-only)
|
||||
|
||||
Поднять временную replica-пару `xray run -c` на localhost-портах с `loglevel:debug` и `curl -x socks5h://…`
|
||||
через тоннель — НЕ трогая боевой инбаунд (см. методику в [`nl-vds-3xui`](../entities/nl-vds-3xui.md)).
|
||||
Признак PQ-несовместимого dest: сервер логирует `REALITY: ... handshake did not complete` **только** при
|
||||
включённом seed.
|
||||
|
||||
## Update 2026-06-05: GUI-клиенты вообще не умеют PQ — отключать ML-DSA-65
|
||||
|
||||
Даже после fix dest (intel→microsoft) **реальные клиенты не коннектились** через Reality (v2rayN 7.19.5,
|
||||
телефонные приложения), хотя ручной xray-конфиг с вписанным `mldsa65Verify` работал. Причина: **GUI-клиенты
|
||||
не передают `mldsa65Verify` в конфиг ядра** (v2rayN пишет его пустым — видно в `binConfigs/configTest*.json`).
|
||||
Replica-матрица на noPQ-сервере:
|
||||
|
||||
| Сервер | Клиент | Результат |
|
||||
|---|---|---|
|
||||
| PQ (mldsa65Seed) | без verify (как v2rayN) | ❌ 000 |
|
||||
| noPQ | без verify | ✅ 204 |
|
||||
| noPQ | с verify | ❌ 000 |
|
||||
|
||||
**Вывод:** ML-DSA-65 (post-quantum REALITY) на середину 2026 поддерживается только ручным xray-конфигом; ни
|
||||
v2rayN, ни мобильные клиенты его не отдают. Для рабочего Reality с GUI-клиентами **PQ надо выключать**
|
||||
(убрать `mldsa65Seed` из инбаунда) → обычный Reality X25519, который умеют все. 3x-UI включает PQ по
|
||||
умолчанию на новых инбаундах — это и ломает «из коробки».
|
||||
|
||||
## ⚠️ Caveat: отключение PQ ≠ рабочий Reality (случай nl-vds 2026-06-05)
|
||||
|
||||
Этот разбор корректно объясняет, почему PQ-Reality ломается и почему PQ надо отключать для GUI-клиентов.
|
||||
**Но отключение PQ + dest microsoft НЕ сделало Reality рабочим у реальных клиентов пользователя** (v2rayN
|
||||
на ПК, телефон) — он всё равно не поднимался, при том что plain VLESS на другом порту работал. В
|
||||
изолированных curl/standalone-xray тестах с того же ПК Reality «проходил» — но это [нерепрезентативно](proxy-debugging-test-the-real-client.md).
|
||||
Причина отказа Reality у боевых клиентов в той сети **не установлена** (перенос порта 443→2053 тоже не
|
||||
помог). Не читать этот concept как «снял PQ → Reality работает»: PQ — лишь одна из преград.
|
||||
|
||||
## Где встречалось
|
||||
|
||||
- [`nl-vds-3xui`](../entities/nl-vds-3xui.md) / [session 2026-06-05](../sources/nl-vds-3xui-setup-2026-06-05.md) — инбаунд 443→2053, обнаружено 2026-06-05.
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
title: Recovery architecture — текущая инфраструктура (2026-05-19, attempt 2)
|
||||
type: concept
|
||||
tags: [architecture, current-state, snapshot, recovery]
|
||||
tags: [architecture, current-state, snapshot, recovery, historical]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md, ../sources/iis-host-migration-2026-05-19.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
> ⚠ **ИСТОРИЧЕСКАЯ ЗАПИСЬ — архитектура на 2026-05-19.** С тех пор всё изменилось: CMS переехала на [[../entities/ruvds-iis-host]] (2026-05-24), MSSQL/MinIO на [[../entities/vds-kzntsv]] (2026-05-22), imgproxy на [[../entities/books-vds]] (2026-06-08), [[../entities/windows-recovery-host]] декоммишнен (2026-06-08). Содержимое ниже — только для понимания recovery-сессии 2026-05-19. Содержит broken links на несуществующие страницы (`cms-server-port-leak-fix`, `cms-admin-assets-root-folder-seed`, `webconfig-password-xml-escape`).
|
||||
|
||||
# Recovery Architecture Snapshot
|
||||
|
||||
Снимок production-инфраструктуры на конец **второй (успешной) попытки** миграции 2026-05-19. 14 cms hostnames теперь идут через native IIS на [[windows-recovery-host]] напрямую. VM `snolla-recovery` — **parallel-fallback**, running но больше не на prod-пути (24h+ soak, потом savestate). 2 stayer routes окончательно **disabled** через traefik (host IIS sites живут для прямого доступа).
|
||||
@@ -57,7 +59,7 @@ stostayer.snolla.com / old.stostayer.ru
|
||||
```
|
||||
|
||||
Host IIS sites `stostayer (:8090)` и `stostayer.old (:8091)` **живут** для прямого/локального доступа. Conn-strings:
|
||||
- `stostayer`: `Data Source=www.stostayer.ru,1433`, user `stayer_site`, password XML-escaped см. [[webconfig-password-xml-escape]]
|
||||
- `stostayer`: `Data Source=www.stostayer.ru,1433`, user `stayer_site`, password XML-escaped (web.config, pass-protected)
|
||||
- `stostayer.old`: `Data Source=localhost` (наш MSSQL контейнер)
|
||||
|
||||
## Запущенные docker контейнеры на хосте
|
||||
@@ -147,10 +149,10 @@ ACL: `IIS AppPool\<site>:(OI)(CI)M` рекурсивно на каждом site
|
||||
|
||||
## Известные открытые баги
|
||||
|
||||
1. ~~**X-Forwarded headers** не передаются от traefik в IIS → CMS делает redirect с `:4443` в URL.~~ → **RESOLVED 2026-05-19 вечер** через URL Rewrite 2.1 + `<serverVariables>` rule на host IIS. Также закрыл утечку `:8089` в admin SPA после attempt 2 миграции. См. [[cms-server-port-leak-fix]].
|
||||
1. ~~**X-Forwarded headers** не передаются от traefik в IIS → CMS делает redirect с `:4443` в URL.~~ → **RESOLVED 2026-05-19 вечер** через URL Rewrite 2.1 + `<serverVariables>` rule на host IIS. Также закрыл утечку `:8089` в admin SPA после attempt 2 миграции.
|
||||
2. **rimiz.ru → 404**. CMS-side, не инфра.
|
||||
3. **ics-artmaterials.com → 404**. Аналогично — CMS-side (`www.ics-artmaterials.com → 301 → ics-artmaterials.com → 404`).
|
||||
8. ~~**emspb.snolla.com `/admin/assets/<guid>/getList` → 500 NullReferenceException**.~~ → **RESOLVED 2026-05-19 вечер** через DB seed root AssetsFolder rows для 15 sites без них (включая emspb, pilorama98, и др.). Симптом был НЕ site-specific — общий для всех sites которые никогда не использовали admin assets UI. См. [[cms-admin-assets-root-folder-seed]]. Долгосрочный TODO — null-guard в `AssetsJsonViewModelBuilder.Build` (требует recompile DLL, отложено до восстановления build env).
|
||||
8. ~~**emspb.snolla.com `/admin/assets/<guid>/getList` → 500 NullReferenceException**.~~ → **RESOLVED 2026-05-19 вечер** через DB seed root AssetsFolder rows для 15 sites без них (включая emspb, pilorama98, и др.). Симптом был НЕ site-specific — общий для всех sites которые никогда не использовали admin assets UI. Долгосрочный TODO — null-guard в `AssetsJsonViewModelBuilder.Build` (требует recompile DLL, отложено до восстановления build env).
|
||||
4. **acme.json HTTP-01 renewal** будет фейлить для доменов с DNS не на нашем IP → переезд на DNS-01 через REGRU до истечения сертификатов (~3 месяца).
|
||||
5. **C:\inetpub\logs\** растёт — нужна ротация.
|
||||
6. **docker.sock provider в traefik** не работает — но не блокирует (всё через file-provider). См. [[traefik-on-windows-docker-desktop]] Pitfall 3.
|
||||
|
||||
56
.wiki/concepts/registry-kzntsv-auth-model.md
Normal file
56
.wiki/concepts/registry-kzntsv-auth-model.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: registry.kzntsv.site auth model — standalone registry:2 + htpasswd (НЕ Gitea-packages)
|
||||
type: concept
|
||||
tags: [registry, vds-kzntsv, htpasswd, docker, auth, gotcha]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
updated: 2026-06-18
|
||||
---
|
||||
|
||||
# registry.kzntsv.site — auth model
|
||||
|
||||
`registry.kzntsv.site` — это **отдельный `registry:2.8.3` + Joxit UI** на [`vds-kzntsv`](../entities/vds-kzntsv.md) (fresh-install 2026-05-20, см. [bootstrap](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 3.3). Auth — **htpasswd Basic**:
|
||||
|
||||
```
|
||||
GET https://registry.kzntsv.site/v2/ → 401
|
||||
Www-Authenticate: Basic realm="Registry" ← Basic, не Bearer/token
|
||||
```
|
||||
|
||||
## Это НЕ Gitea package registry
|
||||
|
||||
Частая ошибка (books-сессия 2026-06-18 на ней споткнулась): принять реестр за Gitea container-registry. Следствия неверны:
|
||||
|
||||
- **Нет** robot-токенов, scope `read:package`/`delete:package`, per-package visibility, public-read флага. Это не Gitea — таких понятий тут нет.
|
||||
- htpasswd-auth **бинарный**: любой аутентифицированный юзер = full **pull + push + delete** по всем репо. Per-repo ACL нет (без token-auth-сервера, которого тут нет). «Read-only кред» физически невозможен — максимум изоляции = отдельный htpasswd-юзер на потребителя (отзывается точечно).
|
||||
|
||||
## Юзеры (htpasswd)
|
||||
|
||||
Файл: `/opt/stacks/registry/auth/htpasswd` (на vds-kzntsv, `vitya:vitya` rw — sudo не нужен), mount `:ro` в контейнер.
|
||||
|
||||
| Юзер | Кред | Назначение |
|
||||
|---|---|---|
|
||||
| `vitya` | `pass vds-kzntsv/full-env` (Pryakhin9) | master / push с CI |
|
||||
| `books-ci` | `pass vds-kzntsv/registry-books-ci` | books job-scheduler docker-runner pull (createPickingListPdf и др.), заведён 2026-06-18 |
|
||||
|
||||
## Добавить юзера
|
||||
|
||||
Реестр **hot-reload'ит htpasswd при изменении файла — рестарт не нужен** (проверено 2026-06-18). bcrypt-строку проще всего сгенерить через httpd-образ, пароль — через stdin (не в argv):
|
||||
|
||||
```bash
|
||||
printf '%s' "$PW" | docker run --rm -i httpd:2-alpine htpasswd -niB <user> >> /opt/stacks/registry/auth/htpasswd
|
||||
# затем: docker rmi httpd:2-alpine (не оставлять образ на VDS — диск)
|
||||
```
|
||||
|
||||
Проверка: `curl -u <user>:<pw> -I -H 'Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' https://registry.kzntsv.site/v2/<repo>/manifests/<tag>` → 200. **Важно:** без правильного `Accept` (включая manifest-list + oci) HEAD вернёт 404 даже при существующем теге — это media-type mismatch, не отсутствие образа.
|
||||
|
||||
## GC / cleanup
|
||||
|
||||
Не через Gitea API. Чистка standalone-реестра:
|
||||
- **mark-delete:** registry v2 `DELETE /v2/<repo>/manifests/<digest>` (`REGISTRY_STORAGE_DELETE_ENABLED=true` уже выставлен). Тот же htpasswd-кред (бинарный auth = delete разрешён).
|
||||
- **disk-reclaim:** только `registry garbage-collect` на host'е — см. [`registry-gc-mount-and-modify-flag`](registry-gc-mount-and-modify-flag.md). Это host-cron, не job из приложения. v2 DELETE сам место не возвращает.
|
||||
- Ручная альтернатива — Joxit UI (`DELETE_IMAGES=true`) на `registry-ui.vds.kzntsv.site`.
|
||||
|
||||
## Связь
|
||||
|
||||
- Хост: [`vds-kzntsv`](../entities/vds-kzntsv.md) (registry-строка в hostnames).
|
||||
- Класс ошибки «no basic auth» в Portainer-стеках: тот же бинарный htpasswd → [`portainer-stack-management-vds`](portainer-stack-management-vds.md).
|
||||
- Инцидент при заведении books-ci кред-файла: [`bindmount-config-edit-preserve-mode`](bindmount-config-edit-preserve-mode.md).
|
||||
77
.wiki/concepts/registry-oci-image-index-gc.md
Normal file
77
.wiki/concepts/registry-oci-image-index-gc.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: registry.kzntsv.site — books-* образы это OCI image-index (multi-manifest) → дата живёт в sub-manifest, GC должен спускаться
|
||||
type: concept
|
||||
tags: [registry, vds-kzntsv, oci, buildx, docker, gc, manifest, gotcha]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
related: [registry-kzntsv-auth-model, registry-gc-mount-and-modify-flag, bindmount-config-edit-preserve-mode]
|
||||
updated: 2026-06-18
|
||||
---
|
||||
|
||||
# books-* образы в registry = OCI image-index, не плоский манифест
|
||||
|
||||
books-* образы (собраны `docker buildx`) пушатся в [`registry.kzntsv.site`](registry-kzntsv-auth-model.md) как **OCI image-index** (`application/vnd.oci.image.index.v1+json`), а не как одиночный image-manifest. Это меняет всё, что трогает дату/размер/удаление образа.
|
||||
|
||||
## Структура (факт, books-web:master, 2026-06-18)
|
||||
|
||||
```
|
||||
tag master → OCI image-index (mediaType: ...image.index.v1+json)
|
||||
.config.digest = null ← у index НЕТ top-level config
|
||||
.manifests = [
|
||||
{ mediaType: ...image.manifest.v1+json, platform: {amd64, linux} }, ← реальный образ
|
||||
{ mediaType: ...image.manifest.v1+json, platform: {unknown, unknown}, ← attestation, скипать
|
||||
annotations["vnd.docker.reference.type"] = "attestation-manifest" }
|
||||
]
|
||||
```
|
||||
|
||||
Дата сборки (`.created`) **не на верхнем уровне** — она в config-blob платформенного sub-manifest:
|
||||
```
|
||||
index → .manifests[] где platform.os/arch != "unknown" → <sub>.config.digest → GET /v2/<repo>/blobs/<digest> → .created
|
||||
```
|
||||
Подтверждено: `books-web` amd64-sub `.config.digest=sha256:45249a6…`, blob → `"created":"2026-06-18T08:56:51Z"`. Дата валидная — просто на уровень глубже.
|
||||
|
||||
## Почему это ломает наивный registry-GC (no-op-баг)
|
||||
|
||||
Если GC берёт `.created` из top-level манифеста тега — у image-index его НЕТ (`config.digest=null`), `getCreated` возвращает `null` **для всех** тегов. Если планировщик удаления защищает группы с `created==null` (разумный fail-safe «не удаляю то, что не датировал») — **drop пуст всегда**, `keepLastN` не применяется, реестр не чистится. Симптом в отчёте: по каждой репе `keep = tags − 1, drop = 0` (−1 = `buildcache`-тег, отваливается в errors при резолве).
|
||||
|
||||
Зафиксировано на books `registryGc` dryRun 2026-06-18 (`packages/task-runner/lib/registryV2.js`: `getCreated`→null, `planDeletions` ставит `protected` любой null-dated группе). Auth при этом исправен (books-ci, 401 нет) — баг чисто в обходе manifest-дерева, не в доступе.
|
||||
|
||||
## Dangling image-индексы — реестр уже засорён ими (2026-06-18)
|
||||
|
||||
Бóльшая часть тегов books-* — **dangling index**: тег указывает на живой OCI image-index, но его платформенный sub-manifest при фетче отдаёт `MANIFEST_UNKNOWN`:
|
||||
```json
|
||||
{ "errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown",
|
||||
"detail":{"Name":"books-web","Revision":"sha256:8d2b447a…"}}] }
|
||||
```
|
||||
Sub-manifest (и его слои) физически удалены, а **тегированный родитель-index остался**. Замер books-web 2026-06-18: **19 не-buildcache тегов → 3 датируемых (живых), 16 dangling**.
|
||||
|
||||
**Происхождение:** классическое последствие host-side `registry garbage-collect`, который для multi-arch НЕ следует ссылкам index→child → удаляет детей-манифесты и блобы, оставляя тег-надгробие на верхнем index'е. (См. [`registry-gc-mount-and-modify-flag`](registry-gc-mount-and-modify-flag.md).)
|
||||
|
||||
**Ловушка для date-based GC:** `getCreated` на dangling вернёт `null` (sub недоступен) → планировщик защитит группу как «недатированную» → dangling **никогда не удаляется**, хотя это лучший кандидат на чистку. Логика задом наперёд, если не различать:
|
||||
- **transient resolve error** (5xx/таймаут) → protect, safe;
|
||||
- **`MANIFEST_UNKNOWN` на sub** (dangling) → **eligible for delete** (не protect); даты нет — в хвост сортировки или дроп безусловно, образ уже сломан (`docker pull` по нему падает).
|
||||
|
||||
**freedBytes-нюанс:** удаление dangling-индекса освобождает ~0 байт (слои уже вычищены прежним GC) — это гигиена tag-list + удаление битых pull-целей, не disk-reclaim. Реальное место по keepLastN экономится только на живых датируемых образах.
|
||||
|
||||
## Real-run 2026-06-18: чистка прошла, но dangling-master снесён у 2 репо
|
||||
|
||||
Первый боевой `registryGc {dryRun:false}` (books `master-289c660`, под добро юзера): **63 DELETE, 0 ошибок, ни одного 405** → `REGISTRY_STORAGE_DELETE_ENABLED=true` подтверждён живьём. 61 dangling + 2 живых datable за keepLastN. Теги: web 20→4, task-runner/job-scheduler 17→5, api 8→1, ops-mcp 7→1.
|
||||
|
||||
**Побочка — урок:** у `books-api` и `books-ops-mcp` сам тег `master` указывал на **dangling**-индекс (образа не пересобирались ~3 нед, sub-манифесты вычищены прежним GC). Логика «dangling → drop безусловно» снесла и named-тег → `…/manifests/master` → **404**. Pull был сломан и до прогона (MANIFEST_UNKNOWN), но теперь тег исчез совсем. **Outage'а нет** (контейнеры books-api/bookva-api/books-ops-mcp крутятся с локальных образов), но любое **пересоздание/redeploy** упрётся в 404 до CI-rebuild.
|
||||
|
||||
**Вывод для GC-политики:** **не удалять named-теги (`master`/`latest`) даже dangling** — это deploy-указатель; лучше логировать «нужен rebuild». books закрыли предохранителем (`62d812e`: protectRe `^buildcache$` → `^buildcache$|^master$|^latest$`). Подозрение на keep/drop digest-коллизию — **снято**: `keep=1` у books-api это buildcache (protected, он и остался); группировка по digest через Map делает keep/drop взаимоисключающими, kept-манифест не удаляется.
|
||||
|
||||
**Восстановление снесённого named-тега — re-push локального образа** (не rebuild): контейнер крутится с локального образа на хосте → `docker tag $(docker inspect -f '{{.Image}}' <ctr>) registry/<repo>:master && docker push` (под books-ci с самого VDS, не из дома). Возвращает **плоский single-platform манифест с .config** (pullable, и будущий GC его датирует). Применено 2026-06-18 к books-api + books-ops-mcp → :master снова 200. **Гоча:** `books-api` вне CI-build-матрицы (вынесен под embed-api-into-web) — для него re-push единственный способ вернуть pullable master, rebuild'ом не выйдет.
|
||||
|
||||
## Правила для любого GC/cleanup над этим реестром
|
||||
|
||||
1. **Accept на manifest-fetch** обязан включать и index, и manifest-list, и oci.image.manifest, и docker.manifest.v2 — иначе registry отдаёт не тот тип / 404 (та же media-type-гоча, что для HEAD тега в [`registry-kzntsv-auth-model`](registry-kzntsv-auth-model.md)).
|
||||
2. **Дату/размер тянуть из платформенного sub-manifest**, спустившись через index. Выбор записи: `platform.os != "unknown"` и `vnd.docker.reference.type != "attestation-manifest"`.
|
||||
3. **DELETE — по digest INDEX'а** (digest, на который указывает тег; берётся из `Docker-Content-Digest` HEAD-заголовка тега), НЕ по sub-manifest. Удаление sub осиротит index. Два разных digest — index-digest для DELETE, sub.config для `.created`.
|
||||
4. **Одна «версия» = один index** (включает и платформенный manifest, и attestation). Группировать на удаление по index-digest; attestation удаляется вместе с index. keepLastN считать по образам, не по записям `.manifests`.
|
||||
5. Disk-reclaim после mark-delete — отдельный host-cron `registry garbage-collect`, см. [`registry-gc-mount-and-modify-flag`](registry-gc-mount-and-modify-flag.md). v2 DELETE сам место не возвращает.
|
||||
|
||||
## Связь
|
||||
|
||||
- [`registry-kzntsv-auth-model`](registry-kzntsv-auth-model.md) — auth/htpasswd, юзер books-ci, GC через v2 DELETE.
|
||||
- [`bindmount-config-edit-preserve-mode`](bindmount-config-edit-preserve-mode.md) — как клали кред books-ci в task-runner config (shadow + mode гочи).
|
||||
- Хост: [`books-vds`](../entities/books-vds.md) (task-runner), реестр на [`vds-kzntsv`](../entities/vds-kzntsv.md).
|
||||
@@ -81,6 +81,84 @@ OpenSSH for Windows **не имеет** `-pw` flag (как `plink`/PuTTY) или
|
||||
|
||||
См. также: support-ticket draft в [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) с конкретными suggestions для Rusonyx.
|
||||
|
||||
## 9. Сеть = /18, не /24. Gw в той же subnet
|
||||
|
||||
Rusonyx прописывает VM в **/18** (`netmask 255.255.192.0`), не в /24. Адреса `89.253.192.0 – 89.253.255.255` — это один большой L2 broadcast domain, gateway `89.253.192.1` сидит **внутри** него.
|
||||
|
||||
Каноническая ifupdown-конфигурация (то что прописывает их `start/ipadd` procedure):
|
||||
|
||||
```
|
||||
auto eth0
|
||||
allow-hotplug eth0
|
||||
iface eth0 inet static
|
||||
address 89.253.255.94
|
||||
netmask 255.255.192.0
|
||||
post-up ip ro add 169.254.0.0/16 dev eth0 metric 400
|
||||
post-up ip ro add 89.253.192.1 dev eth0 metric 400
|
||||
post-up ip ro add default via 89.253.192.1 dev eth0 metric 400
|
||||
post-up ip ro add 89.253.192.0/18 via 89.253.192.1 dev eth0 src 89.253.255.94 metric 400
|
||||
post-up ip ro del 89.253.192.0/18 dev eth0 proto kernel scope link src 89.253.255.94
|
||||
```
|
||||
|
||||
Тонкость: их post-up сначала добавляет 89.253.192.0/18 **через gw** (routed), потом удаляет автоматически добавленный kernel-route (directly attached). То есть весь трафик внутри /18 идёт **через gw**, не L2-broadcast — типичный hosting-pattern для изоляции клиентов друг от друга (`proxy-ARP` / routed-on-host).
|
||||
|
||||
Эфемерная статика для recovery (если конфиг утерян):
|
||||
|
||||
```bash
|
||||
sudo ip addr add 89.253.255.94/18 dev eth0 # /18 — gw сам в подсети, link-route не нужен
|
||||
sudo ip route add default via 89.253.192.1 dev eth0
|
||||
```
|
||||
|
||||
Если по какой-то причине /18 не приемлем — fallback с /24 и link-route к gw:
|
||||
|
||||
```bash
|
||||
sudo ip addr add 89.253.255.94/24 dev eth0
|
||||
sudo ip route add 89.253.192.1 dev eth0 # link-scope route к gw
|
||||
sudo ip route add default via 89.253.192.1
|
||||
```
|
||||
|
||||
DNS resolvers: `89.253.252.30`, `89.253.252.31`.
|
||||
|
||||
Как обнаружить gw/netmask для конкретной VM:
|
||||
|
||||
1. **Соседний VPS у того же хостера** — `ip route` + `ip addr` покажут.
|
||||
2. **`/etc/network/interfaces.d/ifcfg-<iface>`** на самой VM (если конфиг present) — provider's canonical setup.
|
||||
3. Cached DHCP lease (если уцелел): `cat /run/systemd/netif/leases/*` → поле `ROUTER=`.
|
||||
4. Активационное письмо при выдаче VPS.
|
||||
5. Support ticket — последний resort.
|
||||
|
||||
Подробный кейс где это пригодилось — [`vds-kzntsv-dhcp-outage-2026-05-28`](vds-kzntsv-dhcp-outage-2026-05-28.md).
|
||||
|
||||
## 10. Network stack: ifupdown (NOT netplan)
|
||||
|
||||
Rusonyx provisioning ожидает **`ifupdown` + `networking` service**, не netplan/systemd-networkd. Их `start/ipadd/ipdel` procedure (вызывается при start VM / IP add-remove / network reset) кладёт `/etc/network/interfaces.d/ifcfg-<iface>` и ребутает networking.
|
||||
|
||||
Header в их `/etc/network/interfaces`:
|
||||
|
||||
```
|
||||
# Autoconfigured by Provider's start/ipadd/ipdel procedure.
|
||||
# !!! DO NOT MANUALLY EDIT OR DELETE THIS FILE !!!
|
||||
```
|
||||
|
||||
**Проблема**: Ubuntu 24.04 base ships netplan + systemd-networkd. cloud-init часто оставляет `/etc/netplan/50-cloud-init.yaml` с `dhcp4: true`. После первого boot оба стека активны параллельно.
|
||||
|
||||
Скорее всего **8 дней работает потому что DHCP сам всё резолвит**. Но если что-то на стороне хостера разорвёт DHCP-binding (rare, но случается — см. инцидент 2026-05-28), **их auto-recovery `start/ipadd` не сможет применить static config**, потому что networkd «держит» eth0. → VM лежит до тикета.
|
||||
|
||||
**Канон setup после bootstrap для Rusonyx Ubuntu VDS:**
|
||||
|
||||
```bash
|
||||
sudo systemctl disable systemd-networkd.service systemd-networkd.socket systemd-networkd-wait-online.service
|
||||
sudo systemctl mask systemd-networkd.service systemd-networkd.socket systemd-networkd-wait-online.service
|
||||
sudo systemctl mask netplan # preventive
|
||||
# leave networking enabled (default)
|
||||
```
|
||||
|
||||
**Безопасно на running system** — `disable`/`mask` без `stop` не убивают текущий процесс. Изменение вступает только при следующем boot.
|
||||
|
||||
**Anti-pattern:** прописывать persistent статику через `/etc/netplan/*.yaml`. Использовать ifupdown (`/etc/network/interfaces.d/*`), а лучше — попросить хостера через тикет «настройте дефолтную конфигурацию» — они через `start/ipadd` пропишут это сами.
|
||||
|
||||
Подробный кейс — [`vds-kzntsv-dhcp-outage-2026-05-28`](vds-kzntsv-dhcp-outage-2026-05-28.md).
|
||||
|
||||
## Когда применять
|
||||
|
||||
- Любой новый Rusonyx VDS (Astra Облако).
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: MoreThenCms admin (RUVDS) — App_Data RX-only после scp-миграции → HTTP 500 на любой записи (delete/upload)
|
||||
status: fixed 2026-07-03 (Modify выдан на весь App_Data)
|
||||
tags: [cms, snolla, morethencms, iis, ruvds, acl, permissions, migration, assets, 500]
|
||||
related: [[galleries-storage-class-local-not-s3]], [[../entities/ruvds-iis-host]], [[../sources/iis-migration-to-ruvds-2026-05-23]]
|
||||
updated: 2026-07-03
|
||||
---
|
||||
|
||||
# App_Data RX-only → 500 на записи из админки
|
||||
|
||||
## Симптом
|
||||
|
||||
`POST /admin/assets/<ownerId>/delete?path=/&filename=<file>` → **HTTP 500**. В IIS-логе
|
||||
(`C:\inetpub\logs\LogFiles\W3SVC1\`, время в **UTC**; сервер MSK=UTC+3) — `sc-status 500
|
||||
sc-substatus 0 sc-win32-status 0` = чистое managed-исключение, не IIS-уровень. В Windows
|
||||
**Application Event Log пусто**, файловых логов CMS нет → ASP.NET глотает исключение default-handler'ом.
|
||||
GET того же URL → 404 (delete только `[HttpPost]`).
|
||||
|
||||
## Root cause — ACL, не код и не MinIO
|
||||
|
||||
App pool `snolla` = **ApplicationPoolIdentity** → процесс бежит как `IIS AppPool\snolla`.
|
||||
После scp-миграции 2026-05-23 bootstrap выдал этому аккаунту на дереве контента **только
|
||||
`(RX)` (Read&Execute)**, без Modify:
|
||||
|
||||
```
|
||||
C:\sites\snolla\App_Data IIS APPPOOL\snolla:(OI)(CI)(RX) ← только чтение
|
||||
...\labtools-price.pdf IIS APPPOOL\snolla:(I)(RX)
|
||||
```
|
||||
|
||||
Провайдер `assets` в `<fileStorageClients>` = `MoreThenCms.FileStorage.Local.AssetsStorage`
|
||||
(**локальный диск**, `App_Data\assets\<ownerId:N>\<storageFilename>`; `basePath=""`) — см.
|
||||
[[galleries-storage-class-local-not-s3]]. Код-путь удаления:
|
||||
`AssetsController.Delete` → `AssetsService.DeleteAsset` (`MoreThenCms/Assets/Services/AssetsService.cs:84`):
|
||||
`FileExists` (RX-чтение ок → true) → **`DeleteFile` = `File.Delete`** под identity с RX →
|
||||
`UnauthorizedAccessException` → необработанное → **500**. DB-delete (строкой ниже) даже не
|
||||
достигается. `RX` не содержит право DELETE, и на родителе нет `DC` (delete-child) → удалять нечем.
|
||||
`BUILTIN\Users:(AD)(WD)` не спасает — виртуальный аккаунт пула в `IIS_IUSRS`, не в `Users`; и AD/WD — создание, не удаление.
|
||||
|
||||
**Дифдиагностика (чем исключены NRE-версии):** в БД `mssql.kzntsv.site` подтверждены обе строки —
|
||||
`Folders` (root-папка owner'а, `Path='/'`, `Discriminator=AssetsFolder`) и `Files`
|
||||
(`StorageClient='assets'`, `StorageFilename=<file>`), поэтому `GetFolderByPath(...).Id` и
|
||||
`asset.Folder` не-null, выполнение доходит именно до `File.Delete`. Единственная падающая операция — физическое удаление.
|
||||
|
||||
**Класс проблемы шире delete:** RX-only убивает ВСЕ Local-записи из админки — upload ассетов,
|
||||
`contentCache`/`uploadCache`/`imageCache`, галереи, watermarks. Всё это классы Local в `App_Data`.
|
||||
|
||||
## Fix (применён 2026-07-03)
|
||||
|
||||
```powershell
|
||||
# на RUVDS IIS (80.64.31.36), от Administrator
|
||||
icacls "C:\sites\snolla\App_Data" /grant "IIS AppPool\snolla:(OI)(CI)(M)" /T /C
|
||||
```
|
||||
|
||||
`(M)`=Modify (включает DELETE+WRITE), `(OI)(CI)`=наследование на файлы+подпапки, `/T`=протянуть
|
||||
на существующие. Результат: **44293 файла, 0 ошибок**. Проверка: на целевом файле теперь
|
||||
`IIS APPPOOL\snolla:(I)(M)`. Рестарт пула не нужен — ACL применяется на лету. Живой delete-тест
|
||||
намеренно НЕ гонялся (удалил бы реальный ассет); доказательство = ACL на точном таргете + протрассированный код-путь.
|
||||
|
||||
## Split-brain: админка ≠ боевой сайт
|
||||
|
||||
Админка MoreThenCms живёт на **RUVDS IIS** и читает/пишет **локальный** `App_Data`. Боевой
|
||||
snolla-app (labtools.ru и др. на [[../entities/vds-kzntsv]]) отдаёт ассеты/галереи из **MinIO**
|
||||
(`minio.kzntsv.site` = books-vds, bucket `assets/<ownerId>/<file>`), см.
|
||||
[[galleries-storage-class-local-not-s3]]. Классы мигрированы в S3 (галереи 12.06, assets 13.06),
|
||||
но **админка на S3 не переключена** → любая правка ассета через админку меняет только локалку IIS
|
||||
и НЕ отражается на боевом сайте (и наоборот). Пока админка не repointed на S3 — правки зеркалить руками в оба места.
|
||||
|
||||
Пример (2026-07-03): замена `labtools-price.pdf` (ownerId `d375c419…`) — новый файл залит
|
||||
**и** в MinIO (`mc cp --attr Content-Type=application/pdf … snm/assets/d375c419…/labtools-price.pdf`,
|
||||
ETag→`6426ddd0…`) **и** на локальный диск IIS (scp, MD5 сверен). Оба слоя = байт-в-байт с исходником.
|
||||
|
||||
## TODO (настоящее закрытие split-brain)
|
||||
|
||||
Переключить админский `<fileStorageClients>`-класс `assets` (и `galleries` и пр.) на S3-провайдер,
|
||||
указывающий на `minio.kzntsv.site` — тогда админка и боевой app работают с одним хранилищем,
|
||||
ручное зеркалирование и ACL на App_Data перестают быть нужны. Это код/конфиг-таска на стороне MoreThenCms.
|
||||
124
.wiki/concepts/snolla-live-prod-inplace-image-bump.md
Normal file
124
.wiki/concepts/snolla-live-prod-inplace-image-bump.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
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]], [[tandemmebel-vds-deploy-runbook]]
|
||||
updated: 2026-07-12
|
||||
---
|
||||
|
||||
# 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).
|
||||
2026-07-12: tandemmebel.ru (стек 20) — in-place bump 0.42.0→0.42.1 поверх cutover'а того же дня.
|
||||
Класс риска = **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, на стороне прога.
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: SNOLLA local .NET admin restore + on.snolla.com VDS migration — design
|
||||
type: concept
|
||||
tags: [snolla, morethencms, iis, admin, minio, vds, migration, on-snolla, design]
|
||||
related: [[../entities/windows-recovery-host]], [[../entities/ruvds-iis-host]], [[../entities/vds-kzntsv]], [[tandemmebel-vds-deploy-runbook]], [[snolla-live-prod-inplace-image-bump]], [[portainer-stack-management-vds]], [[filestorage-s3-provider@MoreThenCms]]
|
||||
updated: 2026-07-20
|
||||
---
|
||||
|
||||
# SNOLLA local .NET admin restore + on.snolla.com → VDS migration
|
||||
|
||||
Две связанные задачи. **Task A** восстанавливает локальный .NET-админ (чтобы редактировать контент всех сайтов тиража + on.snolla.com). **Task B** мигрирует посадочную `on.snolla.com` с RUVDS на VDS как Node snolla-app, реконструируя отсутствующие исходники из боевого сайта + админки. Task A → питает Task B (без рабочего админа не увидеть контент/структуру on.snolla.com).
|
||||
|
||||
## Контекст
|
||||
|
||||
- **Тираж SNOLLA** (5 сайтов) уже на VDS как Node snolla-app `@snollajs/snolla` 0.42.1, читают контент из общей MSSQL `MoreThenCms` на `mssql.kzntsv.site:1433` + ассеты из MinIO `minio.kzntsv.site`. Стеки Portainer: labtools.ru(17), emspb.ru(18), labtools.pro(19), tandemmebel(20), kupimknigi(21). Публичный snolla-app **без админки** (`apps/web` не несёт admin-роутов).
|
||||
- **На RUVDS IIS** (`80.64.31.36`, Windows Server 2025 Core) остались: catch-all IIS-сайт `snolla` (MoreThenCms .NET 4.8, `/admin` для всех `*.snolla.com`) + посадочная `on.snolla.com`. МинIO-поддержка в .NET-админ добавлена (drop-in `MoreThenCms.FileStorage.S3`, см. MoreThenCms `.wiki/concepts/filestorage-s3-provider.md`).
|
||||
- **Локальный админ-хост** = этот рабочий PC (`windows-recovery-host`, DESKTOP-NSEF0UK). IIS-сайт `snolla` + `C:\sites\snolla\` были снесены 2026-06-08 (RUVDS забрал прод-админ). Живой локальный аналог остался: `stostayer`(:8090)/`stostayer.old`(:8091) — тот же движок MoreThenCms, `/admin`, conn → `mssql.kzntsv.site,1433`. Это рабочий шаблон конфига.
|
||||
- **on.snolla.com** — `dbo.Sites`: `SiteId=B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7`, `Alias=on`, `Title="Internal Site"`, `Culture=en`, `Live=1, Production=1`. Лендинг самого SNOLLA. Отдельного репо/исходников нет.
|
||||
|
||||
## Task A — восстановить локальный .NET-админ (catch-all IIS `snolla`)
|
||||
|
||||
### Адресация (confirmed оператором)
|
||||
Catch-all IIS-сайт `snolla`, binding `*:80` (локально — `127.0.0.1:80` или `*:80` без публикации наружу), **один AppPool `snolla`**. Доступ через `hosts`-override: `127.0.0.1 <alias>.snolla.com` на каждый таргет. Multi-tenant routing в CMS по Host-header → siteId из `dbo.Sites`.
|
||||
|
||||
**Адреса админок** = `<Alias>.snolla.com/admin`:
|
||||
|
||||
| Адрес | PrimaryDomain | siteId |
|
||||
|---|---|---|
|
||||
| `tandemmebel.snolla.com/admin` | tandemmebel.ru | 78080707-F6E0-4330-BA30-7922354C2CEF |
|
||||
| `labtools.snolla.com/admin` | labtools.ru | D375C419-D787-4FCC-8934-A3B9DC951006 |
|
||||
| `labtoolspro.snolla.com/admin` | labtools.pro | 663F9410-A6CC-4651-9A5C-62844A313957 |
|
||||
| `emspb.snolla.com/admin` | emspb.ru | 96EBC481-D26A-47BE-B660-13D49E7D0A61 |
|
||||
| `kupimknigi.snolla.com/admin` | kupimknigi.spb.ru | E924A354-0377-4E1E-80C6-2EB0194AA55F |
|
||||
| `on.snolla.com/admin` | on.snolla.com | B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7 |
|
||||
|
||||
Catch-all обслужит `/admin` для любого сайта из DB при добавлении `127.0.0.1 <alias>.snolla.com` в hosts — тираж = 5 + on.snolla.com минимум, при желании все ~60.
|
||||
|
||||
### Источник `C:\sites\snolla\`
|
||||
**Рекомендация — копировать с RUVDS** (`scp`/`rclone`): RUVDS держит `C:\sites\snolla\` (8.66 GB) = текущий прод-админ **с уже применённым MinIO drop-in**. Копируем → получаем админ с MinIO из коробки, без пересборки solution и без ручного патча DLL. Альтернативы (build из MoreThenCms source + применить S3 drop-in вручную; restore с kreknin-backup) дольше и требуют повторной MinIO-настройки.
|
||||
|
||||
### Шаги
|
||||
1. **Verify source (read-only SSH RUVDS, креды `pass show ruvds-iis/full-env`):** `C:\sites\snolla\` есть; `Web.config` несёт `storageType="MoreThenCms.FileStorage.S3.*"` + endpoint `minio.kzntsv.site`; IIS site `snolla` жив. Если MinIO НЕ применён — fallback: build из source + drop-in из `MoreThenCms.FileStorage.S3/deploy/` (3 DLL + `fileStorageClients-s3.sample.config`).
|
||||
2. **Копировать `C:\sites\snolla\`** RUVDS → локально `C:\sites\snolla\` (~8.66 GB; rsync недоступен на Core — `scp -r` или `rclone copy`, resume через rclone).
|
||||
3. **Репойнт conn-string** в локальном `Web.config` → `Data Source=mssql.kzntsv.site,1433;Initial Catalog=MoreThenCms;User ID=<snolla-db-user>;Password=<...>;MultipleActiveResultSets=True;TrustServerCertificate=True` (паттерн из `stostayer.old/Web.config`). DB-креды — из `pass` (MoreThenCms DB user; тот же, что tandemmebel production.json `DB_USER`/`DB_PASSWORD`).
|
||||
4. **MinIO-ключи** в `<fileStorageClients>`: endpoint `https://minio.kzntsv.site`, бакеты `assets`/`galleries`/`themes`, `accessKey`/`secretKey` — из `pass show minio-vds/full-env`. Если в скопированном Web.config ключи плейсхолдеры `__SET_ON_DEPLOY__` — подставить реальные. Кэши (`imageCache`/`uploadCache`/`contentCache`/`sessionFiles`) оставить `Local`.
|
||||
5. **IIS-сайт `snolla`** воссоздать: AppPool `snolla` (.NET v4.0 Integrated, `ApplicationPoolIdentity`), binding `*:80` catch-all (как на проде). Локальный доступ гарантируется FW-блоком inbound 80 + `hosts`-override → 127.0.0.1 (внешние не достучатся). Multi-tenant по Host-header.
|
||||
6. **ACL**: `icacls C:\sites\snolla /grant "IIS AppPool\snolla:(OI)(CI)(M)" /T` (паттерн `snolla-admin-appdata-acl-500`).
|
||||
7. **hosts-override**: добавить `127.0.0.1 tandemmebel.snolla.com labtools.snolla.com labtoolspro.snolla.com emspb.snolla.com kupimknigi.snolla.com on.snolla.com` в `C:\Windows\System32\drivers\etc\hosts` (нужен elevation).
|
||||
8. **URL Rewrite rule** (X-Forwarded-Proto→HTTPS server-vars) — **НЕ ставить** локально (нет traefik; ходим по http, иначе `:port` leak как в `cms-server-port-leak-fix`).
|
||||
9. **Firewall**: НЕ открывать 80/443 inbound — локальный только.
|
||||
10. **Smoke**: `curl -H "Host: tandemmebel.snolla.com" http://127.0.0.1/admin/account/login` → 200 логин-форма; аналогично для on.snolla.com. MinIO: залить тестовый ассет через админку → виден через `minio.kzntsv.site`/фронтом (приёмка #5 drop-in'а).
|
||||
|
||||
### Гочи (специфичные)
|
||||
- **`appSettings/sitePath`** в catch-all `Web.config` → `C:\sites\snolla\` (multi-tenant; siteId резолвится из Host-header, не из appSettings — в отличие от per-site stostayer.old где `siteId` в appSettings).
|
||||
- **machineKey** — оставить из скопированного Web.config (общий для всех tenant'ов админ-сессий).
|
||||
- **`customErrors mode="Off"`** — обязательно (иначе fatal config error после ASP.NET reload, см. `cms-server-port-leak-fix` side-regression).
|
||||
- **MSSQL-коннект с workstation** — уже работает (stostayer.old его использует); `TrustServerCertificate=True` обязательно (mssql.kzntsv.site cert self-signed/не-Microsoft).
|
||||
|
||||
## Task B — on.snolla.com → VDS (Node snolla-app, реконструкция)
|
||||
|
||||
on.snolla.com = MoreThenCms-сайт на RUVDS (посадочная). На VDS (Linux) .NET Framework не бежит → становится Node snolla-app по образцу `victor/tandemmebel.ru`. Контент (DB-строки + ассеты) уже на vds-kzntsv (общая `MoreThenCms` DB + MinIO). Нет репозитория шаблонов+конфига — собираю из боевого сайта + админки.
|
||||
|
||||
### Реконструкция
|
||||
1. **siteId найден**: `B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7` (alias `on`, culture `en`).
|
||||
2. **Новый репо** `victor/on-snolla` (имя уточнить у оператора) — структура как `tandemmebel.ru`: `apps/web/{server.js,index.js,config/,views/,package.json}`, `deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root, healthcheck `/robots.txt`). `apps/web/package.json` пин `@snollajs/snolla` 0.42.1 (parity с тиражом).
|
||||
3. **production.json**: `siteId=B9ECDB50…`, `siteUrl=https://on.snolla.com`, `data.sequelize` → `MoreThenCms` DB `mssql.kzntsv.site:1433` (Catalog `MoreThenCms`), `s3` → `minio.kzntsv.site`, `imgproxy` → `imgproxy.kzntsv.site`, `media` sharp serve-bytes, `mailSettings`. Секреты в runtime-env стека (Portainer), НЕ в образе/гите — как tandemmebel (8 секретов).
|
||||
4. **Liquid-шаблоны (views/)** — реверс-инжиниринг с боевого `https://on.snolla.com/`: для каждой страницы смотрим HTML, сопоставляем с DB-контент-структурой (секции/блоки через локальный админ из Task A), пишем `layout.liquid` + page/partials. Содержимое (текст/картинки) идёт из DB; шаблоны = layout + разметка. Итеративно: пиши шаблон → рендерь на staging → сверяй с боевым HTML (byte/structure parity).
|
||||
5. **Build на VDS** (обход traefik-499), **throwaway-staging `:50XX`**, **completeness-gate** (sitemap parity vs прод-on.snolla.com — page-locs NEW==PROD, self-consistency), **stack** за traefik (`Host(on.snolla.com)`), **DNS-flip** reg.ru→89.253.255.94. По рецепту `tandemmebel-vds-deploy-runbook` (cutover) + `snolla-live-prod-inplace-image-bump` (gate). `mem_limit 512m`.
|
||||
|
||||
### Trade-off реверс-инжиниринга
|
||||
Боевой HTML даёт вёрстку, но Liquid-примитивы snolla (sections/blocks/feeds) надо сопоставить с DB-структурой — итеративно. Риск: сложная посадочная с нестандартными блоками = больше итераций. Митигация: on.snolla.com Title="Internal Site" culture `en` — вероятно простой лендинг, объём шаблонов ограничен.
|
||||
|
||||
## Последовательность и зависимости
|
||||
|
||||
1. **Task A** — восстановить локальный админ (RUVDS→scp→IIS→MinIO→smoke). Блокирует Task B (нужен админ для контент-инспекции).
|
||||
2. **Task B** — через локальный админ + боевой HTML собрать on.snolla.com snolla-app → build на VDS → cutover.
|
||||
|
||||
## Load-bearing допущения (verify в плане)
|
||||
- RUVDS `C:\sites\snolla\` несёт MinIO drop-in (SSH read-only — первый шаг Task A). Если нет — fallback на build+drop-in.
|
||||
- MinIO-ключи в RUVDS Web.config — реальные, не `__SET_ON_DEPLOY__` (иначе подтянуть из `pass`).
|
||||
- MoreThenCms DB-креды (snolla-user) в `pass` — найти путь (кандидаты: `mssql-vds/...`, общий env). SA-доступ есть (`pass show mssql-vds/sa-password`) — но админ использует app-аккаунт, не SA; уточнить у оператора.
|
||||
- on.snolla.com siteId/контент доступны в общей `MoreThenCms` DB (подтверждено: строка в `dbo.Sites` есть).
|
||||
- `hosts`-override + IIS-воссоздание требуют elevation (admin-сессия на workstation).
|
||||
|
||||
## Rollback
|
||||
- **Task A:** удалить IIS-сайт `snolla` + `C:\sites\snolla\` + откатать hosts-записи. Прод на RUVDS не тронут — rollback = просто не пользоваться локальным админом.
|
||||
- **Task B:** образ предыдущего тега в registry (или RUVDS IIS жив для on.snolla.com пока DNS не флипнут — revert DNS→80.64.31.36).
|
||||
|
||||
## Open questions (для оператора)
|
||||
- Имя нового репо для on.snolla.com (`victor/on-snolla`? `victor/on.snolla.com`?).
|
||||
- MoreThenCms DB-креды: app-аккаунт + путь в `pass` (или использовать SA локально / завести отдельный read-write аккаунт).
|
||||
- Включать ли в локальный админ ВСЕ сайты (~60) или только тираж(5)+on.snolla.com — catch-all даёт все бесплатно, решает hosts-список.
|
||||
71
.wiki/concepts/stostayer-admin-minio-config.md
Normal file
71
.wiki/concepts/stostayer-admin-minio-config.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: stostayer .NET admin — MinIO S3 providers config (creds/endpoint/region)
|
||||
type: concept
|
||||
tags: [stostayer, morethencms, iis, admin, minio, s3, config, windows]
|
||||
related: [[../entities/ruvds-iis-host]], [[galleries-storage-class-local-not-s3]], [[snolla-admin-appdata-acl-500-after-scp-migration]], [[minio-imgproxy-on-vds]], [[snolla-local-admin-and-on-snolla-migration-design]]
|
||||
updated: 2026-07-22
|
||||
---
|
||||
|
||||
# stostayer .NET admin — MinIO S3 providers
|
||||
|
||||
Локальная MoreThenCms-админка stostayer (`C:\sites\stostayer\Web.config`, IIS :8090) переведена с битых S3-настроек на рабочий stostayer MinIO. После правки админка и боевой `stostayer-web` (0.3.20, Node snolla-app) читают одно хранилище — ручное зеркалирование в `App_Data` больше не нужно.
|
||||
|
||||
## Контекст
|
||||
|
||||
- Боевой `stostayer-web:0.3.20` (legacy @stostayer/web, Nuxt2) читает ассеты/галереи из **stostayer MinIO**, не из локальной ФС. Поток отдачи: картинки галерей/asset → 302 на `imgproxy.stostayer.ru/imgproxy2` → imgproxy тянет из MinIO → webp; прочие ассеты — стримом из MinIO (см. [[../entities/ruvds-iis-host]] ранбук).
|
||||
- Локальная админка = тот же движок MoreThenCms .NET 4.8, conn → `www.stostayer.ru,1433` (DB `stostayer`, user `stayer_site`). S3 drop-in `MoreThenCms.FileStorage.S3` (+ `AWSSDK.Core`/`AWSSDK.S3`) уже лежал в `bin/` — провайдеры в `Web.config` были вставлены, но не работали.
|
||||
|
||||
## Креды stostayer MinIO — НЕ в pass
|
||||
|
||||
Лежат в репо-конфиге `~/projects/stostayer.new/packages/web/config/default.json` → секция `s3`:
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| accessKeyId | `stayer_minio` (bucket-user) |
|
||||
| secretAccessKey | `cXY>AVoWBv#_V-SZn6Vu6Tzx&!M[%UOod` |
|
||||
| endpoint | `https://minio-api.stostayer.ru` |
|
||||
| region | `us-west-1` (см. ниже — НЕ `local`) |
|
||||
| pathStyle / ssl | `true` / `true` |
|
||||
|
||||
imgproxy: `https://imgproxy.stostayer.ru`, basePath `/imgproxy2`, key+salt — там же в `default.json` → `imgproxy`.
|
||||
|
||||
## Три бага в исходном Web.config (почему "смотрит на локальное")
|
||||
|
||||
1. **Чужие креды.** `accessKey=AKIAJ2YJP72W6ZHCRE6Q` + `secretKey=7o0Q4NjE5GL…` — это **books-vds/snolla MinIO root-ключ** (см. [[minio-imgproxy-on-vds]]). На stostayer MinIO не зарегистрирован → `The Access Key Id you provided does not exist in our records`. Видимо, конфиг копипастом из snolla catch-all admin ([[snolla-local-admin-and-on-snolla-migration-design]], тот юзает books-vds `minio.kzntsv.site`), без замены ключей на stostayer-свои.
|
||||
2. **Не API-порт.** `serviceURL=https://minio.stostayer.ru` → MinIO отвечает `S3 API Requests must be made to API port`. Прод юзает `https://minio-api.stostayer.ru`. Оба хоста резолвятся в один IP `2a03:6f01:1:2::c66b`, но `minio.stostayer.ru` — не S3 API-эндпоинт.
|
||||
3. **Регион.** `authenticationRegion="local"` → SigV4-подпись регионом не совпала → `AmazonS3Exception: The authorization header is malformed; the region is wrong; expecting 'us-west-1'` (HTTP 400) на `GalleriesStorage.EnsureKeyPrefixExists` → `S3DirectoryInfo.ExistsWithBucketCheck` (провайдер галерей падал на ините, в трассе `ToolsController.ImagePreview` → `CreatePreview`). stostayer MinIO настроен на регион `us-west-1`. Прод `default.json` пишет `region:"local"`, но то Node aws-sdk (galleries через imgproxy мимо app-S3; для .NET AWSSDK регион обязан быть `us-west-1`).
|
||||
|
||||
## Fix (2026-07-22)
|
||||
|
||||
9 S3-блоков в `fileStorageClients` (galleries/assets/images/scripts/stylesheets/imageCache/watermarks + inert-настройки contentCache/uploadCache) — 3 глобальные замены в `Web.config`:
|
||||
|
||||
```
|
||||
accessKey: AKIAJ2YJP72W6ZHCRE6Q → stayer_minio
|
||||
secretKey: 7o0Q4NjE5GLkdC48r0oZFEnqddjPNLqtCk+ZEh+S → cXY>AVoWBv#_V-SZn6Vu6Tzx&!M[%UOod (XML-escape: > → >, & → &)
|
||||
serviceURL: https://minio.stostayer.ru → https://minio-api.stostayer.ru
|
||||
authenticationRegion: local → us-west-1
|
||||
```
|
||||
|
||||
Бэкап: `C:\sites\stostayer\Web.config.bak-2026-07-22`. IIS авто-recycle по Web.config-изменению. `contentCache`/`uploadCache` остались `storageType="Local"` (inert S3-настройки поправились, storageType не трогал — кэши локально, по рецепту [[snolla-local-admin-and-on-snolla-migration-design]] шаг 4).
|
||||
|
||||
`bucketName` уже был корректен и не менялся: galleries→`galleries`, assets→`assets`, images/scripts/stylesheets/imageCache/watermarks→`themes`. МинIO-бакеты (mc ListBuckets): `assets, galleries, themes, contentcache, imagescache, maxmind, searchindexes, seo, sessions, uploadcache`. Бакет `assets/9cf0a8e52cf144619fd290606a146d35/` несёт реальные файлы (0001.png…); `ownerId` = siteId `9cf0a8e5-2cf1-4461-9fd2-90606a146d35` без дефисов lowercased.
|
||||
|
||||
## Verify
|
||||
|
||||
- `mc alias set stn_api https://minio-api.stostayer.ru stayer_minio "<secret>"` → `mc ls stn_api/` = 10 бакетов. ⚠ `MC_HOST_…` с URL-encoded секретом в mc-урле давал signature-mismatch — юзать `mc alias set` с явными аргументами.
|
||||
- Админка: `:8090/admin/account/login` → 200, лог `C:\Logs\stostayer\log.YYYYMMDD.txt` чист от S3-ошибок после recycle.
|
||||
- **Acceptance (реальный клиент, не curl — см. [[verify-on-real-client-not-own-curl-tests]]):** логин → Asset Manager / превью галереи — картинки грузятся из MinIO. Подтверждено оператором 2026-07-22.
|
||||
|
||||
## Гочи
|
||||
|
||||
- **Секрет с XML-specials.** В `Web.config` value-атрибуте `>` и `&` обязаны быть escaped (`>`, `&`) — иначе XML не парсится / .NET получает обрезанный секрет. `#`, `[`, `%`, `!` — безопасны в double-quoted XML-атрибуте.
|
||||
- **`minio.stostayer.ru` ≠ API-порт.** Только `minio-api.stostayer.ru`.
|
||||
- **Регион `us-west-1`, не `local`.** Для .NET AWSSDK админки — обязателен; прод Node `local` — отдельная история (imgproxy + env-override).
|
||||
- **Креды не в pass** — единственный источник: `stostayer.new` config. Если репо уедет — креды потеряются; имеет смысл переложить в `pass` (отдельная hardening-таска).
|
||||
- **Кэши Local** — `contentCache`/`uploadCache` намеренно Local (не S3), `imageCache` → S3/themes. Не унифицировано с snolla-admin (там все кэши Local) — оставлено как было.
|
||||
|
||||
## Связанные
|
||||
|
||||
- [[galleries-storage-class-local-not-s3]] — snolla-тираж: galleries/assets Local→S3 миграция, snolla catch-all admin на books-vds MinIO.
|
||||
- [[snolla-admin-appdata-acl-500-after-scp-migration]] — split-brain админка(Local)↔боевой app(MinIO) на RUVDS; та же проблема «админка не переключена на S3».
|
||||
- [[minio-imgproxy-on-vds]] — books-vds MinIO (root-ключ `AKIAJ2YJP72W6ZHCRE6Q`), который ошибочно попал в stostayer Web.config.
|
||||
120
.wiki/concepts/stostayer-web-deploy-runbook.md
Normal file
120
.wiki/concepts/stostayer-web-deploy-runbook.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: stostayer-web — деплой легаси web на прод клиента (runbook)
|
||||
status: live
|
||||
tags: [stostayer, docker, portainer, deployment, nuxt2, esm, ops, rollback]
|
||||
related: [[portainer-stack-management-vds]]
|
||||
updated: 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`). Кода/версий не бампать.
|
||||
|
||||
```bash
|
||||
# в чекауте нужного коммита, .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
|
||||
|
||||
```bash
|
||||
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 после смены egress** — `docker 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 с самого хоста**:
|
||||
|
||||
```bash
|
||||
# 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, без нагрузки на публичный сайт)
|
||||
|
||||
```bash
|
||||
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):
|
||||
```bash
|
||||
# на хосте (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_ESM` — `packages/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).
|
||||
146
.wiki/concepts/tandemmebel-vds-deploy-runbook.md
Normal file
146
.wiki/concepts/tandemmebel-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
title: tandemmebel.ru snolla-app — VDS deploy runbook (cutover + in-place 0.42.1 bump)
|
||||
type: concept
|
||||
tags: [tandemmebel, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, blog-portfolio]
|
||||
related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[snolla-live-prod-inplace-image-bump]], [[labtools-vds-deploy-runbook]], [[labtools.pro-vds-deploy-runbook]], [[emspb-vds-deploy-runbook]]
|
||||
updated: 2026-07-12
|
||||
---
|
||||
|
||||
# tandemmebel.ru → VDS deploy runbook
|
||||
|
||||
Вынос `tandemmebel.ru` (snolla-приложение, `@snollajs/snolla` **0.42.1**, server-side Liquid, **блог-портфолио БЕЗ e-commerce каталога**)
|
||||
с [[../entities/ruvds-iis-host]] (catch-all CMS, 80.64.31.36) в отдельный docker-контейнер на [[../entities/vds-kzntsv]]
|
||||
(89.253.255.94), за traefik. Модель — snolla-app: читает БД-контент MoreThenCms (`mssql.kzntsv.site`)
|
||||
+ ассеты из MinIO (`minio.kzntsv.site`). In-process sharp (resize+data-driven watermark), delivery=serve-bytes,
|
||||
variant-cache бакет в MinIO. imgproxy ИЗ ПУТИ tandem УБРАН (сырой `/imgproxy` → 404 норма).
|
||||
|
||||
Паттерн = [[emspb-vds-deploy-runbook]] / [[labtools.pro-vds-deploy-runbook]]. Отличие: **блог-портфолио** (не каталожный →
|
||||
order-парити секций НЕ применимо — см. 0.42.1 bump ниже). Culture `ru-RU`.
|
||||
|
||||
## Артефакты
|
||||
- **Код:** `victor/tandemmebel.ru` @ `0cd9351` (apps/web, snolla **0.42.1** / core 0.24.1 / liquid 0.10.2 / data 0.14.1).
|
||||
`deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root uid `node`, healthcheck `/robots.txt`).
|
||||
- **Образ:** `registry.kzntsv.site/tandemmebel:0cd9351` (+`:latest`). Имя **`tandemmebel`** (= имя стека/контейнера).
|
||||
Собран НА VDS (обход traefik-499). digest `sha256:f29c187fe6114d6ab3926132f4b3eb09df9b606d448cd8fd873780687cb46b8f`.
|
||||
- **Стек Portainer:** `tandemmebel` (**Id 20**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/tandemmebel.compose.yml`.
|
||||
- **siteId:** `78080707-F6E0-4330-BA30-7922354C2CEF` (non-secret, в `production.json` — НЕ env).
|
||||
- **siteUrl:** `https://www.tandemmebel.ru` (в `production.json`).
|
||||
- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD` — Portainer stack env (переиспользованы verbatim из labtools стека 17). НЕ в образе, НЕ в git.
|
||||
- **mem_limit:** `512m` (guardrail; см. [[portainer-stack-management-vds]] § Convention).
|
||||
|
||||
## Cutover 2026-07-12 (первый вынос на VDS — DNS-gated)
|
||||
Последовательность (LE-критичность соблюдена) — см. `NEXT_SESSION.md`:
|
||||
1. **Verify авторит. NS** — `nslookup tandemmebel.ru ns1/ns2.reg.ru` = оба `89.253.255.94` (apex + www), не только резолвер.
|
||||
2. **GET стек 20** — образ `tandemmebel:ed96b18` (0.42.0, staging cutover-подготовлен), env 8/8, rule staging `Host(tandemmebel.vds.kzntsv.site)`.
|
||||
3. **PUT стек 20** — `Host(tandemmebel.ru) || Host(www.tandemmebel.ru)`, env-preserving, `prune:false, pullImage:false`. Python urllib UTF-8 (НЕ PS Invoke-RestMethod — gotcha кириллицы, см. [[portainer-stack-management-vds]]).
|
||||
4. **Poll** — контейнер healthy, LE-серт issued on first hit: CN=tandemmebel.ru, SAN оба, issuer YR2, until 2026-10-10.
|
||||
5. **Live-smoke С VDS** — GREEN.
|
||||
6. Compose source-of-truth + борд обновлены.
|
||||
|
||||
**Латентный прод-баг починен cutover'ом:** `Gotham-Pro.css` был 0B на RUVDS → теперь 200/4436B.
|
||||
|
||||
## In-place bump 2026-07-12: 0.42.0 → 0.42.1 (order-tag Drop-field fix)
|
||||
По рецепту [[snolla-live-prod-inplace-image-bump]] (поверх cutover'а того же дня).
|
||||
|
||||
### Консюмер-бамп (решён оператором, без dev-source)
|
||||
Тот же блокер-паттерн 2026-07-04 (заявленная snolla-версия ≠ консюмер-пин) — на этот раз решён самим оператором:
|
||||
- `apps/web/package.json:12` — `0.42.0` → `0.42.1`.
|
||||
- `yarn install` в корне монорепы → обновил корневой `yarn.lock` (snolla 0.42.1 / core 0.24.1 / liquid 0.10.2 / data 0.14.1). Dockerfile `yarn install --immutable` — lock обязан совпадать с пином.
|
||||
- commit `0cd9351` + push origin (git.kzntsv.site/victor/tandemmebel), подтверждён `ls-remote`.
|
||||
- Тег образа = sha монорепы `0cd9351`.
|
||||
|
||||
### Build → staging → gate → swap
|
||||
1. **Build на VDS** (archive `0cd9351` → `~/build/tandemmebel` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN -t .../tandemmebel:0cd9351 -t .../tandemmebel:latest . && push`). Guard: `config/default.json` ABSENT в архиве (.dockerignore), пин = 0.42.1.
|
||||
2. **Throwaway-staging :5020** из env живого контейнера (`docker inspect tandemmebel` → `.staging.env`, фильтр `^(HOSTNAME|PATH|NODE_VERSION|YARN_VERSION|HOME)=`). `docker run -d --name tandemmebel-staging --env-file .staging.env -p 127.0.0.1:5020:5000 .../tandemmebel:0cd9351` → healthy. robots.txt+`/`+sitemap 200.
|
||||
3. **Completeness-gate С VDS** — sitemapindex разворот (`expand()`), два чека:
|
||||
- **NEW==PROD locs: 184 = 184 IDENTICAL** (вкл. 0.42.x sitemap-реструктуризацию — parity сохранился).
|
||||
- **self-consistency + content-not-lost** (locs идентичны → один проход): 183×2xx + 1×404.
|
||||
- Единственный 404 `/articles` — **идентичен прод-оракулу** (404 и на live `https://www.tandemmebel.ru/articles`) → benign (пустая секция-без-индекса, не регрессия). Tandemmebel НЕ каталожный → order-парити **не применимо** (0.42.1 order-fix инертен на этой теме — как archive-роуты на 0.40→0.42).
|
||||
- **GREEN = 0 регрессий в 404/5xx.**
|
||||
4. **Боевой swap** — operator-gated, node `put-stack.js` (env-preserving: 8/8 name+value, `prune:false, pullImage:true`):
|
||||
`META=st20.json node put-stack.js 20 1 ed96b18 0cd9351` → PUT 200. Контейнер 0cd9351+healthy за ~8s.
|
||||
5. **Live-smoke** — `https://www.tandemmebel.ru/{robots.txt,/,/projects,/sitemap.xml}` = 200. TLS-серт CN=tandemmebel.ru **не дёрнут** (in-place swap серт не трогает). `docker rm -f tandemmebel-staging`.
|
||||
|
||||
### Rollback
|
||||
- Тег `ed96b18` (0.42.0) в registry (+ `b02ca18`, 0.16.2-staging) → PUT стека 20 назад.
|
||||
- ИЛИ revert DNS → 80.64.31.36 (RUVDS IIS жив, не тронут — rollback всех мигрированных).
|
||||
|
||||
## In-place bump 2026-07-13: 0.42.1 → 0.42.1 (sha 0cd9351 → 8df10ee, template-only)
|
||||
|
||||
Ops-handoff от tandemmebel-сессии (inbox-запрос): убрать FB/Twitter/Google+ share-кнопки
|
||||
(экстремистистская символика РФ), оставить ВК+Одноклассники. Коммит `8df10ee` в `victor/tandemmebel.ru`
|
||||
master, меняет только `apps/web/views/social_buttons.liquid` (1 file, 12 deletions), snolla pin 0.42.1
|
||||
НЕ менялся → in-place bump на той же 0.42.1 (не консюмер-бамп).
|
||||
|
||||
### Предсборочная верификация (поймала расхождение)
|
||||
Записка от tandemmebel-сессии утверждала: «сейчас живой на стеке 20 — `ed96b18`/0.42.0». Фактически
|
||||
на проде крутился **`0cd9351`/0.42.1** (in-place bump 0.42.0→0.42.1 был 2026-07-12). Без проверки это
|
||||
не повлияло бы (см. ниже), но вслепую строить нельзя. Проверки С VDS + gitea API (токен `gitea/admin-token`):
|
||||
- `docker inspect tandemmebel` → `registry.kzntsv.site/tandemmebel:0cd9351`, healthy, started 2026-07-12.
|
||||
- gitea `compare/0cd9351...8df10ee` → `total_commits:1`: фикс ровно один коммит поверх 0.42.1 (правильная база).
|
||||
- `git show 8df10ee:apps/web/package.json` → `@snollajs/snolla: 0.42.1` (pin не менялся); yarn.lock snolla 0.42.1 /
|
||||
core 0.24.1 / liquid 0.10.2 / data 0.14.1 — идентично live 0cd9351.
|
||||
- `git show 8df10ee --stat` → только `apps/web/views/social_buttons.liquid` (modified).
|
||||
- 8df10ee full sha `8df10eedcba9979ac91dce2025b2ff827d60e808`, master HEAD = 8df10ee.
|
||||
|
||||
Если бы 8df10ee был на базе 0.42.0 (172 locs) → completeness-gate поймал бы регрессию 184→172. Он на 0.42.1 →
|
||||
parity 184=184.
|
||||
|
||||
### Build → staging → gate → swap
|
||||
1. **Build на VDS** из чистого архива `8df10ee` (gitea API `/archive/<fullsha>.tar.gz`, token- auth `oauth2:`-
|
||||
НЕ работает, `Authorization: token`-header на API endpoints работает; git clone по HTTPS фейлится даже
|
||||
с oauth2:token — gitea admin-token = API-only, не git-transport). Extract `--strip-components=1` в
|
||||
`~/build/tandemmebel-8df10ee`. `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN=<books-ci JWT>
|
||||
-t registry.kzntsv.site/tandemmebel:8df10ee -t .../tandemmebel:latest .` → digest
|
||||
`sha256:30a7e5ab82f2bf371042f1b5fa0cd7ac5ceda71379d1da59e148dee8166bd560`, layer-cache hit (package.json+
|
||||
yarn.lock идентичны 0cd9351), push OK.
|
||||
2. **Throwaway-staging :5020** из env живого контейнера (`docker inspect tandemmebel` → `.staging.env`,
|
||||
фильтр `^(HOSTNAME|PATH|NODE_VERSION|YARN_VERSION|HOME|PORT|NODE_ENV)=`). `docker run -d --name
|
||||
tandemmebel-staging --env-file .staging.env -p 127.0.0.1:5020:5000 .../tandemmebel:8df10ee` → healthy 8s.
|
||||
3. **Completeness-gate С VDS** — sitemapindex разворот. **Важно:** staging sitemapindex содержит АБСОЛЮТНЫЕ
|
||||
prod-URLs (siteUrl=prod в production.json) → sub-sitemaps надо фетчить со staging ПО ПУТЯМ
|
||||
(`http://127.0.0.1:5020/sitemap-pages-1.xml`), а не следовать абсолютным URL (иначе сравнишь PROD==PROD).
|
||||
- NEW==PROD locs: **184 = 184 IDENTICAL** (0 prod-only, 0 new-only).
|
||||
- self-consistency: 183×200 + 1×404 (`/articles` benign parity staging==prod).
|
||||
- share-block: staging vk+ok / fb-tw-gp=0 vs prod-before (0cd915 live ещё имел) vk+ok+fb+tw+gp —
|
||||
фикс убирает ровно лишнее, остальное не трогает.
|
||||
- GREEN = 0 регрессий.
|
||||
4. **Боевой swap** — Portainer JWT auth (`POST /api/auth` vitya/Pryakhin9-VDS-2026; API-key `ptr_*` даёт 401,
|
||||
workaround = JWT). GET `/api/stacks/20` (env 8/8) + `/api/stacks/20/file` (compose). Python urllib UTF-8
|
||||
(НЕ PS Invoke-RestMethod — gotcha кириллицы #9). Замена `tandemmebel:0cd9351`→`8df10ee` в StackFileContent
|
||||
(ровно 1 замена, traefik rule line не тронута). PUT `/api/stacks/20?endpointId=1` `{stackFileContent,
|
||||
env: <8 preserved>, prune:false, pullImage:true}` → HTTP 200. Контейнер 8df10ee+healthy ~8s.
|
||||
5. **Live-smoke С VDS** — robots/`/`/sitemap 200, /articles 404 parity, sitemap 184 locs, 4 share-block
|
||||
страницы (project-post ×2 `/projects/2011/...`, `/furniture/bedrooms`, `/furniture/kitchens/classic`) все
|
||||
200 → **vk+ok на месте, fb/tw/gp=0**. TLS-серт CN=tandemmebel.ru (LE YR2, until 2026-10-10) **не дёрнут**.
|
||||
`docker rm -f tandemmebel-staging`.
|
||||
|
||||
### Rollback (8df10ee)
|
||||
- **Образный (предпочт):** PUT стека 20 назад на `0cd9351` (0.42.1, {% order %} fix) — template-only фикс,
|
||||
откатится чисто. Тег в registry.
|
||||
- `ed96b18` (0.42.0) / `b02ca18` — в registry (ed96b18 стёрт с VDS при disk-cleanup 2026-07-13, `pullImage:true`
|
||||
дотянет из registry).
|
||||
- DNS: revert reg.ru → 80.64.31.36 (RUVDS IIS жив, не тронут).
|
||||
|
||||
### Hygiene-заметка
|
||||
Portainer stack 20 file несёт устаревшие STAGING-комменты (строки 16-17 «Домен: STAGING», 53
|
||||
«STAGING-RULE. Cutover: после flip DNS сменить на Host(...)») — но сама `traefik...rule` строка (55) уже LIVE
|
||||
(`Host(tandemmebel.ru) || Host(www.tandemmebel.ru)`, совпадает с label'ом живого контейнера). Косметика,
|
||||
функционально нейтральна. При деплое 8df10ee обновлён только image-line коммент; шапочные STAGING-комменты
|
||||
не трогались (минимальное изменение). Source-of-truth compose `host-stacks/vds-kzntsv/tandemmebel.compose.yml`
|
||||
актуализирован полностью (8df10ee + LIVE-комменты + история bump'ов). При следующем full-sync можно выровнять
|
||||
Portainer file под git-source.
|
||||
|
||||
## Гочи (специфичные)
|
||||
- **Блокер-паттерн консюмер-пина** — snolla-репо релизит версию, но консюмер-пин в tandemmebel-репо отстаёт. На 0.42.0 (2026-07-04) ждали dev-source; на 0.42.1 (2026-07-12) оператор сделал сам. Всегда byte-verify пин+yarn.lock+`ls-remote` перед build.
|
||||
- **`yarn install --immutable`** в Dockerfile — корневой `yarn.lock` обязан быть снапшотом целевого пина, иначе build падает на checksum-mismatch. Бамп = package.json + `yarn install` (обновляет lock) + commit обоих.
|
||||
- **0.42.x sitemap-реструктуризация** — parity сверять по **page-locs** (развёрнутый sitemapindex), НЕ по именам под-sitemap'ов. 184 тут (было 172 на 0.42.0 — выросло, но NEW==PROD).
|
||||
- **`/articles` 404 benign** — loc из sitemap отдаёт 404, если прод-оракул идентичен → не дефект (см. рецепт § Completeness-gate).
|
||||
- Staging-порт `5020` — проверять `docker ps | grep 127.0.0.1:50` перед bind (stale throwaway от прошлого тиража).
|
||||
- Smoke гнать **С VDS** (воркстейшн ловит LAN-DNS-перехват прод-доменов — см. memory `workstation-lan-dns-serves-local-cms-copy`).
|
||||
- build-secret (VERDACCIO_TOKEN в ARG/ENV build-стадии) — известный follow-up, НЕ блокер (runtime-стадия отдельная, токена в финальном образе нет).
|
||||
|
||||
## Тираж snolla 0.42.1 — статус
|
||||
Все 5 snolla-сайтов на VDS:
|
||||
- labtools.ru (17), emspb.ru (18), labtools.pro (19) — 0.42.1 in-place bump 2026-07-05.
|
||||
- kupimknigi (21), **tandemmebel (20)** — 0.42.0 cutover, tandemmebel in-place bump до 0.42.1 2026-07-12. (kupimknigi остался на 0.42.0 по решению оператора — см. `NEXT_SESSION.md`.)
|
||||
@@ -10,6 +10,8 @@ updated: 2026-05-24
|
||||
|
||||
Recipe для one-shot переноса Let's Encrypt certs из traefik `acme.json` (single-file ACME store) в IIS на Windows host с SNI multi-binding'ами. Использован при миграции [[../entities/ruvds-iis-host]] 2026-05-23 для 14 LE certs → 25 HTTPS hostnames bound via SNI. **Это не renewal pipeline**, а bootstrap-перенос — для long-term renewal recommend `win-acme` standalone на IIS host'е (см. footnote).
|
||||
|
||||
> **Update 2026-06-05 — SUPERSEDED для renewal'а.** Этот ручной PFX-перенос больше не на критическом пути: на RUVDS поднят постоянный self-renewing win-acme HTTP-01 pipeline — см. [[winacme-iis-owin-catchall-http01]]. RUVDS больше не зависит от домашнего traefik по сертификатам. Этот recipe оставлен как reference для bootstrap-сценария / если HTTP-01 недоступен.
|
||||
|
||||
## Когда применять
|
||||
|
||||
- Migration: traefik-on-A → IIS-on-B, certs нужны до того как ACME-validation с нового host'а станет возможной (DNS ещё указывает на старый host, HTTP-01 challenge bounce'нется).
|
||||
|
||||
217
.wiki/concepts/vds-kzntsv-dhcp-outage-2026-05-28.md
Normal file
217
.wiki/concepts/vds-kzntsv-dhcp-outage-2026-05-28.md
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
title: vds-kzntsv network-stack mismatch 2026-05-28 — postmortem + recovery procedure
|
||||
type: concept
|
||||
tags: [vds, rusonyx, network, dhcp, outage, postmortem, recovery, runbook, ifupdown, netplan]
|
||||
sources: [../sources/vds-kzntsv-incident-2026-05-28.md]
|
||||
updated: 2026-05-28
|
||||
---
|
||||
|
||||
# vds-kzntsv network-stack mismatch 2026-05-28
|
||||
|
||||
Постмортем + переиспользуемый runbook. Конкретный инцидент: [[../entities/vds-kzntsv]] (89.253.255.94) был недоступен снаружи примерно с **28 мая 2026, ~05:45–08:30 MSK** (~2.5 часа активного outage), плюс ещё ~5.5 часов работы на эфемерной статике до окончательного fix хостером в ~13:52 MSK. Подробная хроника — в [[../sources/vds-kzntsv-incident-2026-05-28]].
|
||||
|
||||
**Root cause (по итогу resolution хостером):** наш Ubuntu 24.04 VDS с момента активации (2026-05-20) работал на **двух параллельно активных** сетевых стеках — `netplan` + `systemd-networkd` (от cloud-init + наши манипуляции) **поверх** `ifupdown` + `networking` (provider's expected stack). Их provisioning кладёт конфиг в `/etc/network/interfaces.d/ifcfg-eth0` через `start/ipadd/ipdel` procedure, и ожидает что ifupdown подхватит при boot. networkd на eth0 этому мешает. 8 дней «работало», потому что networkd сам получал DHCP lease и обходил проблему. Когда DHCP-binding слетел на их стороне (28.05 ~05:45-08:00 MSK), их `start/ipadd` procedure не могла восстановить IP через ifupdown, потому что eth0 был «захвачен» systemd-networkd → VM осталась без IP. Окончательное решение: mask `netplan` + `systemd-networkd`, переход на чистый `networking` stack.
|
||||
|
||||
## Симптомокартина
|
||||
|
||||
| Симптом | Где видно |
|
||||
|---|---|
|
||||
| SSH с любого хоста — `No route to host` / `Destination Host Unreachable` | workstation, sibling-VPS [[../entities/books-vds]] |
|
||||
| Hoster gateway `89.253.192.40` отвечает `Destination Host Unreachable` на ARP | внешний `traceroute` падает на 3-м hop |
|
||||
| VM **жива** (через VNC), процессы и docker контейнеры работают | Rusonyx panel → Консоль |
|
||||
| `ip -br link` показывает `eth0 UP LOWER_UP` с MAC | физический NIC ОК |
|
||||
| `ip a show eth0` — **только** `inet6 fe80::*/64 scope link`, **нет IPv4** | DHCP lease отсутствует |
|
||||
| `ip route` — только docker-сети, **нет default** | следствие отсутствия IPv4 |
|
||||
| `networkctl status eth0` → `State: degraded (configuring)`, `Online state: online` | networkd ждёт ответ от DHCP |
|
||||
| `journalctl -u systemd-networkd -b` — повторяющийся `eth0: DHCPv6 lease lost`, **никаких** DHCPv4 offer'ов | DHCP-сервер хостера не отвечает |
|
||||
| LLDP-сосед видим: `Connected To: hw80.rusonyx.ru on port fe:54:00:9c:63:01` | L2 со свитчем здоров → проблема выше |
|
||||
|
||||
**Анти-симптомы** (что отметает наши конфиг-ошибки):
|
||||
|
||||
- netplan не менялся днями (см. `git log` если бы был — у нас не versioned, но `stat /etc/netplan/*.yaml`).
|
||||
- Тот же шаблон netplan работает на соседнем VPS [[../entities/books-vds]] (89.253.255.133) у того же хостера.
|
||||
- Reboot VM не помогает (uptime после ребута 1 час, IP так и не получен).
|
||||
- Daily backup завершился успешно за ~3 часа до обнаружения отказа → не «давно сломалось», именно острый отказ.
|
||||
|
||||
## Диагностика — алгоритм
|
||||
|
||||
```dot
|
||||
digraph diag {
|
||||
"Внешний SSH/ping не доходит" [shape=diamond];
|
||||
"Открыть VNC → ip -br link" [shape=box];
|
||||
"Link UP?" [shape=diamond];
|
||||
"Без NIC — driver/hardware issue" [shape=box];
|
||||
"ip a show <iface>" [shape=box];
|
||||
"Есть IPv4?" [shape=diamond];
|
||||
"Сеть работает иначе" [shape=box];
|
||||
"networkctl status <iface>" [shape=box];
|
||||
"LLDP видит свитч?" [shape=diamond];
|
||||
"Cable/L2 issue — тикет в DC" [shape=box];
|
||||
"Соседний VPS у того же хостера работает?" [shape=diamond];
|
||||
"Глобальный outage — ждать или тикет" [shape=box];
|
||||
"DHCP server проблема — статика + тикет" [shape=doublecircle];
|
||||
|
||||
"Внешний SSH/ping не доходит" -> "Открыть VNC → ip -br link";
|
||||
"Открыть VNC → ip -br link" -> "Link UP?";
|
||||
"Link UP?" -> "Без NIC — driver/hardware issue" [label="нет"];
|
||||
"Link UP?" -> "ip a show <iface>" [label="да"];
|
||||
"ip a show <iface>" -> "Есть IPv4?";
|
||||
"Есть IPv4?" -> "Сеть работает иначе" [label="да"];
|
||||
"Есть IPv4?" -> "networkctl status <iface>" [label="нет"];
|
||||
"networkctl status <iface>" -> "LLDP видит свитч?";
|
||||
"LLDP видит свитч?" -> "Cable/L2 issue — тикет в DC" [label="нет"];
|
||||
"LLDP видит свитч?" -> "Соседний VPS у того же хостера работает?" [label="да"];
|
||||
"Соседний VPS у того же хостера работает?" -> "Глобальный outage — ждать или тикет" [label="нет"];
|
||||
"Соседний VPS у того же хостера работает?" -> "DHCP server проблема — статика + тикет" [label="да"];
|
||||
}
|
||||
```
|
||||
|
||||
Ключевые команды для каждого узла:
|
||||
|
||||
```bash
|
||||
ip -br link # NIC + state одной строкой на интерфейс
|
||||
ip a show eth0 # есть ли IPv4
|
||||
sudo networkctl status eth0 # State + LLDP-сосед (Connected To: ...)
|
||||
sudo journalctl -u systemd-networkd -b | grep -iE "dhcp|discover|offer|nack" | tail -20
|
||||
sudo ls /run/systemd/netif/leases/ # есть ли свежий lease (пусто — нет ни одного)
|
||||
```
|
||||
|
||||
## Recovery procedure (эфемерная статика — для немедленного восстановления)
|
||||
|
||||
Используется когда сеть лежит **прямо сейчас** и нужно срочно поднять доступ. Маска **/18** (как прописывает хостер) — это самая чистая форма; gw в той же подсети, дополнительный link-route не требуется:
|
||||
|
||||
```bash
|
||||
sudo ip addr add 89.253.255.94/18 dev eth0
|
||||
sudo ip route add default via 89.253.192.1 dev eth0
|
||||
```
|
||||
|
||||
DNS (если `/etc/resolv.conf` не сохранился):
|
||||
|
||||
```bash
|
||||
sudo bash -c 'echo -e "nameserver 89.253.252.30\nnameserver 89.253.252.31" > /etc/resolv.conf'
|
||||
```
|
||||
|
||||
Альтернативная форма с **/24** (если по какой-то причине /18 не приемлемо — например провайдер изменил routing) — тогда нужны 3 команды с link-route к gw:
|
||||
|
||||
```bash
|
||||
sudo ip addr add 89.253.255.94/24 dev eth0
|
||||
sudo ip route add 89.253.192.1 dev eth0
|
||||
sudo ip route add default via 89.253.192.1 dev eth0
|
||||
```
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
ip a show eth0 # inet 89.253.255.94/...
|
||||
ip route # default via 89.253.192.1 dev eth0
|
||||
curl -s -m 5 https://api.ipify.org # должно вернуть 89.253.255.94
|
||||
```
|
||||
|
||||
**Эфемерность:** прописано через `ip` — на reboot стирается. Для persistence см. ниже § «Permanent fix через ifupdown».
|
||||
|
||||
## Permanent fix — через ifupdown stack (то что прописывает хостер)
|
||||
|
||||
**Это и есть financial state после resolution 2026-05-28**. Это конфигурация **которую хостер ставит/восстанавливает через свой `start/ipadd` procedure** — наш `ifcfg-eth0` это просто слепок того что они кладут.
|
||||
|
||||
### /etc/network/interfaces.d/ifcfg-eth0
|
||||
|
||||
```bash
|
||||
# Autoconfigured by Provider's start/ipadd/ipdel procedure.
|
||||
# !!! DO NOT MANUALLY EDIT OR DELETE THIS FILE !!!
|
||||
|
||||
auto eth0
|
||||
allow-hotplug eth0
|
||||
iface eth0 inet static
|
||||
address 89.253.255.94
|
||||
netmask 255.255.192.0
|
||||
post-up ip ro add 169.254.0.0/16 dev eth0 metric 400
|
||||
post-up ip ro add 89.253.192.1 dev eth0 metric 400
|
||||
post-up ip ro add default via 89.253.192.1 dev eth0 metric 400
|
||||
post-up ip ro add 89.253.192.0/18 via 89.253.192.1 dev eth0 src 89.253.255.94 metric 400
|
||||
post-up ip ro del 89.253.192.0/18 dev eth0 proto kernel scope link src 89.253.255.94
|
||||
```
|
||||
|
||||
**Не редактировать руками** — хостер перезапишет через `start/ipadd` при следующих manipulations с VM (resize, network reset). Если нужны user-specific routes — отдельный файл рядом, **не** редактировать ifcfg-eth0.
|
||||
|
||||
### Прибить netplan + systemd-networkd
|
||||
|
||||
```bash
|
||||
sudo systemctl disable systemd-networkd.service systemd-networkd.socket systemd-networkd-wait-online.service
|
||||
sudo systemctl mask systemd-networkd.service systemd-networkd.socket systemd-networkd-wait-online.service
|
||||
sudo systemctl mask netplan # preventive — netplan.service unit not exist в Ubuntu, но mask не повредит
|
||||
|
||||
# verify
|
||||
systemctl is-enabled netplan systemd-networkd networking
|
||||
# expected: masked, masked, enabled
|
||||
```
|
||||
|
||||
**Безопасно на running system** — `disable`/`mask` блокируют только автозапуск на **следующий boot**. Текущий networkd-процесс продолжает работать до явного `stop` или reboot. SSH-сессии и L3-конфиг (IP/маршруты) переживают.
|
||||
|
||||
### Запросить provider-side reset
|
||||
|
||||
Если IP по DHCP не приходит и `/etc/network/interfaces.d/ifcfg-eth0` отсутствует или пустой — тикет в Rusonyx с запросом «настроить дефолтную конфигурацию сети». Они через свою `start/ipadd` procedure пропишут `ifcfg-eth0` и ребутнут VM. После ребута сеть поднимается на ifupdown.
|
||||
|
||||
## Kernel cmdline (для понимания почему `eth0`, а не `ens3`)
|
||||
|
||||
Rusonyx прописывает в GRUB `net.ifnames=0 biosdevname=0` — это **выключает** systemd predictable interface naming. Поэтому интерфейс всегда `eth0`, не `ens3`/`enp0s3` (хотя udev знает их как altnames для `ip a show`). Менять эти флаги в cmdline нельзя — провайдерский `ifcfg-eth0` рассчитан именно на `eth0`.
|
||||
|
||||
## Где взять gateway если он неизвестен
|
||||
|
||||
У Rusonyx и многих российских VDS-хостингов используется **proxy-ARP / unnumbered routing** — gateway сидит в `/18` или `/19` надсети, а наш `/24` гостирует через неё. **Не угадывать `.1` в своей /24** — это не сработает.
|
||||
|
||||
Источники:
|
||||
|
||||
1. **Соседний VPS у того же хостера** — самый надёжный. `ssh sibling 'ip route'` покажет `default via X.X.X.X dev eth0`. Так мы и сделали в этот раз — взяли gw из [[../entities/books-vds]] (89.253.192.1).
|
||||
2. **Cached lease** на самой VM (если уцелел): `cat /run/systemd/netif/leases/*`, `cat /var/lib/dhcp/dhclient.leases` — поля `ROUTER=`, `DNS=`. В этот раз не уцелело.
|
||||
3. **Тикет в support** — последний resort, тратит часы.
|
||||
4. **Активационное письмо** при выдаче VPS — обычно содержит netmask + gw. Но если VPS активирован годы назад — письмо может быть утеряно.
|
||||
|
||||
## Root cause (по итогам resolution)
|
||||
|
||||
**Корректировка относительно нашей первоначальной гипотезы** (которую мы вынесли в тикет на основе наших observations):
|
||||
|
||||
Первоначально мы атрибутировали отказ полностью инфраструктуре хостера (DHCP server / MAC binding). Это было **частично** верно — что-то в их инфре действительно потеряло binding нашей VM 28.05 в окне ~05:45-08:15 MSK, и их `start/ipadd` procedure пыталась его восстановить. Но **их procedure не смогла справиться** потому что наш сетевой стек был сконфигурирован неправильно:
|
||||
|
||||
- Поверх их ожидаемого `ifupdown + networking` (под который рассчитан их `start/ipadd`) у нас активны параллельно `netplan + systemd-networkd` (от cloud-init).
|
||||
- `systemd-networkd` «захватывал» eth0 на уровне routing и DHCP, не давая ifupdown'у нормально применить provider's static config.
|
||||
- 8 дней с этим conflict'ом всё работало, потому что networkd сам получал DHCP lease (там где их `ipadd` не успевал) и сеть жила.
|
||||
- Когда что-то на стороне хостера разорвало DHCP binding — networkd не смог получить новый lease (потому что binding с их стороны был broken), а ifupdown с готовой статикой не смог взять интерфейс (потому что networkd ещё держал его в configuring state).
|
||||
|
||||
**Окончательное решение** хостера (после нашего тикета): `mask netplan + systemd-networkd`, reboot — после boot ifupdown подхватил `ifcfg-eth0` сразу, конфликта нет, сеть работает.
|
||||
|
||||
**Атрибуция ответственности:**
|
||||
|
||||
- **Их сторона:** что-то в их сетевой инфре спровоцировало DHCP-binding loss 28.05 (точная причина не указана в ответе — отписка через сутки). Это **trigger** инцидента.
|
||||
- **Наша сторона:** мы наколхозили `netplan + systemd-networkd` поверх их `ifupdown`-stack (отчасти от cloud-init, отчасти при попытках восстановить сеть через VNC). Эта конфигурация **усугубила** инцидент: их auto-recovery не смогла сработать. Это **prolonging factor**.
|
||||
|
||||
Без conflict'а — инцидент длился бы минуты до auto-recovery. С conflict'ом — длился до нашего тикета + ручного fix хостером.
|
||||
|
||||
## Lessons learned (revised)
|
||||
|
||||
1. **Уважать provider's expected stack.** Rusonyx — ifupdown. Не накатывать netplan поверх, даже если он «по умолчанию в Ubuntu 24.04». Чистый netplan был бы менее проблемным чем mixed setup, но безопаснее всего — то что хостер использует.
|
||||
2. **Hostprovider auto-recovery — реальная вещь.** У Rusonyx есть `start/ipadd` procedure которая инжектит IP при reboot/start. Если она работает корректно — DHCP lease loss восстанавливается прозрачно для клиента. Не мешать её работе.
|
||||
3. **Соsed (sibling VPS) — главный диагностический инструмент.** [[../entities/books-vds]] подтвердил отсутствие глобального outage и дал референс на сетевые параметры (gw, DNS resolvers).
|
||||
4. **Готовый тикет-шаблон важен.** Хороший initial-ticket с конкретикой (LLDP, journalctl, ping/traceroute) сократил процесс. Хостер увидел что мы не зовём «оно не работает» а уже сделали половину RCA.
|
||||
5. **Когда хостер просит configuration change — оценить trade-off.** Их framing «у вас неправильный стек» был корректен (по последствиям), но мы первоначально подозревали что они переводят разговор от своего RCA. Reality — **и то и другое одновременно**: их инфра glitched, наш стек не дал auto-recovery, оба фактора важны.
|
||||
6. **`disable + mask` без `stop` — безопасно на running system.** Не падает SSH, не падает IP. Изменение вступает только на reboot. Это полезный паттерн для «приготовить состояние к чистому reboot, не трогая текущее».
|
||||
|
||||
## Anti-pattern
|
||||
|
||||
**Не делать**: persistent статику через `netplan` для VDS у Rusonyx. Это противоречит их provisioning. Использовать только ifupdown (`/etc/network/interfaces.d/*`), а лучше — попросить их `start/ipadd` procedure сделать это (тикет).
|
||||
|
||||
## Lessons learned
|
||||
|
||||
1. **DHCP — единая точка отказа на стороне хостера.** Если у них падает DHCP-привязка нашей VM, и lease истёк — мы выпадаем независимо от состояния нашего стека. Mitigation: статика в netplan, **но** это требует договорённости с хостером (они могут autosocket pool — статический IP перестанет работать когда они ребалансируют).
|
||||
2. **Soсед — главный диагностический инструмент.** [[../entities/books-vds]] был ключом: подтвердил что L2/глобальный outage отсутствует, дал точный gw для статики. **Иметь второй VPS у того же хостера** — это **бесплатный** мониторинг сетевой инфры хостера.
|
||||
3. **VNC console — обязательный backup-канал.** Без [[rusonyx-vps-onboarding-quirks]] §1-2 и доступа в VNC мы бы не смогли ни диагностировать, ни поднять статику.
|
||||
4. **Activation-email с netmask+gw — критичный артефакт.** Хранить (в pass-store / pinned email folder).
|
||||
5. **Cached lease file** — мог бы сократить recovery на 2-3 шага. Не уцелел. Можно периодически бэкапить `/run/systemd/netif/leases/` куда-то persistent? — minor follow-up, vs стоимость не оправдан.
|
||||
6. **Daily backup-лог = upper-bound оценки downtime.** Если backup прошёл успешно в 05:45 — VDS был жив. Это поможет хостеру в их RCA найти точное окно.
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- [[../entities/vds-kzntsv]] — host details, обновлено note про gw в /18.
|
||||
- [[../entities/books-vds]] — sibling VPS у того же хостера, использован для gw discovery.
|
||||
- [[rusonyx-vps-onboarding-quirks]] — VNC + sshd quirks; quirk #9 добавлен про proxy-ARP gw layout.
|
||||
- [[../sources/vds-kzntsv-incident-2026-05-28]] — полная хроника сессии.
|
||||
- `.tasks/STATUS.md` § disk-89-followups — follow-up GC после recovery.
|
||||
@@ -1,15 +1,17 @@
|
||||
---
|
||||
title: books shared VDS — SSH access audit + retained keys
|
||||
title: vds-kzntsv — SSH access audit + retained keys
|
||||
status: live
|
||||
tags: [vds, ssh, audit, books, tenant-split, ops]
|
||||
related: [[vds-kzntsv]], [[../sources/vds-kzntsv-bootstrap-2026-05-20]]
|
||||
tags: [vds, ssh, audit, infra, ops]
|
||||
related: [[../entities/vds-kzntsv]], [[../sources/vds-kzntsv-bootstrap-2026-05-20]]
|
||||
---
|
||||
|
||||
# books shared VDS — SSH access (post-audit 2026-05-24)
|
||||
# vds-kzntsv — SSH access (post-audit 2026-05-24)
|
||||
|
||||
Snapshot SSH-доступа к `vds-kzntsv` (89.253.255.94 / vds.kzntsv.site), на котором сейчас живёт shared `books`-инсталляция (Slovo + Bookva в одном compose-стеке, до physical split Фазы 4 в [`tenant-split`](https://git.kzntsv.site/victor/books/src/branch/master/.wiki/concepts/tenant-split.md)).
|
||||
Snapshot SSH-доступа к [[../entities/vds-kzntsv]] (`89.253.255.94 / vds.kzntsv.site`) — наш infra-VDS (gitea, registry, ntfy, mssql, minio, owncloud, board-viewer).
|
||||
|
||||
Создан 2026-05-24 в результате таски `[books-ssh-audit-shared-vds]` 🟢 (см. spec в Фазе 3 §SSH-аудит).
|
||||
**Корректировка 2026-05-25:** до этой даты документ ошибочно утверждал что на vds-kzntsv живёт shared books-инсталляция. **Это неверно** — books-стек на отдельном клиентском VDS `89.253.255.133` (см. [[../entities/books-vds]]). vds-kzntsv хостит только нашу infra. Аудит ниже относится к vds-kzntsv SSH (наш infra-host), не к books VDS.
|
||||
|
||||
Создан 2026-05-24 в результате таски `[books-ssh-audit-shared-vds]` 🟢 — задача была названа в предположении что books на vds-kzntsv; reality по сути проверила infra SSH (что и должно было быть).
|
||||
|
||||
## Retained keys
|
||||
|
||||
@@ -52,16 +54,15 @@ Brute-force protection adequate; нет required action.
|
||||
|
||||
`sudo journalctl -u ssh -u sshd --since="7 days ago" | grep Accepted` показал **только** `vitya from 94.19.247.14` (мой home public IP). Никаких других source-IP — clean.
|
||||
|
||||
## Tenant-split implications (Phase 3 acceptance)
|
||||
## Books tenant-split — implications (исправлено 2026-05-25)
|
||||
|
||||
Spec `tenant-split.md` §«Двухуровневая изоляция пользователей → Infra-level»:
|
||||
> на промежуточной стадии не давать аналитикам Bookva SSH к VDS, только app-level. SSH-аудит до cutover'а.
|
||||
Spec `tenant-split.md` §«Двухуровневая изоляция пользователей → Infra-level» и аудит «не давать аналитикам Bookva SSH к VDS» — **должны были применяться к books VDS, не к vds-kzntsv**. Реальный books VDS = `89.253.255.133` ([[../entities/books-vds]]) — отдельный клиентский сервер. SSH-аудит на books VDS — отдельная follow-up задача.
|
||||
|
||||
**Verified clean** as of 2026-05-24 — никаких аналитических SSH-ключей не было выдано. Single-developer setup; analyst access всегда был app-level only. Pre-cutover sigh of relief.
|
||||
vds-kzntsv (этот документ) — infra-VDS, не имеет отношения к books tenants. После Phase 4 books tenant-split:
|
||||
- books VDS (`89.253.255.133`) — текущий shared books → может остаться за Slovo (или Bookva, как пользователь решит).
|
||||
- vds-bookva-new — отдельный VDS у учредителя Bookva. См. `.tasks/books-vds-bookva-bootstrap.md` + `books-dns-cutover-bookva.md`.
|
||||
|
||||
После Phase 4 (physical split на 2 VDS):
|
||||
- vds-kzntsv (Slovo) — продолжит хостить остальную нашу инфру (gitea, registry, ntfy, mssql, minio, owncloud, board-viewer) + books-slovo стек. SSH = vitya only.
|
||||
- vds-bookva-new (Bookva) — отдельная box, отдельный SSH. Заводить core-dev key (vitya) + опционально formally-named Bookva admin.
|
||||
vds-kzntsv остаётся infra-host независимо от tenant-split исхода.
|
||||
|
||||
## How to add a new key (process)
|
||||
|
||||
77
.wiki/concepts/verdaccio-restore-packument-desync.md
Normal file
77
.wiki/concepts/verdaccio-restore-packument-desync.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Verdaccio disaster-restore desync — tarball present, packument stale → EEXISTS 409
|
||||
type: concept
|
||||
tags: [verdaccio, npm, storage, restore, backup, 409, gotcha, postmortem]
|
||||
sources: []
|
||||
updated: 2026-06-11
|
||||
---
|
||||
|
||||
# Verdaccio disaster-restore desync — 409 при publish свежей версии
|
||||
|
||||
## Симптом
|
||||
|
||||
После disaster-restore verdaccio из бэкапа: `npm/yarn publish <pkg>@<newver>` падает с
|
||||
**`409 "this package is already present"`**, хотя в реестре этой версии (по GET) нет —
|
||||
`dist-tags.latest` показывает древнюю версию. На некоторых пакетах GET вообще `404`.
|
||||
|
||||
## Root cause: packument ↔ tarball рассинхрон
|
||||
|
||||
Restore вернул в `/storage/.../<pkg>/`:
|
||||
- **все `.tgz` тарболлы** — до текущих версий включительно (`data-0.9.0.tgz` физически на диске);
|
||||
- но **`package.json` (packument-индекс) — древний** (`versions{}` знает только 0.1.x/0.2.x,
|
||||
`dist-tags.latest=0.2.0`), потому что метадата восстановилась из старого слоя бэкапа
|
||||
либо была затёрта сорванным `unpublish`.
|
||||
|
||||
Verdaccio publish-flow:
|
||||
1. merge новой версии в packument → **OK** (версии нет в индексе);
|
||||
2. запись `.tgz` на диск → файл **уже существует** (с restore) → `EEXISTS` →
|
||||
surface как **`409 this package is already present`**.
|
||||
|
||||
Лог-отпечаток:
|
||||
```
|
||||
PUT /@snollajs%2Fdata
|
||||
http <-- 409 ... error: this package is already present
|
||||
```
|
||||
|
||||
### Подвид: пустой packument после сорванного unpublish
|
||||
|
||||
`unpublish` пишет пустой packument-stub (161 байт, `versions:[]`, `_rev:""`), затем пытается
|
||||
`rmdir` каталог → **`ENOTEMPTY`** (тарболлы на месте) → `500`. Итог: GET `404` (пустой индекс),
|
||||
publish `409` (тарболл цел). Каталог «полупустой»: индекс мёртв, бинарники живы.
|
||||
|
||||
## Fix: хирургия, НЕ `rm -rf` каталога
|
||||
|
||||
`rm -rf /storage/.../<pkg>` снимет 409, но **уничтожит всю историю версий** (тарболлы —
|
||||
locally-published, `_distfiles:{}` ⇒ единственная копия, см. [[verdaccio-prune-semantics]]).
|
||||
|
||||
Правильно — удалить **только коллизирующий целевой `.tgz`** под версию, которую republish'ат:
|
||||
|
||||
```bash
|
||||
# backup сначала
|
||||
sudo tar czf /tmp/verdaccio-rescue-$(date -u +%Y%m%dT%H%M%SZ).tar.gz <pkg-dirs>
|
||||
# снять коллизию — ровно целевой тарболл, остальное не трогать
|
||||
sudo rm /opt/stacks/verdaccio/storage/@scope/<pkg>/<pkg>-<targetver>.tgz
|
||||
# → republish <pkg>@<targetver> из source проходит, packument регенерится корректно
|
||||
```
|
||||
|
||||
Рестарт контейнера **не нужен**: local-storage читает packument/тарболл с диска пер-реквест;
|
||||
packument не трогаем, только убираем лишний `.tgz`, которого в индексе и так нет.
|
||||
|
||||
**Trade-off:** промежуточные версии (тарболлы на диске, но не в индексе после republish) остаются
|
||||
orphaned — безвредны, не ставятся по pin, GC их не трогает (locally-published). Если нужна полная
|
||||
история в индексе — отдельный скрипт-reconstruct packument из всех on-disk тарболлов
|
||||
(`tar xzO package/package.json` per tgz + shasum/integrity) — heavier, по умолчанию не делаем.
|
||||
|
||||
## Применено 2026-06-11
|
||||
|
||||
`@snollajs/{data@0.9.0, mailer@0.7.4, numbering@0.7.4, content-api@0.8.1}` — удалены 4
|
||||
коллизирующих тарболла, бэкап `/tmp/verdaccio-snollajs-rescue-20260611T140801Z.tar.gz` на
|
||||
[[../entities/vds-kzntsv]]. Republish делегирован владельцу пакетов (сессия snolla).
|
||||
Контекст: восстановление verdaccio после wipe (см. также [[verdaccio-token-lifecycle]] — другой
|
||||
класс отказа того же restore-эпизода).
|
||||
|
||||
## Связанные
|
||||
|
||||
- [[verdaccio-prune-semantics]] — locally-published vs proxied, почему `rm` тарболла = permanent loss
|
||||
- [[verdaccio-token-lifecycle]] — restart trap / ephemeral secret (соседний симптом того же restore)
|
||||
- [[../entities/vds-kzntsv]] — хост
|
||||
116
.wiki/concepts/verdaccio-token-lifecycle.md
Normal file
116
.wiki/concepts/verdaccio-token-lifecycle.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Verdaccio token lifecycle — restart trap + JWT fix
|
||||
type: concept
|
||||
tags: [verdaccio, npm, pnpm, yarn, auth, jwt, gotcha, postmortem]
|
||||
sources: []
|
||||
updated: 2026-06-11
|
||||
---
|
||||
|
||||
# Verdaccio token lifecycle — restart trap + JWT fix
|
||||
|
||||
## Root cause: auto-generated secret
|
||||
|
||||
Verdaccio без явного `secret:` в конфиге генерирует случайный секрет **при каждом запуске**. Все токены (legacy opaque и JWT) подписаны этим секретом — перезапуск контейнера = мгновенная инвалидация всех выданных токенов у всех клиентов.
|
||||
|
||||
**Симптом:** `401 Unauthorized` после рестарта VDS/контейнера, несмотря на то что токен в `.npmrc` визуально «есть».
|
||||
|
||||
**Фикс (применён 2026-06-11):** явный `secret:` в `config.yaml` → токены переживают рестарты.
|
||||
|
||||
## `max_users: -1` + pnpm login → 409
|
||||
|
||||
Стандартный workaround «перелогинься» (`pnpm login --registry ...`) не работает при `max_users: -1`.
|
||||
|
||||
Verdaccio htpasswd plugin v6 проверяет `max_users` **первым** в `adduser()`, до проверки существования пользователя:
|
||||
|
||||
```
|
||||
if (max_users === -1) → return 409 "user registration disabled"
|
||||
```
|
||||
|
||||
`pnpm login` отправляет `PUT /-/user/org.couchdb.user:<name>` — это registration-path. Verdaccio блокирует даже для существующих юзеров, даже если пароль верный.
|
||||
|
||||
Изменение `max_users` на положительное число решает проблему, но открывает регистрацию всем — нежелательно.
|
||||
|
||||
## Правильный способ получить токен при `max_users: -1`
|
||||
|
||||
Использовать **web-UI login endpoint**, который не проходит через `adduser()`:
|
||||
|
||||
```powershell
|
||||
$body = '{"username":"vitya","password":"..."}'
|
||||
$r = Invoke-RestMethod -Uri "https://verdaccio.kzntsv.site/-/verdaccio/sec/login" `
|
||||
-Method POST -Body $body -ContentType "application/json"
|
||||
$r.token # JWT, 7d expiry
|
||||
```
|
||||
|
||||
Полученный токен — в `~/.npmrc`:
|
||||
|
||||
```ini
|
||||
//verdaccio.kzntsv.site/:_authToken=eyJ...
|
||||
```
|
||||
|
||||
## Конфиг-фикс (2026-06-11)
|
||||
|
||||
Добавлено в `/opt/stacks/verdaccio/config/config.yaml` на [vds-kzntsv](../entities/vds-kzntsv.md):
|
||||
|
||||
```yaml
|
||||
secret: <32-byte-hex-permanent>
|
||||
|
||||
security:
|
||||
api:
|
||||
jwt:
|
||||
sign:
|
||||
expiresIn: 30d
|
||||
notBefore: 0
|
||||
web:
|
||||
sign:
|
||||
expiresIn: 7d
|
||||
verify: {}
|
||||
```
|
||||
|
||||
До фикса: нет `secret` и нет `security` → legacy tokens, привязанные к ephemeral secret.
|
||||
После: web-UI и npm-API используют один JWT формат, подписанный постоянным секретом.
|
||||
|
||||
> **Примечание:** `security.api.jwt` меняет формат API-токенов с legacy opaque (`5a3X...==`) на JWT (`eyJ...`). Старые legacy-токены в `.npmrc` у всех клиентов перестают работать после смены конфига — нужен refresh.
|
||||
|
||||
## Срок жизни токенов
|
||||
|
||||
| Endpoint | Expiry | Используется для |
|
||||
|---|---|---|
|
||||
| `/-/verdaccio/sec/login` | 7d (web.sign) | Ручной refresh, агенты |
|
||||
| npm API (pnpm/yarn/npm login) | 30d (api.jwt.sign) | После решения `max_users` проблемы |
|
||||
|
||||
## Клиенты: где хранится токен
|
||||
|
||||
### npm / pnpm / yarn classic (1.x)
|
||||
|
||||
Читают `~/.npmrc` (user-level) или `.npmrc` в корне проекта:
|
||||
|
||||
```ini
|
||||
//verdaccio.kzntsv.site/:_authToken=eyJ...
|
||||
@snollajs:registry=https://verdaccio.kzntsv.site/
|
||||
```
|
||||
|
||||
Yarn classic читает тот же `~/.npmrc` — update токена в одном файле покрывает всех.
|
||||
|
||||
### yarn berry (2.x+)
|
||||
|
||||
Не читает `~/.npmrc`. Конфиг в `.yarnrc.yml`:
|
||||
|
||||
```yaml
|
||||
npmRegistries:
|
||||
"https://verdaccio.kzntsv.site":
|
||||
npmAuthToken: "eyJ..."
|
||||
npmAlwaysAuth: true
|
||||
|
||||
npmScopes:
|
||||
snollajs:
|
||||
npmRegistryServer: "https://verdaccio.kzntsv.site"
|
||||
```
|
||||
|
||||
Refresh токена = обновить `npmAuthToken` в `.yarnrc.yml` каждого проекта, либо использовать `yarn npm login --scope snollajs` (тоже обходит `max_users: -1` через web UI path).
|
||||
|
||||
## Связанные страницы
|
||||
|
||||
- **Общая вики:** `concepts/verdaccio-token-usage` (репо `projects-wiki`) — практический runbook аутентификации: два гейта, конфиг клиента, диагностика 401, stale-parent-process trap, чеклист. Эта страница — детальный postmortem про *почему*; runbook — про *как пользоваться*.
|
||||
- [verdaccio-prune-semantics](verdaccio-prune-semantics.md) — storage layout и GC
|
||||
- [yarn-npm-minimal-age-gate](yarn-npm-minimal-age-gate.md) — гейт #2: YN0016 на свежих версиях (Yarn ≥4.16), независим от auth
|
||||
- [vds-kzntsv](../entities/vds-kzntsv.md) — хост где крутится инстанс
|
||||
94
.wiki/concepts/winacme-iis-owin-catchall-http01.md
Normal file
94
.wiki/concepts/winacme-iis-owin-catchall-http01.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: win-acme HTTP-01 авто-renewal на IIS под OWIN-catch-all CMS
|
||||
status: live
|
||||
tags: [windows, iis, ruvds, letsencrypt, win-acme, acme, http-01, owin, cms, ops]
|
||||
related: [[ruvds-iis-host]], [[traefik-acme-json-to-iis-cert-import]], [[windows-server-2025-core-bootstrap]], [[iis-migration-2026-05-19-postmortem]]
|
||||
---
|
||||
|
||||
# win-acme HTTP-01 авто-renewal на IIS под OWIN-catch-all CMS
|
||||
|
||||
Постоянный self-renewing LE-pipeline на RUVDS IIS-хосте ([[ruvds-iis-host]]) для сайта `snolla`
|
||||
(25 hostname через SNI). **Заменяет** ручной метод [[traefik-acme-json-to-iis-cert-import]]
|
||||
(экспорт PFX из домашнего traefik `acme.json` + ручной `Import-PfxCertificate`) — тот делал
|
||||
RUVDS зависимым от домашней машины по сертификатам и требовал ручного продления.
|
||||
|
||||
Поднято 2026-06-05, после full DNS cutover на RUVDS (HTTP-01 challenge до cutover'а отскакивал
|
||||
на windows-source — LE не мог достучаться).
|
||||
|
||||
## Что развёрнуто
|
||||
|
||||
- **win-acme v2.2.9** в `C:\win-acme\` (скачан с GitHub releases, `x64.pluggable`).
|
||||
- Один **SAN-cert на 25 hostname** в store `WebHosting`, установлен во все 25 `*:443:<host>` SNI-биндинга.
|
||||
- **Scheduled task `win-acme-renew-snolla`** — daily 09:00 + random delay 4h, runs as **SYSTEM**,
|
||||
`wacs.exe --renew --baseuri https://acme-v02.api.letsencrypt.org/`. Renewal due 55 дней до expiry.
|
||||
- Idempotent setup-скрипт: `scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1` (фазы `download`/`app`/`probe`/`task`/`verify`).
|
||||
|
||||
## Главный gotcha: OWIN-catch-all жрёт `/.well-known/acme-challenge/`
|
||||
|
||||
Сайт = **MoreThenCms** на OWIN. В корневом `web.config`: `owin:HandleAllRequests=true` +
|
||||
handler `Owin path="*"` + `runAllManagedModulesForAllRequests="true"` → **весь** трафик уходит в
|
||||
managed OWIN-pipeline. HTTP-01 токен (статический файл) отдаётся не статикой, а CMS → `404`/`500`/redirect.
|
||||
LE-валидация падает (`Invalid response ...: 500`).
|
||||
|
||||
Снятие Owin-**handler**'а в дочернем `web.config` НЕ помогает — OWIN перехватывает на уровне
|
||||
**модуля** (HandleAllRequests). Что НЕ сработало:
|
||||
- web.config с `<rewrite><rules><clear/></rules>` — рерайта-то и нет, маршрутит managed-pipeline.
|
||||
- web.config с `<handlers><remove name="Owin"/></handlers>` в обычном (managed) пуле — модуль всё равно жив.
|
||||
|
||||
### Рабочее решение: отдельное IIS-приложение в пуле «No Managed Code»
|
||||
|
||||
`.well-known/acme-challenge` вынесен в **отдельное IIS Application** под сайтом `snolla` с
|
||||
app-pool'ом `acme-challenge`, у которого `managedRuntimeVersion=''` (**No Managed Code**). В таком
|
||||
пуле .NET/OWIN не стартует вообще — токен отдаётся нативным `StaticFileModule`.
|
||||
|
||||
Нюанс: в дочернем web.config всё равно нужно **`<handlers><remove name="Owin"/></handlers>`** —
|
||||
иначе унаследованный managed Owin-handler даёт `500` (managed handler в unmanaged пуле). Плюс
|
||||
extensionless-mime (токены без расширения). Проверенный web.config:
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<handlers><remove name="Owin" /></handlers>
|
||||
<staticContent>
|
||||
<remove fileExtension="." /><mimeMap fileExtension="." mimeType="text/plain" />
|
||||
</staticContent>
|
||||
<modules runAllManagedModulesForAllRequests="false" />
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
Результат: токен отдаётся `200` для **любого** Host — даже для деградировавших CMS-тенантов
|
||||
(`maljarka.tandemmebel.ru`/`rimiz.ru`, которые на app-уровне отдают `502`/`404`), т.к. статика
|
||||
обходит CMS-роутинг.
|
||||
|
||||
### Патч шаблона win-acme
|
||||
|
||||
win-acme при filesystem-валидации **перезаписывает** дочерний web.config своим шаблоном
|
||||
`C:\win-acme\Web_Config.xml` (а при `CleanupFolders=true` потом удаляет токен+web.config). Дефолтный
|
||||
шаблон делает `<staticContent><clear/>` + mime, но **не снимает Owin-handler** → снова `500`.
|
||||
|
||||
Durable-фикс = **пропатчить сам шаблон** `C:\win-acme\Web_Config.xml`, добавив `<remove name="Owin"/>`
|
||||
+ `runAllManagedModulesForAllRequests="false"`. Трекаемая копия: `scripts/iis-migration-to-ruvds/winacme-Web_Config.xml`.
|
||||
Тогда на каждом renewal win-acme сам кладёт рабочий web.config в (постоянное) unmanaged-приложение,
|
||||
валидирует, чистит. Между renewal'ами папка пустая — трафика на неё нет.
|
||||
|
||||
## Процедура де-риска (повторяемо)
|
||||
|
||||
1. `--validationmode http-01 --validation filesystem` локальный probe токена через `localhost` + `Host:`-заголовок (обходит home-DPI middlebox).
|
||||
2. **Staging** через `--baseuri https://acme-staging-v02.api.letsencrypt.org/` (НЕ `--test` — он включает интерактивные промпты, которые вешают non-interactive SSH). pemfiles store + `--installation none` — без правок IIS.
|
||||
3. Прод: `--store certificatestore --installation iis --installationsiteid 1`.
|
||||
|
||||
## Грабли
|
||||
|
||||
- **`--test` ≠ staging-only.** `--test` тащит интерактив («Try in default browser?», «Quit?») → виснет под SSH. Для staging без интерактива — `--baseuri <staging>`.
|
||||
- **pemfiles path должен существовать** заранее, иначе abort до валидации.
|
||||
- **Scheduled task руками, не через win-acme.** `--notaskscheduler` при выпуске; таск создаётся отдельно как SYSTEM (win-acme-овский setup может спросить креды интерактивно). win-acme потом пишет «Scheduled task not configured yet» — косметика, ищет *свой* таск.
|
||||
- win-acme renewal'ы раздельны по `--baseuri` (разные ConfigurationPath): staging-renewal не трогается прод-таском. Staging-renewal всё равно убран для чистоты.
|
||||
|
||||
## Команда боевого выпуска (reference)
|
||||
|
||||
```
|
||||
C:\win-acme\wacs.exe --source iis --siteid 1 --validationmode http-01 --validation filesystem ^
|
||||
--webroot C:\sites\snolla --store certificatestore --installation iis --installationsiteid 1 ^
|
||||
--accepttos --emailaddress <ops-email> --force --notaskscheduler
|
||||
```
|
||||
84
.wiki/concepts/yarn-npm-minimal-age-gate.md
Normal file
84
.wiki/concepts/yarn-npm-minimal-age-gate.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Yarn 4.16 npmMinimalAgeGate — YN0016 "quarantined" на свежих пакетах
|
||||
type: concept
|
||||
tags: [yarn, yarn-berry, verdaccio, npm, supply-chain, gotcha, YN0016, age-gate]
|
||||
sources: []
|
||||
updated: 2026-06-11
|
||||
---
|
||||
|
||||
# Yarn 4.16 `npmMinimalAgeGate` — клиентский «карантин» свежих версий
|
||||
|
||||
## Симптом
|
||||
|
||||
`yarn install` (Yarn Berry **≥4.16**) падает на резолве:
|
||||
|
||||
```
|
||||
@snolla/site-schema-gen@npm:^0.5.0: All versions satisfying "^0.5.0" are quarantined
|
||||
```
|
||||
|
||||
Код ошибки — **YN0016**. Версия физически есть в реестре, `dist-tags.latest` указывает на неё, токен валиден (`whoami` отдаёт юзера) — но yarn её не берёт.
|
||||
|
||||
## Root cause — клиентский age-gate, НЕ сервер
|
||||
|
||||
Yarn 4.16 ввёл настройку **`npmMinimalAgeGate`** (supply-chain мера против свежезалитых вредоносных версий, по следам атак на npm 2025). Из бинаря `~/AppData/Local/node/corepack/v1/yarn/4.16.0/yarn.js`:
|
||||
|
||||
```
|
||||
npmMinimalAgeGate: { type: "DURATION", unit: "m", default: "1d" } # = 1440 минут
|
||||
```
|
||||
|
||||
Фильтр перед резолвом отсеивает версию, если:
|
||||
- у неё нет даты публикации в packument-поле `time`, **или**
|
||||
- `(now − publishDate) < npmMinimalAgeGate`.
|
||||
|
||||
Если после фильтра кандидатов не осталось → `throw YN0016 "...are quarantined"`.
|
||||
|
||||
**Карантин целиком на стороне yarn-клиента.** На сервере verdaccio никакого карантина нет (нет в `config.yaml`, plugins-папка пустая) — серверная проверка пакумента его не видит. Реестр отдаёт версию чисто любому валидному read-токену.
|
||||
|
||||
## Версионная разница (почему «у одних работает»)
|
||||
|
||||
| yarn | `npmMinimalAgeGate` default |
|
||||
|---|---|
|
||||
| ≤ 4.14 (напр. snolla 4.14.1) | `0` — гейта нет |
|
||||
| ≥ 4.16 | `1d` (1440 мин) |
|
||||
|
||||
Проект на 4.14 «просто работает», на 4.16 тот же канон ловит YN0016 первые 24 ч после публикации пакета.
|
||||
|
||||
## Два независимых последовательных гейта
|
||||
|
||||
Важно не путать с auth — это разные стены, в таком порядке:
|
||||
|
||||
1. **auth** — кривой/протухший токен → `YN0041 Invalid authentication` (это 401, см. [[verdaccio-token-lifecycle]]).
|
||||
2. **age-gate** — валидный токен, но версия моложе порога → `YN0016 …quarantined`.
|
||||
|
||||
Решающий признак: если на **валидном** токене (auth прошёл, 401 нет) всё равно падает — это гейт #2, не auth. Token-fix его НЕ снимает.
|
||||
|
||||
## Fix
|
||||
|
||||
```yaml
|
||||
# .yarnrc.yml — глобально:
|
||||
npmMinimalAgeGate: 0
|
||||
```
|
||||
```bash
|
||||
# либо разово, per-invocation (сохраняет защитный дефолт для обычных install'ов):
|
||||
YARN_NPM_MINIMAL_AGE_GATE=0 yarn install
|
||||
```
|
||||
|
||||
### Нюанс: гейт читается ГЛОБАЛЬНО, не per-scope
|
||||
|
||||
В фильтре — `configuration.get("npmMinimalAgeGate")` (глобальный config), не scope-aware lookup. Поэтому `npmMinimalAgeGate` под `npmScopes.@snolla.*` **игнорируется** — нельзя ослабить гейт только для доверенного приватного скоупа. Глобальный `0` снимает age-защиту и с **публичных** npmjs-пакетов, которые verdaccio проксирует через uplink. Это реальный security trade-off — в этом и смысл фичи.
|
||||
|
||||
**Рекомендация:** не коммитить `0` в общий канон бездумно. Для CI / чужих проектов — разовый `YARN_NPM_MINIMAL_AGE_GATE=0` под осознанный install сразу после своей публикации. Для тесного publish→consume dev-loop своих пакетов (напр. pilonuxt) глобальный `0` оправдан — реестр один доверенный, DX важнее. Финальный risk-call — за владельцем экосистемы.
|
||||
|
||||
## Ecosystem heads-up
|
||||
|
||||
Любой потребитель на **yarn ≥4.16**, тянущий свежеопубликованные `@snolla`/`@snollajs` пакеты, будет ловить YN0016 первые 24 ч. Когда snolla-монореп/проекты переедут на 4.16+ — решить, добавлять ли `npmMinimalAgeGate: 0` в snolla-канон `.yarnrc.yml` или держать override on-demand.
|
||||
|
||||
## Применено / источник
|
||||
|
||||
Диагностировано 2026-06-11 совместно с сессией pilonuxt (миграция pnpm→yarn 4.16). Первичная ошибочная гипотеза «карантин = серверный дефект verdaccio» опровергнута: пруф — строка `are quarantined` + `npmMinimalAgeGate` в `yarn.js` 4.16.0, и YN0016 воспроизводится на валидном vitya-токене, снимается только `npmMinimalAgeGate: 0`.
|
||||
|
||||
## Связанные
|
||||
|
||||
- [[verdaccio-token-lifecycle]] — гейт #1 (auth / JWT / 401), соседняя стена
|
||||
- [[verdaccio-restore-packument-desync]] — почему версии вообще пришлось republish (disaster-restore)
|
||||
- [[../entities/vds-kzntsv]] — хост verdaccio
|
||||
150
.wiki/entities/books-vds.md
Normal file
150
.wiki/entities/books-vds.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Books VDS (client server — 89.253.255.133)
|
||||
type: entity
|
||||
tags: [hardware, vds, books, host4g, backup, portainer, elasticsearch]
|
||||
related: [[kreknin-synology]], [[vds-kzntsv]], [[../concepts/portainer-stack-management-books-vds]]
|
||||
updated: 2026-05-25 (ES migration from Windows host)
|
||||
---
|
||||
|
||||
# Books VDS
|
||||
|
||||
Клиентский VDS (отдельный от инфра-VDS [[vds-kzntsv]]). Хостит production books app stack (api/web/scheduler/task-runner) + shared infra (mongo, minio, elasticsearch, traefik, portainer).
|
||||
|
||||
**Это НЕ vds-kzntsv.** Books-стек живёт здесь, не на vds.kzntsv.site. Domain `books.kzntsv.site` (и `bookva.kzntsv.site`) разрешаются в этот IP.
|
||||
|
||||
## Доступ
|
||||
|
||||
- **Public IP:** 89.253.255.133
|
||||
- **Provider:** host4g.ru (DNS вендорский `vps-21075162-277731.host4g.ru`)
|
||||
- **OS:** CentOS 7, kernel 3.10.0-1160.36.2.el7.x86_64 (древний, EOL)
|
||||
- **SSH:** `root@89.253.255.133:22` — only via ed25519 key (`~/.ssh/id_ed25519_books_ops` на workstation)
|
||||
- **Sudo:** root, passwordless (мы под root напрямую)
|
||||
- **Portainer:** `https://portainer.kzntsv.site` (отдельный Portainer, не путать с `portainer.vds.kzntsv.site` для [[vds-kzntsv]])
|
||||
|
||||
**Публичные DB-endpoints (наружу, для локальных `packages/tools`):**
|
||||
- **slovo MariaDB:** `mariadb.kzntsv.site:3306` (контейнер `books-db`), db `books` / user `books`.
|
||||
- **bookva MariaDB:** `89.253.255.133:33306` (контейнер `bookva-db`, маппинг `0.0.0.0:33306->3306` — порт 33306, т.к. 3306 занят slovo). db `books` / user `books`, тот же pw. **DNS-алиаса нет — коннект по IP.** (Verified 2026-06-27.)
|
||||
- **bookva mongo / ES:** НЕ опубликованы — `bookva-mongo` (27017) и `bookva-es` (9200/9300) только docker-internal, наружу не торчат. Локальные tools их не читают; при нужде — SSH-туннель.
|
||||
|
||||
> Проверка экспозиции порта: `docker inspect <c> --format '{{json .NetworkSettings.Ports}}'` или `docker ps --format '{{.Ports}}'` — НЕ `.NetworkSettings.Networks` (та показывает только IP, не публикацию).
|
||||
|
||||
Все creds — в `pass show books-vds/full-env`.
|
||||
|
||||
## Disk
|
||||
|
||||
```
|
||||
/dev/vda1 122G 53G used (44%) 69G free # 2026-06-04, после disk-full remediation
|
||||
```
|
||||
|
||||
Single root partition, нет отдельной `/data`. Docker hub data под `/var/lib/docker/`.
|
||||
|
||||
**Disk-full incident 2026-06-04** (provider Rusonyx monitoring alert, 4.95% free / 96% used). Culprits:
|
||||
- **38 GB unrotated container logs** — `books-task-runner` (32.8 GB) + `bookva-task-runner`-история (5.6 GB). `books-task-runner` (тенант **slovo**) в tight error-loop с ~2026-05-31 (~9 GB/day, no log rotation). Truncated live (`:> json.log`, no restart).
|
||||
- **~26 GB orphaned BuildKit cache** — 11 running `buildx_buildkit_builder-*` daemon-контейнеров, которых нет в `docker buildx ls` (orphan builders), держали 9× `*_state` volumes. Removed (`docker rm -f` + `docker volume rm`).
|
||||
- Result: 96% → 44%.
|
||||
|
||||
**Durable guard added 2026-06-04:** `/etc/logrotate.d/docker-containers` (copytruncate, size 200M, rotate 3, compress) + hourly `/etc/cron.d/docker-logrotate`. Caps любой runaway container-log без docker restart.
|
||||
|
||||
**Root cause залипшего лога (НЕ «протухший ключ» — правка-самообман, см. ниже):**
|
||||
- На books VDS два зеркальных стека: **books-*** (тенант slovo, `books-db`) и **bookva-*** (тенант bookva, `bookva-db`), один и тот же образ. Ozon-вызовы шлёт `*-task-runner` (не scheduler — он лишь триггерит agenda-джобу, HTTP делает task-runner). Только `books-task-runner` сыпал ошибки; `bookva-task-runner` — 0.
|
||||
- В таблице `sellers` (обе БД) два Ozon-продавца: **Slovo (client_id 94191)** и **Bookva (client_id 50542)**. В `books-db` у строки Bookva в `api_key` **намеренно вписан ключ Slovo** (`9683…` вместо живого `ddc2…`) — это **сознательная ревокация** доступа books→Ozon-аккаунт Bookva (сделано в прошлой сессии). Ozon на `Client-Id:50542`+чужой ключ → `code 5 Invalid Api-Key`. **НЕ чинить этот ключ** — это и есть защита. (Живой ключ Bookva — в `bookva-db` и в конфиге, для тенанта bookva.)
|
||||
- Спам шёл оттого, что часть books-agenda-джоб **не заскоуплены** на slovo: при пустом `data.idSeller` таски итерируют ВСЕХ продавцов (`data.idSeller ? filter : allSellers`), включая Bookva с битым ключом. Раньше заскоупили только `ozon stocks syncronization` (`idSeller:2`).
|
||||
|
||||
**Фикс 2026-06-04:** проставлен `data.idSeller=2, otherSellers:[]` в `books-job-scheduler-mongo` (agendaDb.agendaJobs) пяти джобам: `ozon fbs postings syncronization`, `generate old prices`, `ozon fbs products syncronization`, `create ozon products in incorrect state report`, `put on sale products`. Залипшие overdue-прогоны оборваны: unlock (`lockedAt:null`) + `docker restart books-task-runner`. Результат: Invalid-Api-Key 6448/мин → **0**, джобы отработали по slovo и перепланировались.
|
||||
|
||||
**Durability фикса:** правка в mongo переживает деплой. Эти джобы — легаси (`data._fromConfig` отсутствует), reconciler (`lib/reconciler.js`, `RECONCILER_ENABLED=true`) трогает только `_fromConfig:true`-джобы; в `tasks.json` (запечён в образ) у них `schedule:null` → reconciler их пропускает. Эмпирика: `stocks-sync idSeller:2` (только в mongo) пережил деплой 31.05.
|
||||
|
||||
**Полнота скоупа (сверено 2026-06-04).** На books-стороне сосуществуют ДВА поколения джоб:
|
||||
- **Legacy** (без `_fromConfig`, расписание в persisted mongo): seller-перебирающие — `seller.findAll` при пустом `data.idSeller` → ВСЕ продавцы. Ровно 7 таких: `ozonFbsPostingsSyncronization`, `ozonFbsProductsSyncronization`, `ozonStocksSyncronization`, `generateOldPrices`, `createOzonProductsInIncorrectStateReport`, `putOnSaleProducts`, `syncStocksWithWarehouse`. **Все заскоуплены `idSeller:2`** (5 сегодня + stocks + warehouse ранее). Других seller-перебирающих нет.
|
||||
- **Reconciler-gen** (`_fromConfig:true`, из `tasks.json`): фильтруются по **`salesChannels:[2]`** (ozon-канал Slovo), не по idSeller — `load products to ozon`, `ozonProductSalesChannelSync`, `unarchiveAutoArchivedProducts`, `addFixedPriceProductsToAction`, `fbsPicking*` и т.д. Уже корректно указывают только на канал Slovo.
|
||||
|
||||
**Группа В — НЕ дыра (изначальная гипотеза снята).** `sales_channels` в books-db: канал 2 = Slovo **type=ozon**; канал 3 = Bookva **type=ym** (Яндекс.Маркет, не Ozon). Активного Bookva-**ozon**-канала нет → channel-driven Ozon-джобы физически не могут залезть в Bookva. Bookva-`ym` использует отдельный `ym_api_key`, к инциденту не относится.
|
||||
|
||||
Follow-ups:
|
||||
1. **Каноничный фикс — в репо books**: legacy seller-перебирающие джобы стоит либо мигрировать в `tasks.json` со скоупом, либо вывести из эксплуатации (их функции, возможно, уже покрыты reconciler-gen). Риск mongo-only правки: если будущий релиз даст legacy-джобе реальный `schedule` в `tasks.json` без скоупа — reconciler пере-сеет её (data без idSeller) → доступ к Bookva вернётся.
|
||||
2. ~~wiki-drift — `bookva-*` stack не в stack-inventory~~ **частично закрыто 2026-06-12**: bookva-db/mongo/es/minio засечены и (кроме es) добавлены в backup (см. § Backup). Публичный endpoint bookva-db (`:33306`) задокументирован в § Доступ (2026-06-27). Полный stack-inventory bookva-* (api/web/scheduler/task-runner/ntfy) — всё ещё TODO.
|
||||
|
||||
## Стек (2026-05-25 inventory)
|
||||
|
||||
### Portainer-managed stacks (endpoint 1)
|
||||
|
||||
Post-migration 2026-05-25 (см. [[../concepts/portainer-stack-management-books-vds]]).
|
||||
|
||||
| Id | Stack | Container(s) | Bind data path | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| 22 | `books-job-scheduler` | books-job-scheduler + books-job-scheduler-mongo | `/opt/books/job-scheduler/{config,mongo/{db,configdb}}` | agenda jobs (cron + on-demand) |
|
||||
| 23 | `books-api` | books-api | `/opt/books/api/{config,data,upload}` | books API server (Nitro, port 3021) |
|
||||
| 24 | `books-web` | books-web | — | books-Nuxt front-end |
|
||||
| 25 | `books-ntfy` | books-ntfy | `/opt/books/ntfy/` | local ntfy (separate from shared `ntfy.vds.kzntsv.site`) |
|
||||
| 26 | `books-ops-mcp` | books-ops-mcp + books-ops-mcp-bootstrap-1 + books-docker-proxy + books-docker-proxy-ro | `/opt/books/books-ops-mcp/` | books ops-mcp endpoint |
|
||||
| 27 | `chrome` | chrome | — | Puppeteer browser for PDFs / scraping |
|
||||
| 28 | `proxy-chain` | proxy-chain | `/usr/docker/proxy-chain/config` | upstream HTTP-proxy chain, internal-only |
|
||||
| 29 | `imgproxy` | imgproxy + imgproxy-nginx | `/usr/docker/imgproxy/nginx/{cache,nginx.conf}` | serves `imgproxy.kzntsv.site`, S3→minio |
|
||||
| 30 | `minio` | minio | `/usr/docker/minio/data` | S3 blob store, `minio.kzntsv.site` |
|
||||
| 31 | `mongo` | mongo | `/usr/docker/mongo/data/{db,configdb}` | shared MongoDB 4.2 |
|
||||
| 32 | `books-db` | books-db | `/usr/docker/books-db/data` | MariaDB latest, books application DB |
|
||||
| 33 | `elasticsearch` | elasticsearch | `/usr/docker/elasticsearch/{data,snapshots}` | ES 7.10.0 + `path.repo=/snapshots` для backup + `reindex.remote.whitelist=elasticold.kzntsv.site:443` (added 2026-05-25 для migration) |
|
||||
|
||||
### SSH-managed (management plane — Portainer migration skipped)
|
||||
|
||||
Recreate ломает access всему остальному (traefik) или себя (portainer). Оставлены ad-hoc compose в `/usr/docker/<svc>/`.
|
||||
|
||||
| Compose dir | Containers | Bind data path |
|
||||
|---|---|---|
|
||||
| `/usr/docker/traefik/` | traefik | `./letsencrypt` (acme.json) + `./data/traefik.yml` |
|
||||
| `/usr/docker/portainer/` | portainer | named volume `portainer_data` |
|
||||
|
||||
### Build helpers (transient)
|
||||
|
||||
- `buildx_buildkit_builder-*` × 7 — buildkit builders, oneshot за `docker buildx`. Не data.
|
||||
|
||||
## App-config paths
|
||||
|
||||
```
|
||||
/opt/books/api/{config,data,upload}
|
||||
/opt/books/job-scheduler/{config,mongo/{db,configdb}}
|
||||
/opt/books/task-runner/
|
||||
/opt/books/ntfy/ # local ntfy data
|
||||
/opt/books/books-ops-mcp/
|
||||
```
|
||||
|
||||
`config/default.json` overrides image-baked config (MySQL/ES/S3 endpoints).
|
||||
|
||||
## Backup
|
||||
|
||||
Daily 06:00 MSK → kreknin. См. [[../sources/books-vds-backup-daily-kreknin-2026-05-25]]. Pipeline:
|
||||
- DB dumps (slovo): books-db (mariadb), mongo (shared), books-job-scheduler-mongo
|
||||
- DB dumps (bookva, added 2026-06-12): bookva-db (mariadb, тот же root pw что books-db), bookva-mongo (no-auth)
|
||||
- ES snapshots via REST API (file repo `kreknin`): slovo `elasticsearch` + `bookva-es` (раздельные репо/каталоги)
|
||||
- rsync bind paths + dumps + `bookva-minio-data` (named volume, raw) + `/usr/docker/bookva-es` (snapshot repo) + `/opt/books/` + `/etc/{ssh,hosts,cron.d}` + `/root/.ssh` → `vitya@kreknin:/volume1/NetBackup/books-vds/<date>/`
|
||||
- ntfy `BOOKS-VDS backup OK <date>` + email
|
||||
- Retention 7 daily snapshots
|
||||
|
||||
Live since 2026-05-25. **bookva tenant добавлен 2026-06-12** (db/mongo/minio/es verified на kreknin: bookva-mariadb 257M, bookva-mongo 3.2M, bookva-minio 1.4G, bookva-es snapshot 801M).
|
||||
|
||||
**bookva-es snapshot (added 2026-06-12):** Portainer stack 37 пересоздан с `path.repo=/snapshots` + bind `/usr/docker/bookva-es/snapshots` (uid 1000:0), repo `kreknin` зарегистрирован. **Gotcha:** оба ES-каталога называются `snapshots` → в rsync источник bookva-es = родительский `/usr/docker/bookva-es` (basename `bookva-es`), иначе слились бы в один dest/`snapshots/` и испортили оба репо. На kreknin: `snapshots/` (slovo) и `bookva-es/snapshots/` — раздельно. Deploy script: `/opt/stacks/backup/run.sh` (== repo `scripts/books-vds-backup-daily-kreknin/run.sh`).
|
||||
|
||||
## DNS
|
||||
|
||||
- `books.kzntsv.site` → 89.253.255.133 (`books-api` host route via local traefik)
|
||||
- `bookva.kzntsv.site` → 89.253.255.133 (`books-web` host route)
|
||||
- `elasticsearch.kzntsv.site` → 89.253.255.133 (ES via traefik basicAuth)
|
||||
- `portainer.kzntsv.site` → 89.253.255.133 (Portainer UI)
|
||||
- `edge.kzntsv.site` → 89.253.255.133 (Portainer edge agent)
|
||||
|
||||
Auth-зоны DNS — в reg.ru под `kzntsv.site`.
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- [[kreknin-synology]] — backup target.
|
||||
- [[vds-kzntsv]] — наш инфра-VDS (отдельный, gitea/registry/etc.) — НЕ хост books'а.
|
||||
- [[../concepts/vds-kzntsv-ssh-access]] — SSH-аудит инфра-VDS (был misnamed как `books-ssh-access` до 2026-05-25; ошибочно claimed vds-kzntsv hosts books).
|
||||
- [[../sources/books-vds-backup-daily-kreknin-2026-05-25]] — backup standup chronology.
|
||||
|
||||
## Making-of-history notes
|
||||
|
||||
- Discovered 2026-05-25 во время backup-таски. Wiki [[../concepts/books-ssh-access]] до этого ошибочно говорила что books на vds-kzntsv (89.253.255.94). User: «books VDS - это совсем другой сервер, клиентский». Inventory + IP probe confirmed.
|
||||
- Workstation SSH key `id_ed25519_books_ops` уже был задеплоен под root до этой сессии (видимо, прошлый bootstrap забыт-без-документации).
|
||||
- Hybrid stack-management (Portainer + SSH-compose) — pre-existing, не наш design. Migrated 6 SSH-compose стеков → Portainer 2026-05-25 (`books-vds-stacks-to-portainer` 🟢). `traefik` + `portainer` остаются SSH-managed (management plane).
|
||||
- `imgproxy` (2 containers, `imgproxy.kzntsv.site`) был missing из этого entity wiki до Phase 0 probe 2026-05-25 — fixed in flight.
|
||||
- ES indices populated 2026-05-25 — migration с Windows host (`elasticold.kzntsv.site`, ES 7.10.1) → books VDS via reindex-from-remote. 3 indices: `artmone` (2621), `epz` (820604), `products` (105922). Consumers (books-api + books-task-runner + books-job-scheduler) switched `default.json:elasticsearch.node` с elasticold на elasticsearch.kzntsv.site. Source ES остаётся running (rollback). См. [[../../tasks/migrate-elasticsearch-to-books-vds]] § Closure note.
|
||||
80
.wiki/entities/de-vds-3xui.md
Normal file
80
.wiki/entities/de-vds-3xui.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: DE VDS — 3x-UI VLESS node
|
||||
type: entity
|
||||
tags: [vds, cloud, germany, vpn, vless, xray, 3x-ui, proxy, fornex]
|
||||
sources: [../sources/de-vds-3xui-setup-2026-07-14.md]
|
||||
updated: 2026-07-14
|
||||
---
|
||||
|
||||
# DE VDS — 3x-UI
|
||||
|
||||
Личный прокси-VDS в Германии, активирован 2026-07-14, под управлением 3x-UI 3.5.0 / Xray 26.7.11.
|
||||
Назначение — обход блокировок для себя и раздача доступа друзьям. Аналог [[nl-vds-3xui]] (NL/Aeza), поднятый по тем же урокам: повторён **только проверенный plain VLESS**, маскированные протоколы (Reality/MTProto/SOCKS5) НЕ поднимались (на NL у боевых клиентов из РФ не заработали).
|
||||
|
||||
> Не путать с [[nl-vds-3xui]] (`213.176.64.253`, Aeza) и [[vdsina-outline-3xui]] (`46.151.25.64`, VDSina Amsterdam) — это два NL-узла. Этот — Fornex `130.17.17.158`, Германия.
|
||||
|
||||
## Hardware
|
||||
|
||||
- **Локация:** Германия (провайдер **Fornex**, `335555.fornex.cloud`)
|
||||
- **Public IP:** `130.17.17.158` ; **IPv6:** `2a02:6b40:2000:3505::1`
|
||||
- **vCPU / RAM / Disk:** 1 / 2 GiB / 20 GiB NVMe ; **swap 0** ; 300 Мбит/с
|
||||
- **OS:** Ubuntu 24.04.4 LTS (kernel 6.8) ; ufw inactive, iptables ACCEPT
|
||||
|
||||
## Доступ
|
||||
|
||||
- **SSH:** `root@130.17.17.158` (password auth). Host-key (ed25519): `SHA256:1AQ5jyJDE7iunqZe7skV3B+8fpDhPmC4CRFwfrRcmdE`.
|
||||
- С Windows non-interactive: `plink -ssh -batch -hostkey "SHA256:1AQ5...mdE" -pw <pass> root@... -m <script>`. Инлайн-команды с кавычками PowerShell корёжит → скрипт через `-m` (тот же урок, что на [[nl-vds-3xui]]).
|
||||
- **Панель 3x-UI:** `http://130.17.17.158:37601/e2l5qBmNRkJ8Xw9CtM/` (port 37601, http — БЕЗ TLS; random path единственный барьер от сканеров; fail2ban `3x-ipl` active).
|
||||
- **Sub:** `http://130.17.17.158:2096/sub/`.
|
||||
- **Креды:** `pass show de-vds-3xui/full-env`. Креды панель user/pass заданы вручную (install автосгенерил `jclyRE8LrK` + bcrypt, plaintext был недоступен). `settings.secret` (JWT) НЕ ротирован (не светился).
|
||||
|
||||
## Software stack
|
||||
|
||||
| Слой | Компонент | Версия |
|
||||
|---|---|---|
|
||||
| OS | Ubuntu | 24.04.4 LTS |
|
||||
| Panel | 3x-UI (нативный systemd `x-ui.service`, enabled) | **3.5.0** |
|
||||
| Core | Xray (`/usr/local/x-ui/bin/xray-linux-amd64`) | 26.7.11 |
|
||||
| DB | SQLite `/etc/x-ui/x-ui.db` (бэкапы `/etc/x-ui/x-ui.db.bak.*`) | — |
|
||||
| Firewall/IPS | ufw inactive; fail2ban jail `3x-ipl` (поставился вместе с 3x-ui) | — |
|
||||
|
||||
## Текущее состояние (2026-07-14)
|
||||
|
||||
🟢 Работает. **Единственный инбаунд — plain VLESS 32030** (`security=none`, tcp). xray слушает `*:32030`, в `config.json` персистентно (inbounds=2: api-tunnel + 32030). Server-side e2e (xray-клиент на сервере → 32030 → exit) подтверждён: трафик выходит с сервера (IPv4 `130.17.17.158` / IPv6 `2a02:6b40:2000:3505::1`).
|
||||
|
||||
| Порт | Протокол | Статус |
|
||||
|---|---|---|
|
||||
| **32030** | VLESS / tcp, `security=none` | 🟢 сервер-сайд работает. uuid `f3a0dda0-…` (email `vitya`). DPI-детектируем (как и на NL), но живой. |
|
||||
| 37601 | 3x-UI panel (http) | 🟢 |
|
||||
| 2096 | sub server | 🟢 |
|
||||
| 22 | ssh | 🟢 |
|
||||
|
||||
Reality / MTProto / SOCKS5 **намеренно НЕ подняты** — на [[nl-vds-3xui]] у боевых клиентов из РФ они не заработали (см. [`nl-vds-3xui-setup-2026-06-05`](../sources/nl-vds-3xui-setup-2026-06-05.md)); порты зря торчать не должны.
|
||||
|
||||
## Клиент для друзей
|
||||
|
||||
Hiddify / v2rayN / v2rayNG — импорт `vless://f3a0dda0-947b-4a11-b3f5-ca973640f843@130.17.17.158:32030?type=tcp&security=none&encryption=none#de-vless-32030`. При включённом VPN в Telegram прокси не настраивать (моб. «отключить прокси», десктоп «системный»). Полная ссылка/QR — `pass de-vds-3xui/full-env`.
|
||||
|
||||
## Подводные камни (3x-ui 3.5.0 — NEW vs NL-сессии 3.2.7)
|
||||
|
||||
Установка на Germany оказалась нетривиальной из-за новой версии 3x-ui — на NL стояла 3.2.7, тут 3.5.0, и API/DB-модель поменялась. Зафиксировать, чтобы не повторяться:
|
||||
|
||||
- **CSRF на ВСЕХ panel POST.** `CSRFMiddleware` → без валидного `X-CSRF-Token` (header) → `403 Forbidden` с пустым телом + CSP-nonce. Логин-хендлер тут ни при чём (он отдаёт 200+JSON даже при неудаче). Токен: `GET {webBasePath}csrf-token` (публичный, кладёт токен в session-cookie) → вернуть тем же заголовком. Сначала это выглядело как «битые креды» — на деле 403 = отсутствие CSRF-токена.
|
||||
- **login route:** `POST {webBasePath}login`, JSON-тело `{"username","password"}` (form-encoded ≠). 404 на `/login` (роут только под basePath), 403 на `{BP}login` без CSRF.
|
||||
- **Прямой INSERT в `inbounds` НЕ рендерит инбаунд в xray-config в 3.5.0.** Клиентская модель разнесена по таблицам `clients` / `client_inbounds` / `client_traffics`; эти записи заполняются только сервисом `AddInbound` (через panel API/UI), а не raw-SQL. Я дважды вставлял инбаунд в `inbounds` (даже с эталонным JSON, скопированным с рабочего nl-vds 32030) — x-ui видел «Normalized sub_sort_index on 1 inbound(s)», но в `config.json` инбаунд не попадал, xray не слушал. **Лекарство — panel API**, не БД.
|
||||
- **stream_settings naming:** `"tcpSettings"` (не `"tcp"`), с `"header":{"type":"none"}`; `sniffing` = `{"enabled":false}`; vless `settings` = `{clients, decryption:"none", encryption:"none", testseed:[900,500,900,256]}`. Эталон снят с рабочего инбаунда [[nl-vds-3xui]] 32030.
|
||||
- **API add endpoint:** `POST {BP}panel/api/inbounds/add`, body = `model.Inbound` (camelCase: `remark,enable,port,protocol,listen,tag,settings,streamSettings,sniffing,shareAddrStrategy,...`); `settings`/`streamSettings`/`sniffing` — **stringified JSON** (строки, не вложенные объекты).
|
||||
- **`x-ui setting` через wrapper НЕ применяет** `-username/-password` (команда печатает меню, но users-таблицу не меняет). Надо дёргать бинарник напрямую: `systemctl stop x-ui; /usr/local/x-ui/x-ui setting -username … -password …; systemctl start x-ui` → «Username and password updated successfully». Иначе логин сбросит «Invalid username or password».
|
||||
- **3.5.0 hot-applies** новые инбаунды к работающему xray (xray начинал слушать 32030 ещё до rewrite `config.json`); после `x-ui restart` config.json переписывается из БД и инбаунд персистентен.
|
||||
- Install v3.5.0 **автогенерит** рандомные panel user/pass/port/webBasePath (`hasDefaultCredential:false`) + ставит fail2ban — лучше старых версий, но plaintext пароля недоступен (bcrypt) → всё равно перевыставлять через бинарник.
|
||||
|
||||
## Проверено / не проверено
|
||||
|
||||
- ✅ Сервер-сайд: xray слушает 32030, локальный xray-клиент через тоннель выходит с сервера.
|
||||
- ✅ **Реальный клиент из РФ — подтверждён 2026-07-14.** User подключён через этот узел прямо сейчас (сессия идёт через 32030). Plain VLESS снова оправдал «единственный proven-working» статус.
|
||||
|
||||
## Открытые хвосты
|
||||
|
||||
- Per-friend UUID (сейчас один — `vitya`). Заводить через панель при раздаче.
|
||||
- panel на http:3637601 — рассмотреть SSH-tunnel-only или LE-сертификат (опция 20 меню), пока компенсирует random path + fail2ban.
|
||||
- Бэкап `x-ui.db` в pipeline (как на [[backup-inventory-2026-06]] для nl-vds `x-ui.db`) — не заведён.
|
||||
77
.wiki/entities/nl-vds-3xui.md
Normal file
77
.wiki/entities/nl-vds-3xui.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: NL VDS — 3x-UI VLESS node
|
||||
type: entity
|
||||
tags: [vds, cloud, netherlands, vpn, vless, reality, mtproto, xray, 3x-ui, proxy]
|
||||
sources: [../sources/nl-vds-3xui-setup-2026-06-05.md]
|
||||
updated: 2026-06-06
|
||||
---
|
||||
|
||||
# NL VDS — 3x-UI
|
||||
|
||||
Личный прокси-VDS в Нидерландах, активирован 2026-06-05, под управлением 3x-UI (x-ui) / Xray.
|
||||
Назначение — обход блокировок для себя и раздача доступа друзьям.
|
||||
|
||||
> Не путать с [[vdsina-outline-3xui]] (`46.151.25.64`, провайдер VDSina, Амстердам) — это **другой** NL-VDS (Outline + 3x-UI), основной боевой VPN семьи. Этот — Aeza `213.176.64.253`.
|
||||
|
||||
## Hardware
|
||||
|
||||
- **Локация:** Нидерланды (провайдер Aeza, судя по `my.aeza.net` в трафике)
|
||||
- **Public IP:** `213.176.64.253` ; **IPv6:** `2a12:5940:2b1d::2`
|
||||
- **vCPU / RAM / Disk:** 1 / 2 GiB / 30 GiB NVMe
|
||||
- **OS:** Ubuntu 24.04 ; **PTR:** `square-white.ptr.network`
|
||||
|
||||
## Доступ
|
||||
|
||||
- **SSH:** `root@213.176.64.253` (password auth). Host-key (ed25519): `SHA256:XR2+0PCVUoEMrFHOoIu7IPXb4xP2JnYkqFr1uqs8YrA`.
|
||||
- С Windows non-interactive: `plink -ssh -batch -hostkey "SHA256:XR2+...YrA" -pw <pass> root@... "<cmd>"`. Пайп `y` для приёма host-key через PowerShell **не работает** (plink читает промпт с консоли) — передавать `-hostkey` явно. Для python на сервере — заливать скрипт через `pscp` (инлайн-python в plink-арге PowerShell корёжит кавычками).
|
||||
- **Панель 3x-UI:** `https://213.176.64.253:49251/aTKt95PPgt9y2dUHi8/` (port 49251, sub-port 2096).
|
||||
- **Креды:** `pass show nl-vds-3xui/full-env`. **Все логин-креды ротированы 2026-06-05** (оригиналы светились в плейнтексте): root SSH pass, panel user+pass, panel `secret` (JWT — убил install-time API-токен и сессии). Reality privateKey/UUID/seed НЕ ротировались (не светились, на них завязаны клиентские профили).
|
||||
|
||||
## Software stack
|
||||
|
||||
| Слой | Компонент | Версия |
|
||||
|---|---|---|
|
||||
| OS | Ubuntu | 24.04 |
|
||||
| Panel | 3x-UI (x-ui), нативный systemd-сервис | 3.2.7 |
|
||||
| Core | Xray (`/usr/local/x-ui/bin/xray-linux-amd64`) | 26.6.1 |
|
||||
| MTProto | mtg (`9seconds/mtg`, systemd `mtg.service`) | 2.2.8 |
|
||||
| DB | SQLite `/etc/x-ui/x-ui.db` (бэкапы `/root/x-ui.db.bak.*`) | — |
|
||||
| Firewall | ufw inactive, iptables policy ACCEPT | — |
|
||||
|
||||
Конфиг ядра генерится в `/usr/local/x-ui/bin/config.json` из БД при `x-ui restart`.
|
||||
|
||||
## Текущее состояние (итог сессии 2026-06-05) — что РЕАЛЬНО работает
|
||||
|
||||
> **Работает только plain VLESS на 32030.** Через него идёт боевой трафик из РФ (v2rayN на ПК; v2rayNG/Hiddify на телефоне/Mac). Это канал, который раздаём друзьям. Всё «маскированное» (Reality, MTProto, SOCKS5) у реальных клиентов из РФ **не поднялось** — детальный разбор почему в [source-странице](../sources/nl-vds-3xui-setup-2026-06-05.md).
|
||||
|
||||
| Порт | Протокол | Статус у реальных клиентов |
|
||||
|---|---|---|
|
||||
| **32030** | VLESS / tcp, `security=none` | 🟢 **РАБОТАЕТ.** uuid `a6fa7965-…` (sx9l9csam3). Без TLS (DPI-детектируем, но живой). |
|
||||
| 2053 | VLESS + **Reality** X25519, flow vision, dest `www.microsoft.com` (PQ снят) | 🔴 **НЕ работает** у v2rayN/телефона. В изолированных curl/xray-тестах с того же ПК проходил (exit NL, 5 МБ/с), но **боевые клиенты — нет**. Причина не установлена (вероятно — обработка TLS-хендшейка к этому IP в сети пользователя). Был на 443 → перенесён на 2053. Клиенты: `5da48418-…` (pqmkayaxo2), `7941887a-…` (0h5rzrsex3); pbk `zWTp-3dPfB78…`, sid `795fc9`. |
|
||||
| 47020 | SOCKS5 (auth=password) | 🔴 у пользователя режется DPI (на сервере сам по себе ОК, auth работает). user `tg_s5wm7I`. |
|
||||
| 8443 | **MTProto** (mtg, FakeTLS `www.cloudflare.com`) | 🔴 **НЕ работает как Telegram-прокси:** и Telegram Desktop, и телефон висят на «соединение…». Пассивный пинг в Telegram показывает «доступен» — **вводит в заблуждение**. В tcpdump: телефон шлёт 1288-б ClientHello → mtg отвечает **0 байт**. Локальный FakeTLS (`openssl` на сервере) проходит, но реальную MTProto-сессию mtg здесь не держит. |
|
||||
| **32031/32032/33** | **Shadowsocks** `chacha20-ietf-poly1305`, tcp+udp (3 инбаунда под Outline-app) | 🟡 **Протокол ОК, RF не проверен.** Созданы 2026-06-06 (NL-Outline-1/2/3). Серверный e2e-тест (xray-client→SOCKS→curl) прошёл — exit NL. Но SS на этом узле **в РФ DPI-режет** (см. 47020) → внутри РФ скорее всего не поднимется; вне РФ работает. Ключи (`ss://`) и пароли — `pass show nl-vds-3xui/full-env`. |
|
||||
| 13027 | VLESS (id 4 в БД) | ⚪ **Назначение неизвестно** — обнаружен в БД 2026-06-06, в сессии 2026-06-05 не задокументирован. Требует ревизии. |
|
||||
|
||||
## Клиент для друзей (рабочий путь)
|
||||
|
||||
Hiddify (Win/Mac/Android) либо v2rayN/v2rayNG — импорт `vless://…@213.176.64.253:32030?type=tcp&security=none` (ссылка/QR). Полная инструкция для друзей — в [source](../sources/nl-vds-3xui-setup-2026-06-05.md).
|
||||
**При включённом VPN в Telegram прокси не нужен:** на телефоне «Отключить прокси», на десктопе «Использовать системные настройки прокси», кастомные прокси удалить.
|
||||
|
||||
## Что трогали на сервере 2026-06-06
|
||||
|
||||
- **Outline-клиенты.** Заведены 3 отдельных классических Shadowsocks-инбаунда (`chacha20-ietf-poly1305`, tcp+udp) на портах 32031/32032/32033 — по одному `ss://`-ключу под приложение Outline, для возможности независимого отзыва. Выбор classic, а не SS-2022 multi-user: classic понимает любая версия Outline-app; мульти-юзер на одном инбаунде в Xray возможен только для SS-2022. Бэкап БД `/root/x-ui.db.bak.before-ss-*`. Скрипт вставки `/root/add_ss.py`. Ключи в `pass nl-vds-3xui/full-env`.
|
||||
|
||||
## Что трогали на сервере 2026-06-05
|
||||
|
||||
- Reality: dest `intel`→`microsoft`; снят ML-DSA-65 (`mldsa65Seed`); порт 443→2053.
|
||||
- Креды ротированы (см. Доступ).
|
||||
- Поднят mtg (MTProto) на 8443; SOCKS5-инбаунд на 47020.
|
||||
- **MSS-clamp 1360** ставился (ошибочная MTU-гипотеза) и **снят** — он ломал соединения (после снятия комп-телега через системный прокси ожила).
|
||||
|
||||
## Подводные камни / уроки
|
||||
|
||||
- **ML-DSA-65 (PQ-Reality) несовместим с GUI-клиентами** — v2rayN/мобильные не шлют `mldsa65Verify` → PQ надо отключать. Полный разбор: [`reality-pq-mldsa65-dest-incompatibility`](../concepts/reality-pq-mldsa65-dest-incompatibility.md).
|
||||
- **Свои короткие curl/standalone-xray тесты «работали», а боевые клиенты — нет.** Не доверять собственным изолированным тестам как доказательству — проверять на реальном клиенте/устройстве: [`proxy-debugging-test-the-real-client`](../concepts/proxy-debugging-test-the-real-client.md).
|
||||
- **MSS-clamp на прокси-сервере может рвать соединения** — не лепить «на всякий случай».
|
||||
- Reality/MTProto в РФ — нетривиальны; единственное, что тут гарантированно поднялось у реальных клиентов — plain VLESS.
|
||||
@@ -55,7 +55,15 @@ updated: 2026-05-19
|
||||
|
||||
Дополнительные input rules для wan (стандартные OpenWRT): Allow-DHCP-Renew, Allow-Ping, Allow-IGMP, Allow-DHCPv6, Allow-MLD, Allow-ICMPv6-*, Allow-IPSec-ESP.
|
||||
|
||||
## Что bgужно сделать (на потом)
|
||||
## Config backup (с 2026-06-12)
|
||||
|
||||
Daily UCI/config backup → [[kreknin-synology]]:
|
||||
- `/root/uci-backup.sh` — `sysupgrade -b /tmp/uci-backup.tar.gz` → `dbclient -i /root/.ssh/id_kreknin -y vitya@195.19.90.188` (pipe stdin).
|
||||
- Cron `/etc/crontabs/root`: `30 3 * * * /root/uci-backup.sh`.
|
||||
- Ключ роутера `/root/.ssh/id_kreknin` (dropbear ed25519) авторизован на kreknin с **forced-command** `cat > /volume1/NetBackup/openwrt/openwrt-latest.tar.gz` + `no-pty,no-*-forwarding` — ключ умеет только записать один файл (роутер публично-доступен → ограничение обязательно).
|
||||
- Latest-only (без истории снапшотов); tarball ~15 КБ. См. [[../concepts/backup-inventory-2026-06]].
|
||||
|
||||
## Что нужно сделать (на потом)
|
||||
|
||||
- Отключить устаревшие redirects на 192.168.1.10 (DSM/FTP/SQL/Cloud Station) — снижает attack surface.
|
||||
- Запланировать DDNS для случая смены публичного IP (REGRU domains всё ещё указывают на 94.19.247.14).
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
---
|
||||
title: RUVDS IIS Host (Windows Server 2025 Core) — 80.64.31.36
|
||||
title: RUVDS IIS Host (Windows Server 2025 Core) — 80.64.31.36 — DECOMM 2026-07-21
|
||||
type: entity
|
||||
tags: [hardware, vds, ruvds, windows, iis, cms, snolla, kupimknigi]
|
||||
tags: [hardware, vds, ruvds, windows, iis, cms, snolla, kupimknigi, decommissioned]
|
||||
sources: [../sources/iis-migration-to-ruvds-2026-05-23.md, ../sources/ruvds-backup-daily-kreknin-2026-05-24.md]
|
||||
updated: 2026-05-24
|
||||
updated: 2026-07-21
|
||||
---
|
||||
|
||||
# RUVDS IIS Host
|
||||
# RUVDS IIS Host — ⛔ DECOMM 2026-07-21
|
||||
|
||||
> **Декоммишнен 2026-07-21.** Погашен у провайдера, забыт. Деньги экономим. Финальный offsite-снимок на kreknin верифицирован (см. § Decommission ниже). Rollback-target для всех мигрированных snolla-сайтов **более недоступен** — откат только образ-тегом в registry / DNS уже на VDS.
|
||||
>
|
||||
> Что ниже — историческая запись живого хоста (куплен 2026-05-23, DECOMM 2026-07-21, срок жизни ~2 месяца). Креды/доступ сохранены в `pass` ретроспективно, хост выключен.
|
||||
|
||||
Облачный Windows-VDS у [RUVDS](https://ruvds.com), DC Королёв. Куплен 2026-05-23 как destination для миграции IIS-хостинга с [[windows-recovery-host]] — closing SPOF домашней машины для prod CMS сайтов (snolla CMS multi-tenant). Это **отдельный VDS** от [[vds-kzntsv]] (Rusonyx Linux, инфра-стек): RUVDS = только IIS catch-all для CMS hostnames, Rusonyx VDS = gitea/verdaccio/registry/DB park.
|
||||
|
||||
## Decommission (2026-07-21)
|
||||
|
||||
Сессия 2026-07-21: аудит «что осталось на RUVDS перед отказом от провайдера».
|
||||
|
||||
**Живого публично не осталось** — всё snolla-тираж уехало на VDS:
|
||||
- Публичный DNS → RUVDS (`80.64.31.36`): **только `rimiz.ru`/`www.rimiz.ru`** — и тот труп (HTTP 404 на RUVDS; сломан с 2026-05-19 iis-migration cleanup, не чинен, не мигрирован).
|
||||
- Всё остальное → VDS `89.253.255.94`: `on.snolla.com`, `snolla.com` apex, `kupimknigi.spb.ru`, `tandemmebel.ru`/www, `labtools.ru`/www, `labtools.pro`/www, `emspb.ru`/www, `pilorama98.ru`/www, `maljarka.tandemmebel.ru`, `ics-artmaterials`/`pilorama98`/`sestech`/`artmone`/`rimiz` `.snolla.com`. Smoke VDS 2026-07-21: 200 (кроме `snolla.com` apex — 404 TRAEFIK DEFAULT CERT, нет Host-правила; мёртв везде, отдельная мелочь, НЕ блокировала DECOMM).
|
||||
- VDS snolla-стеки Portainer: labtools(17)/emspb(18)/labtools-pro(19)/tandemmebel(20)/kupimknigi(21)/on-snolla(22).
|
||||
- Локальный .NET-админ (catch-all IIS `snolla` на воркстейшне, [[snolla-local-admin-and-on-snolla-migration-design]] §Task A) от RUVDS НЕ зависит — читает `mssql.kzntsv.site` + `minio.kzntsv.site` (оба на vds-kzntsv).
|
||||
|
||||
**Финальный offsite-снимок верифицирован 2026-07-21:**
|
||||
- `RUVDS-Backup-Daily` последний прогон 2026-07-21 04:30:01, `LastTaskResult=0`, 0 missed. DONE 72 мин (лог `C:\ProgramData\backup\logs\2026-07-21.log`): IIS-config snapshot + 13 certs exported (non-exportable pvt-key warning benign) + rclone sync snolla/applicationHost/iis-backup/certs/ssh-config + retention keep 7.
|
||||
- kreknin `/volume1/NetBackup/ruvds-iis/2026-07-21/` = **8.8G**, создан 05:37, полный (`sites/`+`iis-config/`+`iis-backup-webconfiguration/`+`certs/`+`ssh-config/`). 7 снимков в retention (2026-07-15…07-21).
|
||||
- Динамика (.NET-админ не несёт) — MSSQL+MinIO на vds-kzntsv, бэкапятся отдельно (см. [[backup-inventory-2026-06]]). Сам .NET-админ = статичные бинарники MoreThenCms + S3 drop-in, восстанавливаемо из source + локальная копия на воркстейшне.
|
||||
|
||||
**Loose end — RESOLVED 2026-07-21:** `rimiz.ru`/`www.rimiz.ru` A-запись переправлена оператором на `89.253.255.94`. Авторит. NS ns1/ns2.reg.ru = `89.253.255.94` (verified); public resolvers ещё держат stale `80.64.31.36` по TTL 86400 — сбросится в сутки. На RUVDS больше НИ ОДИН публичный домен не смотрит.
|
||||
|
||||
## Hardware / tariff
|
||||
|
||||
- **Vendor:** RUVDS (https://ruvds.com)
|
||||
@@ -39,13 +60,14 @@ updated: 2026-05-24
|
||||
|---|---|---|---|
|
||||
| `snolla` (AppPool `snolla`, .NET v4.0 Integrated, ApplicationPoolIdentity) | `C:\sites\snolla\` (8.66 GB / 44725 files, copied 2026-05-23 via SSH/scp) | `*:80:` catch-all HTTP + 25× `*:443:<hostname>` HTTPS SNI | catch-all для всех CMS hostnames через multi-tenant routing в самом CMS |
|
||||
|
||||
**25 HTTPS hostnames bound via SNI** (LE certs imported from traefik `acme.json`, valid до 2026-07-22, R13):
|
||||
- ✅ `emspb.ru` / `www.emspb.ru` — DNS flipped 2026-05-24 ~12:00
|
||||
**HTTPS hostnames bound via SNI** (было 25; сейчас **23** после декоммишна emspb.ru/www — 2026-07-02. `Get-WebBinding` total: 26→24 включая `*:80:` catch-all). LE certs теперь через [[../concepts/winacme-iis-owin-catchall-http01]] (binding-driven `--source iis`, cert до 2026-09-03):
|
||||
- 🗑️ `emspb.ru` / `www.emspb.ru` — **декоммишн 2026-07-02**: миграция на [[../entities/vds-kzntsv]] подтверждена оператором (off-LAN проверка с IIS-хоста: оба → 89.253.255.94 HTTP 200), SNI-биндинги сняты. Backup конфига `pre-emspb-decommission-2026-07-02`. `emspb.snolla.com` **оставлен** (НЕ мигрирован, DNS всё ещё → IIS). Rollback = re-add 2 биндингов (cert в store) или `Restore-WebConfiguration`.
|
||||
- ✅ `kupimknigi.spb.ru` — DNS flipped 2026-05-24 ~00:00 (pilot)
|
||||
- ⏳ 22 more in queue (`pilorama98.ru`/www, `labtools.ru`/www, `labtools.pro`/www, 12× `*.snolla.com`, `rimiz.ru`/www, `maljarka.tandemmebel.ru`, etc.) — pending DNS TTL drop в reg.ru (TTL=86400 пока)
|
||||
- ⛔ `tandemmebel.ru` / `www.tandemmebel.ru` — scope-narrowed 2026-05-24, остаются на windows-IIS на неопределённый срок
|
||||
- 🗑️ `tandemmebel.ru` / `www.tandemmebel.ru` — **cutover VDS 2026-07-12**: DNS reg.ru → 89.253.255.94, traefik Host-rule flipped, LE-серт issued. RUVDS IIS биндинги оставлены как rollback (не декоммишн).
|
||||
|
||||
**Degraded (CMS-side, не migration defect):** `maljarka.tandemmebel.ru`, `rimiz.ru`/www, `rimiz.snolla.com` — возвращают 502/404 (pre-existing dead-routes от [[../sources/iis-host-migration-2026-05-19]] cleanup; на source IIS:8089 тоже не работают).
|
||||
**Degraded (CMS-side, не migration defect):** `rimiz.ru`/www, `rimiz.snolla.com` — возвращают 502/404 (pre-existing dead-routes от [[../sources/iis-host-migration-2026-05-19]] cleanup; на source IIS:8089 тоже не работают).
|
||||
- ✅ `maljarka.tandemmebel.ru` — **пофикшен 2026-06-08**: 502-на-HTTPS был из-за `Sites.SettingsData=NULL` (нет блока `httpSecure`) → `KeyNotFoundException` в MoreThenCms. Корень + fix: [[../concepts/morethencms-null-settingsdata-https-502]]. rimiz — **другая** причина (его `SettingsData` заполнен).
|
||||
|
||||
## Ключевые папки
|
||||
|
||||
@@ -61,9 +83,9 @@ Daily 04:30 MSK → [[kreknin-synology]] via rclone+SFTP. Снимаются: `C
|
||||
|
||||
## Известные риски / SPOF
|
||||
|
||||
- **Image-pipeline SPOF:** RUVDS IIS вызывает `imgproxy.kzntsv.site` который **всё ещё** на [[windows-recovery-host]] — если домашняя машина гаснет, картинки rendering падает (HTML отдаётся OK с RUVDS). Verified post-migration `Test-NetConnection imgproxy.kzntsv.site -Port 443` = OK. Не closing полный SPOF — defer (Option B local nginx-relay / Option D decompile DLL, см. [[../concepts/iis-cutover-to-vds-services]]).
|
||||
- ~~**Image-pipeline SPOF:** RUVDS IIS вызывает `imgproxy.kzntsv.site` который всё ещё на [[windows-recovery-host]]~~ **СНЯТ 2026-06-08** — imgproxy переехал на [[../entities/books-vds]], windows-recovery-host декоммишнен. SPOF закрыт.
|
||||
- **2 GB RAM tight:** w3wp ~330 MB cold start. Monitor under prod load — возможно потребуется `RecyclingPeriodicRestartMemory 200MB` per pool.
|
||||
- **LE renewal pipeline отсутствует:** certs истекают 2026-07-22 (~60 дней window). Pending — `win-acme` standalone с HTTP-01 после full DNS swap (см. [[../sources/iis-migration-to-ruvds-2026-05-23]] Open questions).
|
||||
- ~~**LE renewal pipeline отсутствует:** certs истекают 2026-07-22~~ **ЗАКРЫТ 2026-06-05** — win-acme v2.2.9 + HTTP-01 развёрнут, новый cert (25-SAN) до 2026-09-03. SYSTEM ScheduledTask renewal раз в 60 дней. Подробности: [[../concepts/winacme-iis-owin-catchall-http01]].
|
||||
- **Home-network HTTP middlebox** (DPI/transparent proxy в OpenWRT/ISP) — mangles Host header для direct external HTTP smoke testing. Real end-users из других сетей не affected. См. [[../concepts/windows-server-2025-core-bootstrap]] Update 2026-05-24.
|
||||
|
||||
## Vendor onboarding
|
||||
@@ -79,4 +101,5 @@ Daily 04:30 MSK → [[kreknin-synology]] via rclone+SFTP. Снимаются: `C
|
||||
- Backup chronology: [[../sources/ruvds-backup-daily-kreknin-2026-05-24]]
|
||||
- Bootstrap recipe: [[../concepts/windows-server-2025-core-bootstrap]]
|
||||
- Cert import recipe: [[../concepts/traefik-acme-json-to-iis-cert-import]]
|
||||
- LE auto-renewal recipe: [[../concepts/winacme-iis-owin-catchall-http01]]
|
||||
- Driver / SPOF rationale: [[../concepts/future-resilient-architecture-goals]]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
title: Snolla Recovery VM (VirtualBox) — savestate'нута 2026-05-21 после 36h успешного soak
|
||||
title: Snolla Recovery VM (VirtualBox) — УДАЛЕНА 2026-06-08
|
||||
type: entity
|
||||
tags: [vm, virtualbox, windows, iis, cms, recovery, savestate]
|
||||
tags: [vm, virtualbox, windows, iis, cms, recovery, savestate, decommissioned]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md, ../sources/iis-host-migration-2026-05-19.md, ../concepts/iis-migration-2026-05-19-postmortem.md]
|
||||
updated: 2026-05-21
|
||||
updated: 2026-06-11
|
||||
---
|
||||
|
||||
> ⚠ **УДАЛЕНА 2026-06-08.** VM `snolla-recovery` удалена в рамках декоммишна [[windows-recovery-host]] (освобождено ~95 ГБ). `unregistervm --delete` выполнен, `.vmdk` снесён. Страница сохранена как историческая. Данные ниже — состояние на 2026-05-21.
|
||||
|
||||
# Snolla Recovery VM
|
||||
|
||||
VirtualBox-VM на [[windows-recovery-host]], в которой работает CMS-стек (IIS + .NET + код CMS). Импортирован из OVA-снапшота 2024-10-27, который лежал в Hyper Backup репо.
|
||||
|
||||
@@ -26,11 +26,16 @@ Production CMS (MoreThenCms) **остаётся на** [`windows-recovery-host`]
|
||||
## Доступ
|
||||
|
||||
- **Public IP:** `89.253.255.94`
|
||||
- **Subnet / gateway:** **/18** (`netmask 255.255.192.0`), gateway `89.253.192.1`. Эта конфигурация **прописывается провайдером** в `/etc/network/interfaces.d/ifcfg-eth0` через их `start/ipadd` procedure — не редактировать руками. См. quirk #9-10 в [`rusonyx-vps-onboarding-quirks`](../concepts/rusonyx-vps-onboarding-quirks.md).
|
||||
- **Сетевой стек:** **`ifupdown` + `networking`** (provider's expected; `netplan` + `systemd-networkd` masked после resolution 2026-05-28 incident — см. [`vds-kzntsv-dhcp-outage-2026-05-28`](../concepts/vds-kzntsv-dhcp-outage-2026-05-28.md))
|
||||
- **Kernel cmdline:** `net.ifnames=0 biosdevname=0` — отсюда `eth0` (не `ens3`/`enp0s3`, эти как altnames в `ip a`)
|
||||
- **DNS resolvers:** `89.253.252.30`, `89.253.252.31` (Rusonyx)
|
||||
- **Vendor hostname:** `vps-21075162-534388.host4g.ru`
|
||||
- **Hypervisor (LLDP-сосед):** `hw80.rusonyx.ru` (нужно знать для тикетов хостеру)
|
||||
- **DNS:** `vds.kzntsv.site` (A → 89.253.255.94) + wildcard `*.vds.kzntsv.site` + service hostnames `git/registry/verdaccio.kzntsv.site` (REGRU)
|
||||
- **VNC console:** через Rusonyx панель (кнопка «Остановить VNC» в Управление сервером → Консоль может потребоваться при stale attachment — см. [`rusonyx-vps-onboarding-quirks`](../concepts/rusonyx-vps-onboarding-quirks.md))
|
||||
- **SSH:** `ssh -i ~/.ssh/id_ed25519 vitya@89.253.255.94` (root login + password auth disabled пост-bootstrap; sudo NOPASSWD для vitya)
|
||||
- **Креды:** `~/projects/.common/secrets/vds-kzntsv.env` (root initial pass, sudo pass, portainer admin, DB passwords, registry, portainer API key, traefik dashboard basicauth)
|
||||
- **Креды:** `pass show vds-kzntsv/full-env` (root initial pass, sudo pass, portainer admin, DB passwords, registry, portainer API key, traefik dashboard basicauth, ntfy, verdaccio CI). *Историческая заметка: до перехода на pass-store креды лежали в `~/projects/.common/secrets/vds-kzntsv.env` — папка более не существует.*
|
||||
|
||||
## Software stack
|
||||
|
||||
@@ -52,6 +57,7 @@ Production CMS (MoreThenCms) **остаётся на** [`windows-recovery-host`]
|
||||
| NPM | verdaccio | 6 | `/opt/stacks/verdaccio/` |
|
||||
| Docker registry | registry | 2.8.3 + joxit UI | `/opt/stacks/registry/` |
|
||||
| Personal cloud | ownCloud Infinite Scale (oCIS) | 7.1.0 | `/opt/stacks/owncloud/` |
|
||||
| CMS DB | MSSQL Express 2022 (Linux) | 2022-latest | `/opt/stacks/databases/mssql/` — TCP via traefik :1433 → `mssql.kzntsv.site`. Подробности: [[../concepts/mssql-on-vds]] |
|
||||
|
||||
## Docker networks (external)
|
||||
|
||||
@@ -66,7 +72,7 @@ Production CMS (MoreThenCms) **остаётся на** [`windows-recovery-host`]
|
||||
| `traefik.vds.kzntsv.site` | Traefik dashboard | basicAuth vitya / Pryakhin9 | Traefik runtime view |
|
||||
| `git.kzntsv.site` | Gitea | kreknin users restored | Git hosting |
|
||||
| `verdaccio.kzntsv.site` | Verdaccio | kreknin htpasswd (vitya) | Private npm |
|
||||
| `registry.kzntsv.site` | Docker Registry | vitya / Pryakhin9 (htpasswd) | Docker images |
|
||||
| `registry.kzntsv.site` | Docker Registry | htpasswd Basic: `vitya` / Pryakhin9; `books-ci` (pass `vds-kzntsv/registry-books-ci`, заведён 2026-06-18). **Standalone `registry:2`, НЕ Gitea-packages** — модель auth + добавление юзеров + GC: [`registry-kzntsv-auth-model`](../concepts/registry-kzntsv-auth-model.md) | Docker images |
|
||||
| `registry-ui.vds.kzntsv.site` | Joxit Registry UI | (proxied к registry, та же auth) | GUI cleanup |
|
||||
| `owncloud.kzntsv.site` | oCIS (ownCloud Infinite Scale) | admin + vitya (pass `owncloud/*`); basic auth enabled для WebDAV/LibreGraph | Personal cloud, replace мёртвой Synology ownCloud (см. [`ocis-on-vds-deploy-recipe`](../concepts/ocis-on-vds-deploy-recipe.md)) |
|
||||
| `postgres.vds.kzntsv.site:5432` | Postgres TLS | postgres / hex32 | Shared DB |
|
||||
@@ -125,10 +131,16 @@ DB TLS: self-signed certs (CN matches hostname), клиент с `verify-none` /
|
||||
- Заменяет старый CMS-инфра-host [`windows-recovery-host`](windows-recovery-host.md) **только для инфраструктурных сервисов** (gitea/verdaccio/registry/DBs); production CMS остаётся на recovery-host.
|
||||
- Не зависит от [`dead-synology-diskstation`](dead-synology-diskstation.md) (тот мёртв).
|
||||
|
||||
## Known issues
|
||||
|
||||
- **2026-05-28 network-stack mismatch (~2.5 ч outage + ~5.5 ч на статике до resolution):** наш netplan+networkd конфликтовал с provider's expected ifupdown stack, поэтому когда DHCP-binding кратко потерялся на стороне хостера — auto-recovery (их `start/ipadd`) не сработала. Resolution: mask netplan/networkd + reboot + provider reset config. Подробности + runbook + lessons-learned: [`vds-kzntsv-dhcp-outage-2026-05-28`](../concepts/vds-kzntsv-dhcp-outage-2026-05-28.md). **Не использовать netplan на этом VDS** (anti-pattern).
|
||||
- **2026-07-13 disk cleanup + container-log rotation guard:** диск поднялся до **94 % (11 GiB free)** — та же болезнь что на [[../entities/books-vds]] 2026-06-04, но guard на infra НЕ стоял. Корень — docker daemon `json-file` без `max-size` + ноль ротации; `owncloud` oCIS натёк **13 GiB** json-лога (остальное: traefik 2.8 GiB, gitea 1.9 GiB; `/var/lib/docker/containers` = 19 GiB). Ручная чистка: truncate логов (19 GiB → 1.2 MiB), `docker builder prune` (-5.5 GiB), `apt-get clean` (-1.2 GiB), `journalctl --vacuum-size=50M` (-0.4 GiB), `docker image prune -a` (-0.35 GiB — старые snolla-теги делят base-слои с running, уникальных мало). Итого **~28 GiB освобождено → 94 % → 74 % (39 GiB free)**. Durable guard: `/etc/logrotate.d/docker-containers` (`copytruncate, size 200M, rotate 3, compress, hourly`) + hourly cron `/etc/cron.d/docker-logrotate` (минута 7). Альтернатива на уровне daemon (`log-opts: max-size=50m, max-file=3` в `/etc/docker/daemon.json`) не применена — потребует рестарта dockerd = рестарт всех 30 контейнеров; logrotate copytruncate выбран как non-disruptive.
|
||||
|
||||
## Open issues / TODO
|
||||
|
||||
- DB TLS = self-signed → нужен LE-cert sidecar (lego watch acme.json → extract PEM → reload DBs). Сейчас клиенты обходятся `verify-none`. [`db-tls-self-signed-via-traefik-raw-tcp`](../concepts/db-tls-self-signed-via-traefik-raw-tcp.md).
|
||||
- Backup pipeline — TODO ([`vds-backup-rsync-kreknin`](../../.tasks/vds-backup-rsync-kreknin.md)).
|
||||
- GC cron для verdaccio + registry — TODO ([`vds-gc-cron`](../../.tasks/vds-gc-cron.md)).
|
||||
- GC cron для verdaccio + registry — TODO ([`vds-gc-cron`](../../.tasks/vds-gc-cron.md)). **Срочность снижена после 2026-07-13 cleanup** (94 % → 74 %, 39 GiB free); container-log rotation — DONE (см. Known issues ↑). Registry + verdaccio GC cron — отдельный TODO, не связан с логами.
|
||||
- ntfy push — TODO ([`vds-ntfy-push`](../../.tasks/vds-ntfy-push.md)).
|
||||
- `iputils-ping` не установлен на host — `apt install iputils-ping` при следующем maintenance, иначе пользоваться `curl` для проверки сети.
|
||||
- Hermes — defer, ждёт уточнения user.
|
||||
|
||||
72
.wiki/entities/vdsina-outline-3xui.md
Normal file
72
.wiki/entities/vdsina-outline-3xui.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: VDSina Outline VPN + 3x-UI node (Amsterdam)
|
||||
type: entity
|
||||
tags: [vds, cloud, netherlands, amsterdam, vpn, outline, shadowsocks, 3x-ui, xray, vless, vdsina]
|
||||
sources: [../sources/vdsina-outline-3xui-inventory-2026-06-24.md]
|
||||
updated: 2026-06-24
|
||||
---
|
||||
|
||||
# VDSina Outline + 3x-UI
|
||||
|
||||
Личный прокси-VDS в Амстердаме (провайдер **VDSina**), основной боевой VPN семьи/друзей.
|
||||
Назначение пользователя: «мой публичный api, он же основной VPN». Два независимых стека на одной машине — **Outline** (Shadowsocks, основной канал клиентов) и **3x-UI / Xray** (VLESS-инбаунды).
|
||||
|
||||
> Не путать с [[nl-vds-3xui]] (`213.176.64.253`, провайдер Aeza) — это **другой** NL-VDS. Этот — VDSina `46.151.25.64`.
|
||||
|
||||
## Hardware / OS
|
||||
|
||||
- **Локация:** Нидерланды, Амстердам ; провайдер VDSina
|
||||
- **Hostname / PTR:** `v1621967.hosted-by-vdsina.ru`
|
||||
- **Public IP:** `46.151.25.64`
|
||||
- **vCPU / RAM / Disk:** 1 (Common KVM) / 1 GiB (965 MiB) / 30 GiB ; **swap 138 MiB**
|
||||
- **Virtualization:** microsoft (Hyper-V) ; **OS:** Ubuntu **20.04.6 LTS** (панель VDSina пишет «Ubuntu 22» — фактически 20.04) ; kernel 5.4.0-216
|
||||
- **План трафика:** 32 TB/мес
|
||||
- **Firewall:** ufw **inactive**, iptables ACCEPT
|
||||
|
||||
## Доступ
|
||||
|
||||
- **SSH:** `root@46.151.25.64` (password auth). Host-key (ed25519): `SHA256:yRFesrmfNhsIeTSQ0ZWM3MWMjWxO0m3JaZsdbmy6Nx4`.
|
||||
- С Windows non-interactive: `plink -ssh -batch -hostkey "SHA256:yRFes...Nx4" -pw <pass> root@46.151.25.64 -m <script>`. Инлайн-команды с `()`/кавычками PowerShell корёжит — заливать скрипт и звать через `-m` (как для [[nl-vds-3xui]]).
|
||||
- **Креды:** `pass show vdsina-outline/full-env`.
|
||||
- **3x-UI панель:** `http://46.151.25.64:50806/ecCqrtFVl4zl5kFjdj/` (webPort 50806, webBasePath `/ecCqrtFVl4zl5kFjdj/`). User `kIqmxpsisp` / pass — в `pass` (получен от пользователя 2026-06-24 через `x-ui` меню; plaintext, не хеш). Access URL по выводу `x-ui` — **http** (cert-файлы в настройках есть, но панель отдаётся по http).
|
||||
- **Outline Manager:** add-server секрет `apiUrl https://46.151.25.64:8080/Kv5DD_ZHm9X3VC_InFY2ww` + `certSha256 2CB0FB47…F919D`.
|
||||
|
||||
## Software stack
|
||||
|
||||
| Слой | Компонент | Деталь |
|
||||
|---|---|---|
|
||||
| OS | Ubuntu | 20.04.6 LTS |
|
||||
| VPN #1 | **Outline / Shadowbox** | docker `quay.io/outline/shadowbox:stable` + `watchtower` (auto-update). Up 2 недели. |
|
||||
| VPN #2 | **3x-UI (x-ui)** | нативный systemd `x-ui.service` (enabled), панель + Xray |
|
||||
| Core | Xray | **25.10.15** (`/usr/local/x-ui/bin/xray-linux-amd64`) |
|
||||
| DB | SQLite `/etc/x-ui/x-ui.db` | sqlite3 CLI на сервере **не установлен** — читать через `python3` модуль sqlite3 |
|
||||
| Metrics | prometheus (`:9090`) + node exporter (`:9091`) | локальные, Outline-метрики |
|
||||
|
||||
## Inbounds / каналы
|
||||
|
||||
### Outline (Shadowsocks) — основной канал
|
||||
- **Порт 443**, `outline-ss-server`, метод `chacha20-ietf-poly1305`. Mgmt API на `:8080`.
|
||||
- **11 access-keys** (id | name): 0 vitya mobile · 1 vitya notebook · 2 natasha mobile · 3 mi box · 4 router · 5 NFS · 6 alexey · 7 планшет · 8 s · 9 natasha notebook · 10 tescha. Пароли/ss-ссылки — `pass`.
|
||||
|
||||
### 3x-UI / Xray (VLESS) — **все `security=none`** (без TLS/Reality)
|
||||
| id | Порт | Proto / network | Клиенты |
|
||||
|---|---|---|---|
|
||||
| 2 | 43666 | VLESS / **xhttp** | 6 (syjgmaqi, 3mgkl84n, c24i9qfc, cy7p02jx, irwnxyul, rv5618li) |
|
||||
| 4 | 46743 | VLESS / **grpc** (serviceName empty) | 1 (lou87iks) |
|
||||
| 3 | 1420 | mixed (socks/http) | локальный |
|
||||
| 1 | 33980 | VLESS | **DISABLED** |
|
||||
|
||||
Sub-сервис включён: port 2096, path `/sub/`.
|
||||
|
||||
## Текущее состояние (live 2026-06-24)
|
||||
|
||||
🟢 Здоров. Up **17 дней** (load 0.02). Disk **26%** (7.3/30 G). Outline + watchtower + x-ui — все `Up`/`active`. Порты 443/8080/50806/2096/43666/46743 слушают.
|
||||
- RAM тесная: 965 MiB, used ~464 MiB, swap 138 MiB (48 used). На 1 GiB при росте клиентов — риск OOM, держать в уме.
|
||||
- В логах x-ui — фоновый шум TLS-handshake error от сканеров (85.217.140.39, 66.132.172.41) на webPort — норма для открытой панели.
|
||||
|
||||
## Подводные камни / заметки
|
||||
|
||||
- **Outline data-port = 443** → классический `:443` занят Shadowsocks, не TLS-сайтом. Для маскировки VLESS под :443 места нет.
|
||||
- VLESS-инбаунды без TLS/Reality (`security=none`) — DPI-детектируемы; основной маскированный/боевой канал тут — **Outline**, не Xray.
|
||||
- sqlite3 CLI отсутствует — для чтения `x-ui.db` использовать `python3 -c` / heredoc.
|
||||
- Provider-панель врёт про версию OS (показывает 22, реально 20.04).
|
||||
@@ -3,13 +3,32 @@ title: Windows Recovery Host (рабочий PC пользователя)
|
||||
type: entity
|
||||
tags: [hardware, windows, docker, virtualbox, iis, recovery, imgproxy]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md, ../sources/iis-migration-to-ruvds-2026-05-23.md]
|
||||
updated: 2026-05-24
|
||||
updated: 2026-06-17
|
||||
---
|
||||
|
||||
# Windows Recovery Host
|
||||
|
||||
Личный Windows-PC пользователя, который во время recovery стал production-сервером для всех клиентских сайтов.
|
||||
|
||||
## ⚠ Состояние после декоммишена (2026-06-08)
|
||||
|
||||
Машина **прибрана**: почти всё уехало на VDS (CMS→RUVDS, MSSQL→[[vds-kzntsv]], MinIO/imgproxy/ES→books VDS), бэкапы настроены, локальный контент удалён. **Разделы «Что хостит сейчас» / «Запущенные docker контейнеры» / «Host IIS configuration» ниже — ИСТОРИЯ (до 2026-06-08), не текущее состояние.**
|
||||
|
||||
**Что осталось на машине сейчас:**
|
||||
- **IIS:** только сайты `stostayer` (:8090) и `stostayer.old` (:8091). Сайт `snolla` + `C:\sites\snolla` удалены. Оба — движок MoreThenCms, **админка по пути `/admin`** (`http://localhost:<port>/admin`). Наружу не светят (traefik routes `.yml.disabled`), доступ только локальный.
|
||||
|
||||
| Сайт | Порт | Connection string → БД | Что за БД |
|
||||
|---|---|---|---|
|
||||
| `stostayer` | `:8090` | `Data Source=www.stostayer.ru,1433` · `Catalog=stostayer` · user `stayer_site` | **внешний прод** SQL Server 2022 (чужая инфра, реальный боевой stostayer); файлы — S3 `minio.stostayer.ru` |
|
||||
| `stostayer.old` | `:8091` | `Data Source=mssql.kzntsv.site,1433` · `Catalog=stostayer` · user `snolla` | **наш** MSSQL на [[vds-kzntsv]] (orphan-user `snolla` починен); файлы — local storage |
|
||||
|
||||
Live-проверка **2026-06-17**: `W3SVC`+`WAS` Running, оба порта `:8090`/`:8091` LISTENING; `:80` (бывший `snolla`) не слушается.
|
||||
- **Docker:** `lightrag-*` + `postgres` (оставлены), `markitdown-mcp`×3 (MCP-инфра агента). Снесены: traefik, mssql, minio, imgproxy(+nginx), elasticsearch + 21 husk-стек. `diskstation/` пуст.
|
||||
- **VirtualBox:** только `mutable-dev-environment` (оставлен). VM `snolla-recovery` (95ГБ) удалена. `C:\nas-recovery` (OVA+vm-sites, ~150ГБ) удалён.
|
||||
- **Диск C:** free **62 → 275 ГБ**. Ещё ~176 ГБ docker-reclaim лежит в WSL2-vhdx — вернётся после `wsl --shutdown` + `Optimize-VHD` (follow-up вне agent-сессии).
|
||||
|
||||
Детали: `.tasks/decommission-windows-recovery-host.md`. image-pipeline SPOF (imgproxy на этой машине) **снят** — `imgproxy.kzntsv.site` теперь на books VDS.
|
||||
|
||||
## Hardware / OS
|
||||
|
||||
- **Hostname:** DESKTOP-NSEF0UK
|
||||
@@ -76,7 +95,7 @@ updated: 2026-05-24
|
||||
- `C:\sites\` — **inert**, готов для следующей попытки миграции (см. [[iis-migration-2026-05-19-postmortem]]):
|
||||
- `snolla\` — Web.config patched (sitePath + conn → localhost)
|
||||
- `stostayer\` — Web.config patched (conn → `www.stostayer.ru,1433` с XML-escape `&`)
|
||||
- `stostayer.old\` — Web.config patched (conn → localhost)
|
||||
- `stostayer.old\` — Web.config conn → `mssql.kzntsv.site,1433` (репойнтнут с `localhost` после миграции MSSQL на [[vds-kzntsv]])
|
||||
- `snolla-identity-manager\` — deploy-артефакт, IIS-сайта нет
|
||||
- `C:\Users\vitya\projects\MoreThenCms\` — git-репо с исходниками CMS
|
||||
|
||||
|
||||
@@ -8,41 +8,69 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
|
||||
|
||||
## Entities
|
||||
|
||||
- [books-vds](entities/books-vds.md) — Books VDS (89.253.255.133 — host4g.ru, client server hosting books app + shared mongo/minio/elasticsearch)
|
||||
- [dead-synology-diskstation](entities/dead-synology-diskstation.md) — мёртвая Synology DiskStation (source NAS)
|
||||
- [kreknin-synology](entities/kreknin-synology.md) — Kreknin Synology (backup target + DDNS)
|
||||
- [nl-vds-3xui](entities/nl-vds-3xui.md) — NL VDS 3x-UI VLESS/Reality node (213.176.64.253) — Reality 443 сломан (PQ×intel), plain VLESS 32030 работает
|
||||
- [de-vds-3xui](entities/de-vds-3xui.md) — DE VDS 3x-UI VLESS node (130.17.17.158, Fornex Germany) — plain VLESS 32030 security=none; 3x-ui 3.5.0 gotchas (CSRF на POST, client-tables, binary-only setting)
|
||||
- [openwrt-router](entities/openwrt-router.md) — OpenWRT Router (192.168.1.1)
|
||||
- [ruvds-iis-host](entities/ruvds-iis-host.md) — RUVDS IIS Host (Win Server 2025 Core, 80.64.31.36) — CMS catch-all destination
|
||||
- [snolla-recovery-vm](entities/snolla-recovery-vm.md) — Snolla Recovery VM (VirtualBox, savestate'нута 2026-05-21 после 36h soak)
|
||||
- [vds-kzntsv](entities/vds-kzntsv.md) — VDS kzntsv (Rusonyx 160 NVMe cloud server)
|
||||
- [vdsina-outline-3xui](entities/vdsina-outline-3xui.md) — VDSina Amsterdam VPN (46.151.25.64) — Outline (основной, 11 keys, :443) + 3x-UI/Xray VLESS; основной боевой VPN семьи
|
||||
- [windows-recovery-host](entities/windows-recovery-host.md) — Windows Recovery Host (рабочий PC пользователя)
|
||||
|
||||
## Concepts
|
||||
|
||||
|
||||
- [admin-infra-project](concepts/admin-infra-project.md) — design + migration plan для OpeItcLoc03/admin (canonical)
|
||||
- [books-ssh-access](concepts/books-ssh-access.md) — SSH access audit на shared VDS (books Slovo+Bookva), retained keys + add/revoke processes
|
||||
- [backup-inventory-2026-06](concepts/backup-inventory-2026-06.md) — карта всех машин × что реально бэкапится (с доказательством); дыры по приоритету (kreknin SPOF, nl-vds x-ui.db, bookva-*, openwrt) — триггер: MSSQL 3-нед gap
|
||||
- [bindmount-config-edit-preserve-mode](concepts/bindmount-config-edit-preserve-mode.md) — правка bind-mounted config'а через mktemp+mv роняет режим 644→600 → non-root контейнер (uid 1000) EACCES crash-loop; chmod --reference=backup. + гоча: bind-mount затеняет config образа целиком → класть полную секцию, не дельту. Инциденты books-job-scheduler + task-runner 2026-06-18
|
||||
- [vds-kzntsv-ssh-access](concepts/vds-kzntsv-ssh-access.md) — SSH access audit на vds-kzntsv (infra VDS), retained keys + add/revoke processes (was misnamed `books-ssh-access` до 2026-05-25 — content всегда был про vds-kzntsv)
|
||||
- [compose-bcrypt-escape-trap](concepts/compose-bcrypt-escape-trap.md) — Docker Compose ест `$` в bcrypt hashes
|
||||
- [db-tls-self-signed-via-traefik-raw-tcp](concepts/db-tls-self-signed-via-traefik-raw-tcp.md) — DB TLS через traefik raw TCP с self-signed certs
|
||||
- [docker-host-loopback-detect](concepts/docker-host-loopback-detect.md) — Docker host-loopback detection — как доказать что host.docker.internal не петля
|
||||
- [emspb-vds-deploy-runbook](concepts/emspb-vds-deploy-runbook.md) — emspb.ru snolla-app на VDS (стек 18): сборка/env/cutover 07-02 + **0.42.1 in-place bump 07-05** (образ `emspb:95a5c42`)
|
||||
- [es-destructive-delete-incident-2026-05-26](concepts/es-destructive-delete-incident-2026-05-26.md) — ES indices wiped on canonical — **true root cause (2026-05-29): ransom-бот через открытый `0.0.0.0:9200` мимо traefik, free-ES без auth**; гипотеза operator-error опровергнута. Fix = убрать публикацию host-порта (Control #3) + restore. Exposure-audit: mongo/mariadb/minio тоже exposed (credentialed)
|
||||
- [future-resilient-architecture-goals](concepts/future-resilient-architecture-goals.md) — fault-tolerance roadmap placeholder (расширяется через `[resilience-roadmap-design]`)
|
||||
- [hyper-backup-structure-and-recovery](concepts/hyper-backup-structure-and-recovery.md) — Hyper Backup — структура репо и стратегия восстановления
|
||||
- [iis-migration-2026-05-19-postmortem](concepts/iis-migration-2026-05-19-postmortem.md) — post-mortem миграции CMS на нативный IIS, 2026-05-19
|
||||
- [labtools-vds-deploy-runbook](concepts/labtools-vds-deploy-runbook.md) — labtools.ru snolla-app на VDS (стек 17): сборка/env/cutover 07-02 (Yandex DNS!) + **0.42.1 in-place bump 07-05** (образ `labtools:566d41c`, order-фикс + sitemap починен)
|
||||
- [labtools.pro-vds-deploy-runbook](concepts/labtools.pro-vds-deploy-runbook.md) — labtools.pro snolla-каталог на VDS (стек 19): сборка/env/cutover 07-02 + **0.42.1 in-place bump 07-05** (образ `labtools-pro:0610432`, order-парити 3 секции)
|
||||
- [tandemmebel-vds-deploy-runbook](concepts/tandemmebel-vds-deploy-runbook.md) — tandemmebel.ru snolla-блог-портфолио на VDS (стек 20): cutover 07-12 (DNS reg.ru, LE) + **0.42.1 in-place bump 07-12** (образ `tandemmebel:0cd9351`, 184/184 parity, /articles 404 benign)
|
||||
- [minio-imgproxy-on-vds](concepts/minio-imgproxy-on-vds.md) — MinIO+imgproxy stack на VDS, миграция с upgrade 2020→2025 + DNS swap pending
|
||||
- [morethencms-null-settingsdata-https-502](concepts/morethencms-null-settingsdata-https-502.md) — MoreThenCms тенант с `Sites.SettingsData=NULL` → 502 только на HTTPS (нет `httpSecure` → KeyNotFound); fix = UPDATE + recycle. maljarka 2026-06-08
|
||||
- [snolla-admin-appdata-acl-500-after-scp-migration](concepts/snolla-admin-appdata-acl-500-after-scp-migration.md) — админский delete/upload ассета → 500: app pool имел на `App_Data` только RX после scp-миграции → `File.Delete` UnauthorizedAccessException. Fix = icacls Modify. + split-brain: админка пишет Local, боевой app читает MinIO
|
||||
- [mssql-container-data-restore](concepts/mssql-container-data-restore.md) — MSSQL контейнер с восстановленными production data — паттерн
|
||||
- [mssql-on-vds](concepts/mssql-on-vds.md) — MSSQL Express 2022 Linux на VDS — миграция + login orphan fix + traefik TCP gotchas
|
||||
- [ocis-on-vds-deploy-recipe](concepts/ocis-on-vds-deploy-recipe.md) — oCIS на VDS — deploy recipe + non-obvious gotchas (UID 1001 vs 1000, basic auth, LibreGraph user-create)
|
||||
- [portainer-2.21-admin-password-regression](concepts/portainer-2.21-admin-password-regression.md) — Portainer 2.21 `--admin-password` regression + min 12-char policy
|
||||
- [portainer-stack-management-vds](concepts/portainer-stack-management-vds.md) — Portainer-managed stacks на VDS — canonical pattern + migration script + gotchas
|
||||
- [portainer-stack-management-books-vds](concepts/portainer-stack-management-books-vds.md) — Portainer-managed stacks на books VDS — pattern application + migration log 2026-05-25
|
||||
- [portainer-stack-management-vds](concepts/portainer-stack-management-vds.md) — Portainer-managed stacks на VDS — canonical pattern + migration script + stack-redeploy recipe (новый тег) + gotchas (вкл. PS 5.1 ISO-8859-1 коррапт кириллицы при API round-trip)
|
||||
- [snolla-live-prod-inplace-image-bump](concepts/snolla-live-prod-inplace-image-bump.md) — переиспользуемый рецепт обновления образа на ЖИВОМ snolla-стеке VDS in-place (build→throwaway-staging-acceptance С VDS→env-preserving Portainer PUT→live-smoke); вкл. `put-stack.js`; отработан на тираже 0.42.1 (4 сайта: labtools/emspb/labtools.pro 2026-07-05 + tandemmebel 2026-07-12)
|
||||
- [stostayer-web-deploy-runbook](concepts/stostayer-web-deploy-runbook.md) — деплой легаси web (`www.stostayer.ru`) на прод клиента: build→registry→Portainer-стек 16 через container-IP API с хоста; **БЛОКЕР: легаси не пересобрать с master (ESM-стена: top-level await @stostayer/data на build, ERR_REQUIRE_ESM @snollajs/snolla в рантайме)**; гоча — один push без retry (VPN-IP банится хостером)
|
||||
- [stostayer-admin-minio-config](concepts/stostayer-admin-minio-config.md) — локальная .NET-админка stostayer (:8090) на stostayer MinIO: креды НЕ в pass (в stostayer.new config), endpoint `minio-api.stostayer.ru` (minio.stostayer.ru — не API-порт), region `us-west-1` (не local); fix 2026-07-22 — 3 замены в Web.config, админка=прод единое хранилище
|
||||
- [proxy-debugging-test-the-real-client](concepts/proxy-debugging-test-the-real-client.md) — анти-паттерн: свои curl/standalone-тесты «работают», а боевой клиент пользователя нет; источник истины — реальный клиент
|
||||
- [reality-pq-mldsa65-dest-incompatibility](concepts/reality-pq-mldsa65-dest-incompatibility.md) — REALITY+ML-DSA-65 (PQ) не работает с не-PQ dest (Akamai/intel шлёт HRR); fix = PQ-совместимый dest или выключить PQ; **caveat: снятие PQ ≠ рабочий Reality у GUI-клиентов**
|
||||
- [recovery-architecture-snapshot](concepts/recovery-architecture-snapshot.md) — текущая recovery architecture (2026-05-19/21, attempt 2)
|
||||
- [registry-gc-mount-and-modify-flag](concepts/registry-gc-mount-and-modify-flag.md) — Docker Registry GC mount layout + `-m` flag
|
||||
- [registry-kzntsv-auth-model](concepts/registry-kzntsv-auth-model.md) — registry.kzntsv.site = standalone registry:2 + htpasswd Basic (бинарный доступ, НЕ Gitea-packages, нет per-repo ACL/robot-токенов); как заводить htpasswd-юзеров (hot-reload), GC через v2 DELETE; юзеры vitya + books-ci
|
||||
- [registry-oci-image-index-gc](concepts/registry-oci-image-index-gc.md) — books-* образы в registry = OCI image-index (buildx multi-manifest): top-level `.config`=null, дата `.created` живёт в платформенном sub-manifest; наивный GC по top-level дате → null у всех → no-op (keepLastN не применяется). DELETE по index-digest, дата из sub. + **dangling-индексы**: 16/19 тегов books-web — тег жив, sub-manifest `MANIFEST_UNKNOWN` (вычищен прежним host-GC); date-GC защищает их как null-dated → не чистит, хотя они и есть мусор. Зафиксировано на books registryGc dryRun 2026-06-18
|
||||
- [rusonyx-vps-onboarding-quirks](concepts/rusonyx-vps-onboarding-quirks.md) — Rusonyx VPS onboarding quirks (Astra Облако / myvm.rusonyx.ru)
|
||||
- [traefik-acme-json-to-iis-cert-import](concepts/traefik-acme-json-to-iis-cert-import.md) — Экспорт LE certs из traefik acme.json в IIS (PFX + SNI bindings)
|
||||
- [traefik-file-watch-wsl2-broken](concepts/traefik-file-watch-wsl2-broken.md) — Traefik file-watch broken под Docker Desktop Windows (WSL2 9p mount)
|
||||
- [traefik-on-windows-docker-desktop](concepts/traefik-on-windows-docker-desktop.md) — Traefik на Windows Docker Desktop — нюансы
|
||||
- [traefik-tcp-passthrough-vs-starttls](concepts/traefik-tcp-passthrough-vs-starttls.md) — Traefik TCP passthrough vs STARTTLS-protocols
|
||||
- [vbox-windows-stability-tuning](concepts/vbox-windows-stability-tuning.md) — VirtualBox + Windows-гость — нюансы стабильности cross-hypervisor миграции
|
||||
- [vds-kzntsv-dhcp-outage-2026-05-28](concepts/vds-kzntsv-dhcp-outage-2026-05-28.md) — DHCP outage 2026-05-28 на vds-kzntsv — диагностика + recovery runbook (link UP но без IPv4 → статика + тикет хостеру)
|
||||
- [verdaccio-prune-semantics](concepts/verdaccio-prune-semantics.md) — Verdaccio prune semantics — proxied vs locally-published
|
||||
- [verdaccio-restore-packument-desync](concepts/verdaccio-restore-packument-desync.md) — disaster-restore: тарболл на диске + древний packument → EEXISTS 409; fix = снять коллизирующий .tgz, не rm -rf каталог
|
||||
- [verdaccio-token-lifecycle](concepts/verdaccio-token-lifecycle.md) — restart trap (ephemeral secret) + max_users:-1 pnpm 409 + JWT fix + yarn clients
|
||||
- [yarn-npm-minimal-age-gate](concepts/yarn-npm-minimal-age-gate.md) — Yarn ≥4.16 client-side age-gate (YN0016 «quarantined») на версиях <24ч; глобальный, не per-scope; fix npmMinimalAgeGate:0
|
||||
- [winacme-iis-owin-catchall-http01](concepts/winacme-iis-owin-catchall-http01.md) — win-acme HTTP-01 авто-renewal LE на IIS под OWIN-catch-all CMS — challenge в отдельном No-Managed-Code приложении + патч шаблона; заменяет ручной PFX-импорт
|
||||
- [wd40efax-smr-cascade](concepts/wd40efax-smr-cascade.md) — WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18
|
||||
- [windows-server-2025-core-bootstrap](concepts/windows-server-2025-core-bootstrap.md) — Win Server 2025 Core (RUVDS) — bootstrap для IIS-хоста: default-blockers + transfer-методов матрица
|
||||
- [mcp-init-resilience](concepts/mcp-init-resilience.md) — mcp-init-resilience
|
||||
|
||||
## Packages
|
||||
|
||||
@@ -53,5 +81,10 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
|
||||
- [iis-host-migration-2026-05-19](sources/iis-host-migration-2026-05-19.md) — IIS Host Migration Session 2026-05-19 chronology
|
||||
- [iis-migration-to-ruvds-2026-05-23](sources/iis-migration-to-ruvds-2026-05-23.md) — IIS migration to RUVDS — session 2026-05-23/24 (SSH/scp pivot, 25 SNI bindings, partial DNS cutover)
|
||||
- [nas-recovery-session-2026-05-18](sources/nas-recovery-session-2026-05-18.md) — NAS Recovery Session 2026-05-18/19 chronology
|
||||
- [books-vds-backup-daily-kreknin-2026-05-25](sources/books-vds-backup-daily-kreknin-2026-05-25.md) — books VDS daily backup → kreknin pipeline standup 2026-05-25 (ES file-snapshot repo + DB dumps + curl SMTP)
|
||||
- [ruvds-backup-daily-kreknin-2026-05-24](sources/ruvds-backup-daily-kreknin-2026-05-24.md) — RUVDS daily backup → kreknin pipeline standup 2026-05-24 (rclone+SFTP, SYSTEM task)
|
||||
- [nl-vds-3xui-setup-2026-06-05](sources/nl-vds-3xui-setup-2026-06-05.md) — NL VDS 3x-UI setup+troubleshooting session; итог: у реальных клиентов из РФ работает только plain VLESS 32030 (Reality/MTProto/SOCKS не поднялись)
|
||||
- [de-vds-3xui-setup-2026-07-14](sources/de-vds-3xui-setup-2026-07-14.md) — DE VDS (Fornex Germany) setup session; 3x-ui 3.5.0 gotchas: CSRF на panel POST, прямой DB-insert не рендерит (client-tables), wrapper x-ui setting не применяет; plain VLESS 32030 confirmed на real RF-клиенте
|
||||
- [vdsina-outline-3xui-inventory-2026-06-24](sources/vdsina-outline-3xui-inventory-2026-06-24.md) — VDSina Amsterdam (46.151.25.64) inventory: Outline 11 keys + 3x-UI VLESS, секреты в pass vdsina-outline
|
||||
- [vds-kzntsv-bootstrap-2026-05-20](sources/vds-kzntsv-bootstrap-2026-05-20.md) — VDS bootstrap session 2026-05-20 chronology
|
||||
- [vds-kzntsv-incident-2026-05-28](sources/vds-kzntsv-incident-2026-05-28.md) — DHCP outage incident session 2026-05-28 — chronology + ticket text
|
||||
|
||||
87
.wiki/log.md
87
.wiki/log.md
@@ -2,6 +2,65 @@
|
||||
|
||||
Append-only log of wiki operations (ingests, promotions, lints, migrations).
|
||||
|
||||
## [2026-07-22] fix | NEW concepts/stostayer-admin-minio-config.md — локальная .NET-админка stostayer (`C:\sites\stostayer\Web.config`, IIS :8090) переведена на рабочий stostayer MinIO. Исходно S3-провайдеры (`MoreThenCms.FileStorage.S3` в `bin/`) уже стояли, но с 3 багами: (1) чужие креды `AKIAJ2YJP72W6ZHCRE6Q` = books-vds/snolla root-ключ (копипаст из snolla catch-all admin, не заменён на stostayer-свои → "Access Key Id does not exist"); (2) `serviceURL=https://minio.stostayer.ru` — НЕ S3 API-порт ("S3 API Requests must be made to API port"), прод юзает `minio-api.stostayer.ru`; (3) `authenticationRegion="local"` → SigV4 регион-мismatch → `AmazonS3Exception: the region is wrong; expecting 'us-west-1'` (400) на `GalleriesStorage.EnsureKeyPrefixExists` (`ToolsController.ImagePreview` → `CreatePreview`). Креды stostayer MinIO **не в pass** — лежат в `~/projects/stostayer.new/packages/web/config/default.json` → `s3`: accessKey `stayer_minio`, endpoint `https://minio-api.stostayer.ru`, region `us-west-1`. Fix: 3 глобальные замены × 9 S3-блоков в Web.config (accessKey/secretKey[XML-escaped `>`/`&`]/serviceURL) + region `local`→`us-west-1`; бэкап `Web.config.bak-2026-07-22`; IIS recycle, лог чист. mc `alias set` + ListBuckets OK (10 бакетов: assets/galleries/themes/…); `assets/9cf0a8e52cf144619fd290606a146d35/` несёт реальные файлы. Acceptance оператором: картинки грузятся. Админка=прод единое хранилище, ручное зеркалирование в `App_Data` упразднено. +index.md concepts-строка. Cross-ref [[galleries-storage-class-local-not-s3]], [[snolla-admin-appdata-acl-500-after-scp-migration]], [[minio-imgproxy-on-vds]].
|
||||
|
||||
## [2026-07-14] ingest | sources/de-vds-3xui-setup-2026-07-14.md (new) + entities/de-vds-3xui.md (sources: привязан) — полноценный ingest DE-сессии: sources-хроника с борьбой против 3x-ui 3.5.0 (CSRF-403 на login → `CSRFMiddleware`/`X-CSRF-Token`/`GET {BP}csrf-token`; прямой DB-insert в `inbounds` не рендерит → client-tables `clients`/`client_inbounds`/`client_traffics` заполняются только `AddInbound` → panel API `POST {BP}panel/api/inbounds/add` с stringified settings/streamSettings/sniffing; wrapper `x-ui setting` не применяет креды → бинарник). Эталон JSON снят с рабочего nl-vds 32030. +index.md sources-строка. Real RF-client confirmed.
|
||||
|
||||
## [2026-07-14] decision | entities/de-vds-3xui.md — real-client из РФ подтверждён (user подключён через 32030, сессия идёт через узел). Plain VLESS = proven-working на обоих NL+DE. Задача [de-vds-3xui-setup] закрыта. Перекрестил `Проверено` блок ✅.
|
||||
|
||||
## [2026-07-14] ingest | entities/de-vds-3xui.md (new) — поднят VPN-VDS в Германии (Fornex `130.17.17.158`, Ubuntu 24.04, 1/2G/20G), аналог [[nl-vds-3xui]]. Повторён **только proven-working plain VLESS 32030** `security=none` (на NL у боевых клиентов из РФ заработал только он); Reality/MTProto/SOCKS5 намеренно НЕ подняты. Креды → `pass de-vds-3xui/full-env`. 3x-ui тут **3.5.0** (на NL была 3.2.7) — вскрыл новые гоча, зафиксированы в entity: (1) **CSRF на всех panel POST** (`CSRFMiddleware` → 403 пустое тело+CSP-nonce без заголовка `X-CSRF-Token`; токен — `GET {BP}csrf-token`; login = `POST {BP}login` JSON); (2) **прямой INSERT в `inbounds` НЕ рендерит инбаунд в 3.5.0** — клиентская модель разнесена по `clients`/`client_inbounds`/`client_traffics`, заполняется только сервисом `AddInbound` → инбаунд надо создавать через panel API `POST {BP}panel/api/inbounds/add` (body=model.Inbound camelCase, settings/streamSettings/sniffing stringified); (3) **wrapper `x-ui setting -username/-password` НЕ применяет** — только бинарник `/usr/local/x-ui/x-ui setting` (после `systemctl stop x-ui`). Server-side e2e подтверждён (xray слушает 32030, exit=сервер); реальный РФ-клиент ждёт проверки user. +index.md.
|
||||
|
||||
## [2026-07-12] deploy | tandemmebel.ru in-place bump 0.42.0→0.42.1 LIVE (стек 20) — поверх cutover'а того же дня (стек уже боевой, образ `ed96b18`/0.42.0). Консюмер-бамп сделан оператором сам (тот же блокер-паттерн 0.42.0 2026-07-04 — теперь решён без запроса dev-source): пин `apps/web/package.json:12` 0.42.0→0.42.1 + `yarn install` (lock snolla 0.42.1/core 0.24.1/liquid 0.10.2/data 0.14.1) + commit `0cd9351` + push origin (ls-remote confirmed). Verdaccio: 0.42.1 published (latest). Build на VDS → `registry.kzntsv.site/tandemmebel:0cd9351` (digest f29c187f). Throwaway-staging :5020 из env живого стека, healthy. Completeness-gate С VDS: **184/184 parity** (NEW==PROD, вкл. 0.42.x sitemap-реструктуризацию), единственный 404 `/articles` идентичен прод-оракулу → benign. Operator-gated PUT стека 20 (node put-stack.js, env 8/8 preserve, prune:false pullImage:true) → контейнер 0cd9351+healthy ~8s. Live-smoke GREEN (robots/home/projects/sitemap 200, TLS-серт CN не дёрнут). 0.42.1 = order-tag Drop-field fix, инертен на блог-портфолио (без e-commerce каталога). NEW concepts/tandemmebel-vds-deploy-runbook.md + UPDATE concepts/snolla-live-prod-inplace-image-bump.md (тираж 0.42.1 = 4 in-place bump-сайта) + host-stacks/vds-kzntsv/tandemmebel.compose.yml (тег+комментарий 0.42.1) + index.md. Rollback = тег ed96b18 в registry (+ b02ca18). **Тираж snolla 0.42.1 закрыт полностью.**
|
||||
|
||||
## [2026-07-12] deploy | tandemmebel.ru cutover LIVE — последний из тиража snolla 0.42.x. DNS reg.ru `tandemmebel.ru`+`www`→89.253.255.94 (verify ns1/ns2.reg.ru оба), traefik Host-rule staging→боевой на стеке 20 (env 8/8 preserve, prune:false, pullImage:false — образ `ed96b18`/0.42.0 не менялся, без пересборки по решению оператора). LE-серт issued on first hit: CN=tandemmebel.ru, SAN оба, until 2026-10-10. Live-smoke С VDS GREEN: key pages 200, sitemap 184 page-locs, sharp media webp 200, robots из БД. Gotham-Pro.css 0B→4436B (латентный прод-баг починен). Rollback = revert DNS→80.64.31.36 ИЛИ PUT staging-host. UPDATE entities/ruvds-iis-host (tandemmebel→🗑️ cutover VDS, биндинги оставлены как rollback) + host-stacks/vds-kzntsv/tandemmebel.compose.yml (rule боевой, заголовок live). **Тираж snolla 0.42.x полностью живой (5/5 стеков на VDS).**
|
||||
|
||||
## [2026-07-03] ingest | NEW concepts/snolla-admin-appdata-acl-500-after-scp-migration — разведка «почему `/admin/assets/<owner>/delete` → 500» на RUVDS IIS. Корень: app pool `IIS AppPool\snolla` (ApplicationPoolIdentity) имел на `C:\sites\snolla\App_Data` только `(RX)` после scp-миграции 23.05 → `File.Delete` в `AssetsService.DeleteAsset` кидает UnauthorizedAccessException → необработанное 500 (event log пуст, IIS substatus 0/win32 0). NRE-версии исключены проверкой строк в БД (Folders root + Files). Fix: `icacls App_Data /grant "IIS AppPool\snolla:(OI)(CI)(M)" /T` (44293 файла, verified `(I)(M)` на таргете). Класс шире delete — все Local-записи из админки (upload/кэши/галереи). Также: заменён `labtools-price.pdf` (owner d375c419) в ОБА хранилища (MinIO `assets/…` ETag→6426ddd0 + локалка IIS, MD5 сверены). Split-brain админка(Local)↔боевой app(MinIO) — cross-ref [[concepts/galleries-storage-class-local-not-s3]].
|
||||
|
||||
## [2026-06-29] deploy | pilonuxt `cf2bba2` LIVE (стек 16) — ADR-0010: формы переехали с monolith `@snollajs/snolla` на `@snollajs/forms-api`, snolla **дропнут целиком**, core@0.1.1 (Buffer вместо btoa). Тот же build-пайплайн (форс-регенерация клиента + save|ssh load + push с VDS). **Новый обязательный шаг — tree-check `.output/server/node_modules` ПЕРЕД перекатом** (вся сага content-api↔snolla про dual-instance, dev маскирует unmet-deps): `docker run --rm --entrypoint sh <img> -c 'find /app/.output/server/node_modules ...'` → подтвердил 0× snolla, 0× btoa, ровно 1× data@0.9.1, core 0.1.1 + content-api 0.14 + forms-api 0.1.0. Smoke: SSR `/`+`/catalog`→200 (= btoa/snolla-дроп бандл не сломал), robots/yandex из БД (ADR-0009 не регресс), форма `POST /snolla-forms/forms?path=/checkout` пустой→422 JSON (valid НЕ слал — реальное письмо менеджеру), `/nope`→404. **Гоча smoke-харнеса:** forms-api отдаёт 422 только при `Accept: application/json` (иначе `prefersJson(req)` false → ветка `res.status(result.status||404).end()` даёт 404 — это не баг приложения, а отсутствие Accept в curl). Источник: наряд [deploy-pilonuxt-forms-api-drop-snolla]. См. [[concepts/portainer-stack-management-vds]] § Stack redeploy.
|
||||
|
||||
## [2026-06-29] deploy | pilonuxt `40bb383` LIVE (стек 16) — DB-backed staticPages + robots.txt (ADR-0009). Build из монорепы victor/pilorama98.ru: ключевой шаг — **форс-регенерация gitignored-клиента** `apps/web/src/generated/` (`rm -rf` + `yarn workspace nuxt-app schema-gen` против live MSSQL), иначе ensure-schema skip-if-present собрал бы старый клиент без `getStaticPage`/`getRobotsTxt` → server-500. Push с дома повторил traefik-499 на `.output`-слое даже при образе 425МБ (не multi-GB) → канонический fallback `docker save | ssh vds 'docker load'` + push с VDS (registry локален) сработал. Перекат через Portainer PUT стек 16 (`pullImage:true`, байты+UTF8 gotcha #9). Smoke acceptance 6/6 телами: robots полный из БД (Yandex Clean-param + Sitemap, не статика), yandex/google верификации 200, /nope→Nuxt-404, /+/catalog→200. Источник: наряд [deploy-pilonuxt-static-pages-robots]. См. [[concepts/portainer-stack-management-vds]] § Stack redeploy.
|
||||
|
||||
## [2026-06-24] ingest | entities/vdsina-outline-3xui.md (new) + sources/vdsina-outline-3xui-inventory-2026-06-24.md (new) — задокументирован ранее неизвестный VDS: VDSina Амстердам `46.151.25.64` (`v1621967.hosted-by-vdsina.ru`), «основной боевой VPN» семьи. Два стека на одной машине: Outline/Shadowbox (docker, :443 chacha20, 11 access-keys, mgmt :8080) + 3x-UI/Xray 25.10.15 (панель :50806 `/ecCqrtFVl4zl5kFjdj/`, VLESS xhttp :43666 / grpc :46743, все security=none). Ubuntu 20.04.6 (панель VDSina врёт «22»), 1 vCPU/1 GiB/30 GiB, up 17д, healthy. Секреты → `pass vdsina-outline/full-env` (host-key, root pass, Outline mgmt apiUrl+cert+11 keys, panel user+secret). Открытое: пароль панели — только bcrypt-хеш, plaintext не извлечён. Кросс-линк добавлен в [[nl-vds-3xui]] (другой NL-VDS, Aeza). +index.md (2 строки). Источник: SSH-инвентаризация 2026-06-24.
|
||||
|
||||
## [2026-06-18] ingest | concepts/registry-oci-image-index-gc.md (раздел real-run) — первый боевой registryGc dryRun:false на dangling-фиксе (books master-289c660, под добро юзера): 63 DELETE, 0 ошибок, 0×405 → REGISTRY_STORAGE_DELETE_ENABLED=true подтверждён живьём; 61 dangling + 2 datable снесены, реестр почищен (web 20→4 и т.д.). Побочка-урок: master у books-api/books-ops-mcp был dangling (не пересобирались ~3нед) → снесён → :master 404 (outage нет, контейнеры на локальных образах, но redeploy упрётся). Политика: не удалять named-теги master/latest даже dangling (protectRe → +master/latest); проверить keep/drop digest-коллизию. Источник: real run + post-state probe books-* :master.
|
||||
|
||||
## [2026-06-18] ingest | concepts/registry-oci-image-index-gc.md (раздел dangling-индексы) — перепрогон registryGc dryRun на descent-фиксе (master-dcd7c91) вскрыл 2-й root-cause: реестр засорён dangling image-индексами (тег жив, платформенный sub-manifest отдаёт MANIFEST_UNKNOWN — вычищен прежним host-side `registry garbage-collect`, не следящим index→child для multi-arch). books-web: 19 тегов → 3 датируемых, 16 dangling. date-GC защищает их как null-dated → не удаляет, хотя они и есть мусор (логика задом наперёд). Рекомендация books: различать transient-error (protect) vs MANIFEST_UNKNOWN (eligible). freedBytes от dangling ~0 (слои уже вычищены). +index.md hook. Источник: dryRun #2 + полный manifest-обход books-web 2026-06-18.
|
||||
|
||||
## [2026-06-18] ingest | concepts/registry-oci-image-index-gc.md (new) + concepts/bindmount-config-edit-preserve-mode.md (расширен) — следствие закрытия [books-task-runner-registry-auth-cred]. (1) Новый концепт: books-* образы в registry = OCI image-index (buildx), top-level `.config`=null, дата `.created` в платформенном sub-manifest → наивный GC по top-level дате даёт null у всех → планировщик защищает null-dated группы → drop=0 всегда (keepLastN не применяется). Правила: Accept со всеми media-types, дата из sub, DELETE по index-digest (не sub), 1 версия=1 index+attestation. Зафиксировано на books registryGc dryRun (deleted=0 при 14 tags). (2) bindmount-постмортем дополнен: bind-mount затеняет config-каталог образа целиком → нужна полная кред-секция, не дельта; + worked example task-runner registry-секция (mode не слетел, постмортем сработал). +index.md (2 строки). Источник: dryRun-верификация registryGc + манифест-dump books-web 2026-06-18.
|
||||
|
||||
## [2026-06-18] ingest | concepts/registry-kzntsv-auth-model.md (new) + concepts/bindmount-config-edit-preserve-mode.md (new) — registry.kzntsv.site = standalone registry:2 + htpasswd Basic (бинарный доступ, НЕ Gitea-packages; нет per-repo ACL/robot-токенов; hot-reload htpasswd; GC через v2 DELETE + host garbage-collect; завёл юзера books-ci для books job-scheduler pull, кред в pass vds-kzntsv/registry-books-ci). Постмортем: правка bind-mounted `default.json` через mktemp+mv уронила режим 644→600 → `books-job-scheduler` EACCES crash-loop (прод-даун slovo-cron), fix chmod/chown --reference=backup. +entities/vds-kzntsv.md (registry-строка) +index.md. Источник: консультация+инфра-деплой books registry-auth + инцидент 2026-06-18.
|
||||
|
||||
## [2026-06-17] ingest | concepts/stostayer-web-deploy-runbook.md (new) — ранбук деплоя легаси web на прод stostayer: канал build(offline)→push docker.stostayer.ru→Portainer-стек 16 через container-IP API (172.18.0.2:9000) с хоста мимо Angie BA; rollback; БЛОКЕР ESM-стена (top-level await @stostayer/data на build + ERR_REQUIRE_ESM @snollajs/snolla в рантайме d02f740) → 0.3.18 остаётся; гоча VPN-IP бан хостером при retry-push-шторме. Источник: попытка деплоя `stostayer-web-complaint-form-deploy` 2026-06-17 (откат на 0.3.18). +index.md.
|
||||
|
||||
## [2026-06-17] update | entities/windows-recovery-host.md — уточнён live-state двух stostayer-IIS-сайтов: добавлена таблица порт→connection string→БД (`stostayer` :8090→`www.stostayer.ru,1433` внешний прод + S3 minio.stostayer.ru; `stostayer.old` :8091→`mssql.kzntsv.site,1433` наш VDS), путь админок `/admin`, live-проверка 2026-06-17 (W3SVC Running, :8090/:8091 LISTENING, :80 нет). Пофикшена стейловая строка в «Ключевые папки» (stostayer.old conn localhost→mssql.kzntsv.site). Источник: live-аудит IIS на DESKTOP-NSEF0UK.
|
||||
|
||||
## [2026-06-17] update | concepts/portainer-stack-management-vds.md +раздел «Stack redeploy (новый image tag)» (JWT→GET file байтами→подмена тега→PUT pullImage:true, env=[] плейсхолдером, тело UTF-8) + gotcha #9 (PS 5.1 Invoke-RestMethod декодит /file как ISO-8859-1 → кириллические compose-комментарии mojibake → PUT падает `yaml: could not find expected ':'`; fix = байты+UTF8) + #10 (пустой env схлопывается). Источник: redeploy-pilonuxt-gsc-structured-data (19a4a84 в проде, stack 16).
|
||||
|
||||
## [2026-06-14] update | concepts/minio-imgproxy-on-vds.md +раздел «S3 access для клиентских приложений» (lookup: endpoint `https://minio.kzntsv.site` / сырой `89.253.255.133:9000` / inter `http://minio:9000`, root accessKey `AKIAJ...` + secret в `pass minio-vds/full-env`, форма ключа, ssl/region/pathStyle). Закрывает класс-вопрос «дай MinIO endpoint+креды» БЕЗ SSH. Прецедент: inbox snolla 2026-06-14 (raw-stream content-api), ответ отправлен.
|
||||
## [2026-06-11] ingest | verdaccio-restore-packument-desync — postmortem: disaster-restore вернул тарболлы но древний/пустой packument → publish свежей версии EEXISTS 409. Fix: снять только коллизирующий целевой .tgz (бэкап first), republish; НЕ rm -rf каталог (убьёт locally-published историю). Применено к @snollajs/{data,mailer,numbering,content-api}.
|
||||
## [2026-06-11] ingest | verdaccio-token-lifecycle — restart trap (ephemeral secret → 401), max_users:-1 + pnpm 409 постмортем, JWT config fix, yarn classic vs berry token refresh recipe.
|
||||
## [2026-06-11] ingest | yarn-npm-minimal-age-gate — Yarn ≥4.16 client-side supply-chain gate: версии <24ч → YN0016 «quarantined». Независим от auth (gate #2 после 401). default 1d, читается глобально (per-scope не работает), fix npmMinimalAgeGate:0 / YARN_NPM_MINIMAL_AGE_GATE=0. Диагностировано с pilonuxt; опровергнута первичная гипотеза «серверный карантин verdaccio».
|
||||
## [2026-06-11] update | MSSQL backup gap closed — `/opt/stacks/backup/scripts/run.sh` расширен: 5 prod DBs через `docker exec mssql sqlcmd BACKUP DATABASE ... WITH COPY_ONLY, INIT` (Express, без COMPRESSION); .bak в bind-mount → mv в DUMP_DIR → rsync kreknin. Обновлён [[concepts/mssql-on-vds]] (TODO→реализовано).
|
||||
|
||||
## [2026-06-11] lint | 10 issues found, 8 fixed inline
|
||||
- 🔴 recovery-architecture-snapshot.md: добавлена пометка ИСТОРИЧЕСКАЯ ЗАПИСЬ (рерайт текущей архитектуры отложен — большая задача)
|
||||
- 🔴 snolla-recovery-vm.md: помечена УДАЛЕНА 2026-06-08, updated date исправлен
|
||||
- 🟡 ruvds-iis-host.md: зачёркнуты 2 закрытых риска (imgproxy SPOF снят, LE renewal закрыт); добавлена ссылка на winacme; updated 2026-06-11
|
||||
- 🟡 future-resilient-architecture-goals.md: dead task link cms-stopgap-backup-daily заменён на текстовое примечание
|
||||
- 🟡 mssql-on-vds.md: исправлен frontmatter (status→type, добавлен updated)
|
||||
- 🟡 vds-kzntsv.md: добавлен MSSQL в software stack table
|
||||
- 🟡 orphan winacme-iis-owin-catchall-http01: входящая ссылка добавлена из ruvds-iis-host
|
||||
- ✅ ДОФИКСИРОВАНО: 3 broken links в recovery-architecture-snapshot убиты; TODO mssql backup integration закрыт (реализован)
|
||||
- ⚪ НЕ ФИКСИРОВАНО: orphan mssql-container-data-restore — оставлен
|
||||
|
||||
## [2026-06-08] update | entities/windows-recovery-host — декоммишн после Synology-recovery: снесены traefik/mssql/minio/imgproxy/es + 21 husk-стек + VM snolla-recovery (95ГБ) + C:\nas-recovery (~150ГБ) + IIS-сайт snolla + C:\sites\snolla + локальный MSSQL. Осталось: IIS stostayer/stostayer.old (репойнт на mssql.kzntsv.site, orphan-fix), lightrag, markitdown-MCP, mutable-dev VM. C: free 62→275ГБ (+~176ГБ docker pending vhdx-compact). image-pipeline SPOF снят. Источник: `.tasks/decommission-windows-recovery-host.md`.
|
||||
|
||||
## [2026-06-08] ingest | concepts/morethencms-null-settingsdata-https-502 — RCA+fix: `maljarka.tandemmebel.ru` отдавал 502 только на HTTPS (HTTP=200). Корень = `dbo.Sites.SettingsData=NULL` у тенанта maljarka (нет блока `httpSecure`) → `KeyNotFoundException` в `SnollaMiddleware`/`Owin.ErrorHandler` на HTTPS-ветке. Fix = `UPDATE dbo.Sites SET SettingsData='{httpSecure:...}'` + `Restart-WebAppPool snolla`; verified 443→200 server-local + external. Audit: maljarka — единственный NULL-сайт с :443-биндингом; rimiz degraded по другой причине. Обновлён [[../entities/ruvds-iis-host]]. Источник: live-диагностика 2026-06-08.
|
||||
|
||||
## [2026-06-05] ingest | concepts/winacme-iis-owin-catchall-http01 — постоянный self-renewing LE pipeline на RUVDS IIS (win-acme v2.2.9, 25-SAN cert, SYSTEM scheduled task). Главное: OWIN-catch-all CMS жрёт `/.well-known/acme-challenge/` → решено отдельным IIS-приложением в пуле «No Managed Code» + патч шаблона `Web_Config.xml` (snять Owin-handler). Заменяет ручной [[traefik-acme-json-to-iis-cert-import]] для renewal'а; снимает зависимость RUVDS от домашнего traefik. Закрывает дедлайн cert-expiry 2026-07-22 (новый cert до 2026-09-03). Источник: `.tasks/iis-migration-to-ruvds.md` Decisions log 2026-06-05.
|
||||
|
||||
## [2026-05-28] ingest | concepts/es-destructive-delete-incident-2026-05-26 — RCA для удаления 3 user-индексов на canonical ES `elasticsearch.kzntsv.site` (books VDS stack 33) во время cutover-prep. Caller identity unrecoverable (audit log = X-Pack платный, traefik accessLog был выключен). 2 preventive controls applied + verified: ES env `action.destructive_requires_name=true` + traefik JSON accessLog. Источник: `.tasks/restore-elasticsearch-indices-books-vds.md` § Closure note.
|
||||
|
||||
## 2026-05-21
|
||||
|
||||
- bootstrap: empty wiki skeleton (CLAUDE.md, index.md, log.md, overview.md, raw/README.md) committed
|
||||
@@ -23,3 +82,31 @@ Append-only log of wiki operations (ingests, promotions, lints, migrations).
|
||||
## [2026-05-24] ingest | concepts/books-ssh-access — SSH access audit на shared VDS pre-cutover Фазы 3 tenant-split. Retained keys table (1 key, vitya core dev), removed cosmetic dead root key, sshd hardening verified via `sshd -T` (not raw config), fail2ban active 2670 failed/37 banned, only vitya@94.19.247.14 в access log last 7 days. Source: .tasks/books-ssh-audit-shared-vds.md.
|
||||
|
||||
## [2026-05-24] ingest | RUVDS IIS migration + backup pipeline — entities/ruvds-iis-host (NEW, 80.64.31.36 Win Server 2025 Core, 25 SNI bindings, 2/24 hostnames DNS-flipped) + sources/iis-migration-to-ruvds-2026-05-23 (NEW, SSH/scp pivot после SMB-block by home-ISP) + sources/ruvds-backup-daily-kreknin-2026-05-24 (NEW, rclone+SFTP SYSTEM task daily 04:30) + concepts/traefik-acme-json-to-iis-cert-import (NEW, PFX+SNI recipe) + UPDATE concepts/windows-server-2025-core-bootstrap (SMB deprecate, HTTP middlebox warning, HTTP/2 note, backup-strategy + cert-import закрыты) + UPDATE entities/windows-recovery-host (IIS partial-cutover state, imgproxy SPOF carve-out) + UPDATE overview (RUVDS line). Sources: .tasks/iis-migration-to-ruvds.md, .tasks/ruvds-backup-daily-kreknin.md.
|
||||
|
||||
## [2026-05-28] ingest | vds-kzntsv DHCP outage postmortem — concepts/vds-kzntsv-dhcp-outage-2026-05-28 (NEW, симптомокартина + диагностический алгоритм + recovery runbook + RCA) + sources/vds-kzntsv-incident-2026-05-28 (NEW, timeline 05:25 backup OK → 08:15 detect → 08:43 statics fix → 08:50 disk GC; ticket text) + UPDATE entities/vds-kzntsv (Доступ §+subnet/gw/hypervisor/DNS resolvers; pass-store вместо .common/secrets; Known issues §NEW with 2026-05-28 incident; Open issues bump GC priority + iputils-ping note) + UPDATE concepts/rusonyx-vps-onboarding-quirks (quirk #9 NEW — gw в /18 надсети + recovery commands). Live session, no raw source ingested.
|
||||
|
||||
## [2026-05-28] update | vds-kzntsv DHCP outage post-resolution — после reset хостером в 13:52 MSK выяснилось что root cause — конфликт двух сетевых стеков (netplan+networkd поверх ожидаемого provider's ifupdown). Их `start/ipadd` ожидает чистый ifupdown, не мог auto-recover при разрыве DHCP binding. Resolution: mask netplan+systemd-networkd, reboot, provider положил `/etc/network/interfaces.d/ifcfg-eth0` с /18 netmask. UPDATE concepts/vds-kzntsv-dhcp-outage-2026-05-28 (revised RCA + permanent-fix § + anti-pattern + revised lessons-learned) + UPDATE sources/vds-kzntsv-incident-2026-05-28 (timeline до 14:00 + final config) + UPDATE entities/vds-kzntsv (mask /18, ifupdown stack, kernel cmdline net.ifnames=0) + UPDATE concepts/rusonyx-vps-onboarding-quirks (quirk #9 переписан про /18, quirk #10 NEW про ifupdown vs netplan stack). Memory `vds-kzntsv-rusonyx-network-recovery` переписан с новыми фактами.
|
||||
|
||||
## [2026-05-29] update | ES destructive-delete RECURRENCE — true root cause найден, диагноз сменился. UPDATE concepts/es-destructive-delete-incident-2026-05-26 (correction-блок + Hypothesis помечена ОПРОВЕРГНУТА + новая секция «Рецидив 2026-05-29 — true root cause»: ransom-бот через открытый `0.0.0.0:9200` мимо traefik+basicAuth, ES 7.10 free без auth; `read_me`=BTC-выкуп; снос by-name мимо Control #1; accessLog пуст т.к. бот шёл прямо в :9200; firewalld bypass docker-publish; Control #3 = убрать публикацию host-порта via Portainer PUT stack 33; restore из daily-2026-05-25; exposure-audit таблица — mongo/books-db/bookva-db/minio/bookva-minio тоже exposed но credentialed). bookva не пострадал (bookva-es порт не публикует). Live incident response session.
|
||||
|
||||
## [2026-06-05] ingest | NL VDS 3x-UI node + Reality PQ×dest root-cause
|
||||
- new entities/nl-vds-3xui.md (213.176.64.253, Ubuntu 24.04, 3x-UI 3.2.7 / Xray 26.6.1); creds -> pass nl-vds-3xui/full-env
|
||||
- new concepts/reality-pq-mldsa65-dest-incompatibility.md (root cause: ML-DSA-65 PQ ClientHello X25519MLKEM768 + Akamai dest www.intel.com -> HRR -> handshake fail; fix = PQ-capable dest e.g. www.microsoft.com, verified via isolation matrix)
|
||||
- index.md updated (entity + concept). Fix NOT applied — awaiting user command.
|
||||
|
||||
## [2026-06-05] ingest | NL VDS 3x-UI session — honest final state + lessons
|
||||
- NEW sources/nl-vds-3xui-setup-2026-06-05.md (chronicle: Reality PQ fix, port 443→2053, mtg MTProto, SOCKS5, friend-client instr; HONEST outcome — у реальных клиентов из РФ работает только plain VLESS 32030)
|
||||
- NEW concepts/proxy-debugging-test-the-real-client.md (anti-pattern: свои curl/standalone-тесты ≠ боевой клиент; overclaim «РКН режет 443» / MSS-clamp по MTU-догадке сломал коннект)
|
||||
- REWROTE entities/nl-vds-3xui.md — убраны ложные «Reality verified/fixed», «mtg неотличим»; честная таблица статусов (32030 🟢, Reality 2053 / MTProto 8443 / SOCKS 47020 🔴 у клиентов); MSS-clamp снят
|
||||
- UPDATE concepts/reality-pq-mldsa65-dest-incompatibility.md — caveat «снятие PQ ≠ рабочий Reality у GUI-клиентов» + source link
|
||||
- index.md updated (2 new pages). Креды/ссылки — pass nl-vds-3xui/full-env.
|
||||
|
||||
## [2026-06-06] decision | nl-vds-3xui: +3 classic Shadowsocks inbounds (ports 32031/32/33) for Outline-app, per-key revocable; protocol e2e-tested OK, RF reachability unverified (SS DPI-blocked in RF); creds in pass; discovered undocumented VLESS:13027.
|
||||
|
||||
## [2026-06-10] ingest | concepts/mcp-init-resilience
|
||||
|
||||
## [2026-06-12] migrate | galleries pilorama98 → S3. NEW concepts/galleries-storage-class-local-not-s3.md (root: storageClient `galleries` = Local disk class, never in S3; snolla ждёт s3://galleries/<siteId>/<file> → 404). Путь A: bucket `galleries` создан, 301 файл (77 MiB) с RUVDS IIS залиты verbatim, imgproxy 200 verified с books-vds. UPDATE concepts/minio-imgproxy-on-vds.md (stale-баннер: pipeline на books-vds с 08.06, не windows-host). Task migrate-gallery-originals-to-s3 🟢.
|
||||
|
||||
## [2026-06-27] refactor | books-vds: документирован публичный bookva-db endpoint :33306 (+ slovo/bookva DB-exposure, mongo/ES internal) в § Доступ; закрыт wiki-drift по порту
|
||||
|
||||
## [2026-07-05] decision | тираж snolla 0.42.1 (live-prod in-place bumps). NEW concepts/snolla-live-prod-inplace-image-bump.md (переиспользуемый рецепт build→staging-acceptance-С-VDS→env-preserving Portainer PUT `put-stack.js`→live-smoke). UPDATE 3 рунбука секцией «0.42.1 in-place bump»: labtools-vds-deploy-runbook (стек 17, `labtools:566d41c`), emspb-vds-deploy-runbook (18, `emspb:95a5c42`), labtools.pro-vds-deploy-runbook (19, `labtools-pro:0610432`). FIX orphan: все 3 рунбука добавлены в index.md (не были каталогизированы). Tasks `[labtools-ru|emspb|labtools-pro-deploy-snolla-0-42-1]` 🟢.
|
||||
|
||||
111
.wiki/sources/books-vds-backup-daily-kreknin-2026-05-25.md
Normal file
111
.wiki/sources/books-vds-backup-daily-kreknin-2026-05-25.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: books VDS daily backup → kreknin — pipeline standup 2026-05-25
|
||||
type: source
|
||||
tags: [backup, books-vds, kreknin, rsync, elasticsearch-snapshot, curl-smtp, cron, ntfy]
|
||||
ingested: 2026-05-25
|
||||
raw_path: ../../.tasks/books-vds-backup-daily-kreknin.md
|
||||
updated: 2026-05-25
|
||||
---
|
||||
|
||||
# books VDS daily backup → kreknin
|
||||
|
||||
Поднят ежедневный backup pipeline с [[../entities/books-vds]] (`89.253.255.133` / host4g.ru / CentOS 7) → [[../entities/kreknin-synology]] (`195.19.90.188`) в **06:00 MSK**. Параллель к [[vds-kzntsv-bootstrap-2026-05-20]] §backup и [[ruvds-backup-daily-kreknin-2026-05-24]] (pattern mirror), но source = CentOS 7 с hybrid SSH-compose + Portainer стек.
|
||||
|
||||
Источник истины — `.tasks/books-vds-backup-daily-kreknin.md` и `scripts/books-vds-backup-daily-kreknin/`.
|
||||
|
||||
## Background — почему отдельно
|
||||
|
||||
Прошлые сессии (и `[[../concepts/vds-kzntsv-ssh-access]]` до 2026-05-25) ошибочно утверждали что books-стек живёт на [[../entities/vds-kzntsv]]. **Это не так:** books на отдельном клиентском VDS `89.253.255.133`. User указал на ошибку явно: «books VDS - это совсем другой сервер, клиентский». См. [[../entities/books-vds]] для актуального factsheet.
|
||||
|
||||
## Scope
|
||||
|
||||
| Path on books VDS | Reason | Approx size |
|
||||
|---|---|---|
|
||||
| `/opt/books/{api,job-scheduler,task-runner,ntfy,books-ops-mcp}` | App configs (`config/default.json` overrides) | < 100 MB |
|
||||
| `/usr/docker/minio/data` | MinIO blobs (S3) — основная масса | ~4.5 GB |
|
||||
| `/usr/docker/elasticsearch/snapshots` | ES snapshot files (repo `kreknin`) | < 1 MB (read_me index only) |
|
||||
| `/usr/docker/traefik/{letsencrypt,data}` | acme.json + traefik.yml | < 5 MB |
|
||||
| `/etc/{ssh,hosts,cron.d}` | system config | < 50 KB |
|
||||
| `/root/.ssh` | root SSH state | < 5 KB |
|
||||
| `mariadb.sql.gz` (live dump) | books-db full dump | ~270 MB |
|
||||
| `mongo.archive.gz` (live dump) | shared mongo (root auth) | ~170 KB |
|
||||
| `job-scheduler-mongo.archive.gz` (live dump) | scheduler agenda (no auth) | ~3 MB |
|
||||
|
||||
Total после initial sync: **4.87 GB** (`du -sh` reported 4.8 GB после rsync).
|
||||
|
||||
Не бэкапятся: `/var/lib/docker/`, `/var/log/`, transient buildx volumes.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **Tool = bash + rsync + curl** (no rclone, no msmtp). Linux native, no extra installs. Pattern mirror VDS-infra `run.sh`.
|
||||
- **ES snapshot via REST API** (filesystem repo `kreknin`), не data-dir rsync. Required one-time stack edit `/usr/docker/elasticsearch/docker-compose.yml`: добавлены env `path.repo=/snapshots` + bind `./snapshots:/snapshots`, ES recreated. Снимки атомарны и Lucene-consistent.
|
||||
- **Email via `curl --ssl-reqd --url smtps://...:465`** (implicit TLS), не msmtp. CentOS 7 + EOL repos = msmtp install painful; curl native + Yandex SMTP works. Initial attempt с `smtp://...:465` + `--ssl-reqd` дала 30-sec timeout (curl попытался STARTTLS на TLS-only-from-start порту) — исправлено переходом на `smtps://`.
|
||||
- **RFC 5322 CRLF headers + Date + MIME-Version + Content-Type** в email-body. Yandex отвергает без Date. Initial body без них принимался при STARTTLS теста, но for production safety — full headers.
|
||||
- **`StrictHostKeyChecking=yes`** (НЕ `accept-new`) — CentOS 7 OpenSSH 7.4 не поддерживает `accept-new` (added in 7.6). kreknin host key pre-populated в `/root/.ssh/known_hosts` на bootstrap step.
|
||||
- **Retention 7 daily snapshots** (matches VDS + RUVDS pattern). ES snapshot pruning отдельно — via REST DELETE.
|
||||
- **`ntfy.vds.kzntsv.site/vds-backup` topic** (shared канал с VDS + RUVDS + windows-host). Title `BOOKS-VDS backup OK <date>` отличает от других хостов на phone-side.
|
||||
- **books-job-scheduler-mongo без auth** — `MONGO_INITDB_ROOT_*` env пустой, `mongodump --archive` без `--uri` достаточен. Shared `mongo` — root auth (urircreds).
|
||||
- **Cron 06:00 MSK** — после VDS-infra (05:00), RUVDS (04:30), windows-host (05:30). Break перед US-EU business start.
|
||||
- **`books-vds-portainer/full-env` consolidated** → `books-vds/full-env` (parity с `vds-kzntsv/full-env` one-file-per-host pattern).
|
||||
|
||||
## Хронология standup'а (2026-05-25)
|
||||
|
||||
- **08:30:** user-указание начать `stateful-split-volume-copy` под supervision. Probe `vds-ops` MCP показал что books-db нет на vds-kzntsv (только generic mariadb/mongo/minio). Wiki `[[../concepts/vds-kzntsv-ssh-access]]` (тогда `books-ssh-access.md`) врала что books живёт на vds-kzntsv.
|
||||
- **08:45:** user: «89.253.255.133 - books VDS, клиентский. Сделаем backup на kreknin». Pivot фокус.
|
||||
- **08:48:** SSH probe `id_ed25519_books_ops` → ready, root access OK. Inventory: hybrid Portainer + SSH-compose, hosting books-api/web/scheduler/task-runner/db + shared infra.
|
||||
- **08:55:** Portainer API token request — user создал `claude-code-automation` PAT в Portainer UI (`portainer.kzntsv.site`). Saved в pass `books-vds-portainer/full-env`. API tested OK. But `elasticsearch`/`mongo`/`minio`/`books-db`/`traefik` нет среди Portainer-managed stacks — they're SSH-compose. User: «после backup'а мигрируем максимум под Portainer».
|
||||
- **08:58:** ES probe — `path.repo` не настроен, snapshot API не работает без edit'а compose.
|
||||
- **09:00:** User approval на ES stack edit «сейчас, ES никто не использует». Sed edit `docker-compose.yml` + recreate. docker-compose 1.29 → docker 26 incompat (`KeyError: 'ContainerConfig'`) → manual `docker rm` renamed-stuck → `docker-compose up -d` clean → ES ready in 30s. Snapshot repo `kreknin` registered + test snap SUCCESS → deleted.
|
||||
- **09:14:** Wrote `scripts/books-vds-backup-daily-kreknin/{run.sh,.env.example,README.md}` локально. Built `.env` from pass entries (books-vds + vds-kzntsv NTFY + snolla-smtp SMTP). Deployed `/opt/stacks/backup/{run.sh,.env}` на VDS via scp. Generated `kreknin-key` ed25519 на VDS. Authorized pubkey on kreknin via workstation `id_ed25519_kreknin`. Pre-populated kreknin host key в `/root/.ssh/known_hosts` (CentOS 7 no accept-new).
|
||||
- **09:15:** Smoke run start. DB dumps OK (mariadb 270MB, mongo 170KB, scheduler-mongo 2.8MB). ES snapshot SUCCESS. rsync started.
|
||||
- **09:29:** rsync done, 4.87 GB in **13m28s** (7.6 MB/s). ntfy push отправлен. Email **timeout 30s** — bug в email_send.
|
||||
- **09:30:** Diagnose. SMTP_PORT=465 + `smtp://` + `--ssl-reqd` = curl попытался STARTTLS на implicit-TLS-only порту. Fix: `smtps://` schema. Plus CRLF headers + Date header. Re-tested: SMTP send OK.
|
||||
- **09:32:** Updated run.sh re-deployed. Cron installed `/etc/cron.d/books-vds-backup` (0 6 * * * root /opt/stacks/backup/run.sh). crond active.
|
||||
|
||||
## Pipeline state
|
||||
|
||||
- **Live:** cron `books-vds-backup`, daily 06:00 MSK, root user.
|
||||
- **Verified:** initial sync 4.87 GB end-to-end + ntfy push отправлен.
|
||||
- **Phone-side ntfy verify:** **pending user confirmation** (был ли push `BOOKS-VDS backup OK 2026-05-25` на ntfy app в общем канале `vds-backup`).
|
||||
- **Email verify:** sent после fix (см. 09:32 fix). User должен подтвердить получение `[BOOKS-VDS] backup OK <date>` или standalone fix-test message в inbox.
|
||||
- **Day-2 verify:** automatic at 06:00 MSK 2026-05-26 — first cron-trigger.
|
||||
- **Retention prune verify:** automatic at day-8 (когда snapshot count > 7).
|
||||
|
||||
## Bugs fixed during smoke
|
||||
|
||||
- **SMTP timeout 30s** — `smtp://...:465` + `--ssl-reqd` = curl STARTTLS на implicit-TLS-only порту. Server ждёт TLS handshake, curl ждёт plain EHLO, deadlock. Fix: `smtps://` scheme.
|
||||
- **email RFC 5322** — initial body had no Date/MIME headers, only From/To/Subject. Yandex отвергает без Date. Fix: добавлены Date, MIME-Version, Content-Type CRLF headers.
|
||||
- **CentOS 7 OpenSSH 7.4 no accept-new** — `StrictHostKeyChecking=accept-new` parse-error. Fix: `StrictHostKeyChecking=yes` + pre-populated known_hosts.
|
||||
- **docker-compose 1.29 ↔ docker 26 KeyError ContainerConfig** — recreate failed mid-way, container renamed-stuck. Manual `docker rm -f` + clean `up -d` rescued.
|
||||
|
||||
## Open follow-ups (не блокер)
|
||||
|
||||
- **Migrate SSH-compose стеки под Portainer** (отдельная таска, user-requested после backup live). Targets: elasticsearch, mongo, minio, books-db, traefik, proxy-chain.
|
||||
- **SSH-аудит books VDS** — отдельный аудит по pattern [[vds-kzntsv-ssh-access]]. На сейчас знаем только root key `id_ed25519_books_ops` под root — других пользователей не enum'ил. Pre-existing keys unknown.
|
||||
- **kreknin space audit** — quarterly check `du -sh /volume1/NetBackup/*` чтобы не упереться в 5.7T limit.
|
||||
- **wd40 backup**: рассмотреть второй backup target (B2 / Glacier) для resilience — см. [[../concepts/future-resilient-architecture-goals]].
|
||||
|
||||
## Атомарный revert
|
||||
|
||||
```bash
|
||||
# На books VDS:
|
||||
ssh root@89.253.255.133 'rm /etc/cron.d/books-vds-backup; rm -rf /opt/stacks/backup /var/log/books-vds-backup'
|
||||
|
||||
# Revert ES path.repo edit:
|
||||
ssh root@89.253.255.133 'cd /usr/docker/elasticsearch && \
|
||||
cp docker-compose.yml.bak-pre-snapshots-2026-05-25 docker-compose.yml && \
|
||||
docker rm -f elasticsearch && docker-compose up -d'
|
||||
|
||||
# На kreknin (через SSH):
|
||||
ssh vitya@195.19.90.188 'rm -rf /volume1/NetBackup/books-vds'
|
||||
# Remove the books-vds-backup-20260525 pubkey line from authorized_keys.
|
||||
```
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- Source host: [[../entities/books-vds]]
|
||||
- Backup target: [[../entities/kreknin-synology]]
|
||||
- Pattern parent: [[vds-kzntsv-bootstrap-2026-05-20]] §backup (msmtp + rsync --link-dest)
|
||||
- Sibling pipeline: [[ruvds-backup-daily-kreknin-2026-05-24]] (rclone+SFTP)
|
||||
- Notification: общий ntfy topic `vds-backup` на `ntfy.vds.kzntsv.site`.
|
||||
- Misnaming legacy: [[../concepts/vds-kzntsv-ssh-access]] (бывший `books-ssh-access` до 2026-05-25).
|
||||
74
.wiki/sources/de-vds-3xui-setup-2026-07-14.md
Normal file
74
.wiki/sources/de-vds-3xui-setup-2026-07-14.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: DE VDS 3x-UI — setup session 2026-07-14
|
||||
type: source
|
||||
tags: [vless, germany, fornex, 3x-ui, csrf, troubleshooting, session]
|
||||
ingested: 2026-07-14
|
||||
raw_path: (live session, no raw file)
|
||||
sources: []
|
||||
updated: 2026-07-14
|
||||
---
|
||||
|
||||
# DE VDS 3x-UI — сессия настройки 2026-07-14
|
||||
|
||||
Хроника заведения прокси-VDS [`de-vds-3xui`](../entities/de-vds-3xui.md) (Fornex, Германия). Аналог [`nl-vds-3xui`](../entities/nl-vds-3xui.md) — повторены **только proven-working** решения, маскированные протоколы НЕ поднимались.
|
||||
|
||||
## Закуп / стартовая точка
|
||||
|
||||
Заказ: VPS Custom 1-2-20, Ubuntu 24.04, без панели, Германия, 300 Мбит/с, 1 vCPU / 2 ГБ / 20 ГБ NVMe. Провайдер по факту — **Fornex** (`335555.fornex.cloud`), IP `130.17.17.158`. Свежая машина: только `:22`, ufw inactive, swap 0. Креды → `pass de-vds-3xui/full-env` (host-key `SHA256:1AQ5…cmdE`).
|
||||
|
||||
Подход (по урокам NL-сессии [`nl-vds-3xui-setup-2026-06-05`](nl-vds-3xui-setup-2026-06-05.md)):
|
||||
- только plain VLESS `security=none` (Reality/MTProto/SOCKS5 у боевых клиентов из РФ не заработали);
|
||||
- проверять на реальном клиенте, не на своих curl-тестах ([`proxy-debugging-test-the-real-client`](../concepts/proxy-debugging-test-the-real-client.md));
|
||||
- креды сразу в `pass`.
|
||||
|
||||
## Что сделано
|
||||
|
||||
1. **Recon + `pass`.** SSH через `plink -m <script>` (инлайн-кавычки PowerShell корёжит — тот же урок, что на NL). Креды в `pass de-vds-3xui/full-env`.
|
||||
2. **Установка 3x-ui v3.5.0** официальным `install.sh` (нативный systemd, плюс автоставка fail2ban `3x-ipl`). v3.5.0 **автогенерит** рандомные panel user/pass/port/webBasePath (`hasDefaultCredential:false`) — лучше старых версий, НО plaintext пароля недоступен (bcrypt).
|
||||
3. **Panel-креды перевыставлены** на свои (random user/pass) — см. гоча ниже про wrapper.
|
||||
4. **Plain VLESS 32030** `security=none` создан — см. ниже, не напрямую, а через panel API (борьба с 3.5.0).
|
||||
5. **Server-side e2e:** xray-клиент на сервере → 32030 → exit = IP сервера (IPv4 `130.17.17.158`, IPv6 `2a02:6b40:2000:3505::1`).
|
||||
6. **Real-client из РФ — подтверждён:** user подключён через этот узел прямо во время сессии. ✅
|
||||
|
||||
## Борьба с 3x-ui 3.5.0 (главное знание сессии)
|
||||
|
||||
На NL стояла 3.2.7; тут 3.5.0, и API/DB-модель поменялись. Это и заняло основное время.
|
||||
|
||||
### Гоча 1 — login 403 (CSRF, не креды)
|
||||
|
||||
`POST {webBasePath}login` возвращал `403 Forbidden` с **пустым телом** + CSP-nonce заголовками. Выглядело как «битые креды», но:
|
||||
- `/login` → 404 (роут только под `webBasePath`);
|
||||
- `{BP}login` → 403 (роут есть, но режется middleware);
|
||||
- x-ui лог молчит (запрос до хендлера не доходит);
|
||||
- Referer/Origin/UA/X-Requested-With — не помогали.
|
||||
|
||||
**Root cause** (найден через source на GitHub): `internal/web/middleware/security.go` → `CSRFMiddleware` — на всех unsafe-методах (POST) без валидного CSRF-токена → `c.AbortWithStatus(403)` (пустое тело, а SecurityHeaders уже навесил CSP). Логин-хендлер тут **ни при чём** (он даже при неудаче отдаёт 200+JSON). Токен: `GET {webBasePath}csrf-token` (публичный, кладёт `CSRF_TOKEN` в session-cookie) → вернуть тем же заголовком `X-CSRF-Token`.
|
||||
|
||||
### Гоча 2 — прямой INSERT в `inbounds` не рендерит инбаунд
|
||||
|
||||
Первые две попытки — `INSERT INTO inbounds ...` с валидным JSON (вторая — с эталонным `settings`/`stream_settings`/`sniffing`, **скопированным 1:1 с рабочего nl-vds 32030**). x-ui стартовал, логировал `Normalized sub_sort_index on 1 inbound(s)`, но в `/usr/local/x-ui/bin/config.json` инбаунд **не попадал**, xray `:32030` не слушал. Никакой ошибки в логе.
|
||||
|
||||
**Root cause:** в 3.5.0 клиентская модель разнесена по таблицам `clients` / `client_inbounds` / `client_traffics`; эти записи заполняются **только сервисом `AddInbound`** (пути panel API/UI), а не raw-SQL. Инбаунд без клиентских строк x-ui в xray-конфиг не рендерит.
|
||||
|
||||
**Лекарство:** panel API `POST {BP}panel/api/inbounds/add` — body = `model.Inbound` (camelCase: `remark,enable,port,protocol,listen,tag,settings,streamSettings,sniffing,shareAddrStrategy,...`), причём `settings`/`streamSettings`/`sniffing` — **stringified JSON** (строки, не вложенные объекты). После API-add xray сразу начал слушать 32030 (3.5.0 hot-applies к работающему xray), а после `x-ui restart` — конфиг персистентен.
|
||||
|
||||
### Гоча 3 — wrapper `x-ui setting` не применяет креды
|
||||
|
||||
`x-ui setting -username … -password …` (shell-wrapper) печатает меню, но `users`-таблицу **не меняет** → логин падает «Invalid username or password». Работает только бинарник напрямую: `systemctl stop x-ui; /usr/local/x-ui/x-ui setting -username … -password …` → «Username and password updated successfully».
|
||||
|
||||
### Эталон JSON (снят с рабочего nl-vds 32030)
|
||||
|
||||
- `settings` (vless): `{"clients":[{auth,comment,created_at,email,enable,expiryTime,id(uuid),limitIp,password,reset,security:"auto",subId,tgId,totalGB,updated_at}], "decryption":"none","encryption":"none","testseed":[900,500,900,256]}`
|
||||
- `streamSettings`: `{"network":"tcp","tcpSettings":{"acceptProxyProtocol":false,"header":{"type":"none"}},"security":"none"}` — **`tcpSettings`, не `tcp`**, с `header.type:none`.
|
||||
- `sniffing`: `{"enabled":false}` (простой, без `destOverride`).
|
||||
|
||||
## Рабочее решение для друзей
|
||||
|
||||
`vless://f3a0dda0-947b-4a11-b3f5-ca973640f843@130.17.17.158:32030?type=tcp&security=none&encryption=none#de-vless-32030` — Hiddify/v2rayN/v2rayNG. При включённом VPN в Telegram прокси не настраивать. Ключи/uuid — `pass de-vds-3xui/full-env`.
|
||||
|
||||
## Открытые хвосты
|
||||
|
||||
- **Per-friend UUID** (сейчас один — `vitya`): заводить через панель при раздаче, для возможности отзыва.
|
||||
- **DPI-стойкий канал** (не plain VLESS) — отдельная нерешённая задача (на NL Reality у боевых клиентов так и не поднялся).
|
||||
- **Panel на http:37601** — random path + fail2ban компенсируют; при желании — SSH-tunnel-only или LE-серт (опция 20 меню).
|
||||
- **Бэкап `x-ui.db`** в pipeline (как `backup-inventory-2026-06` для nl-vds) — не заведён.
|
||||
46
.wiki/sources/nl-vds-3xui-setup-2026-06-05.md
Normal file
46
.wiki/sources/nl-vds-3xui-setup-2026-06-05.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: NL VDS 3x-UI — setup & troubleshooting session 2026-06-05
|
||||
type: source
|
||||
tags: [vless, reality, mtproto, hiddify, v2rayn, rkn, troubleshooting, session]
|
||||
ingested: 2026-06-05
|
||||
raw_path: (live session, no raw file)
|
||||
sources: []
|
||||
updated: 2026-06-05
|
||||
---
|
||||
|
||||
# NL VDS 3x-UI — сессия настройки/диагностики 2026-06-05
|
||||
|
||||
Хроника заведения и отладки прокси-VDS [`nl-vds-3xui`](../entities/nl-vds-3xui.md).
|
||||
|
||||
## Что сделано
|
||||
|
||||
1. **Заведение в вики + ротация кредов.** Узел задокументирован, креды (SSH root, panel, JWT secret) ротированы и положены в `pass nl-vds-3xui/full-env` (оригиналы светились в плейнтексте).
|
||||
2. **Reality 443 диагностика.** Не коннектился. Root cause: 3x-UI включил ML-DSA-65 (PQ) + dest `www.intel.com` (Akamai, не умеет PQ key-exchange) → handshake не достраивался. Изоляционная матрица на replica-парах. Разбор: [`reality-pq-mldsa65-dest-incompatibility`](../concepts/reality-pq-mldsa65-dest-incompatibility.md).
|
||||
3. **Fix Reality (server-side):** dest→`www.microsoft.com`, затем снят `mldsa65Seed` (GUI-клиенты не шлют `mldsa65Verify`), затем порт 443→2053.
|
||||
4. **MTProto:** поднят mtg 2.2.8 (FakeTLS cloudflare) на 8443.
|
||||
5. **SOCKS5:** инбаунд на 47020 (auth).
|
||||
6. **Инструкции для друзей** (Hiddify, Win/Mac/Android).
|
||||
|
||||
## Чем закончилось (честный итог)
|
||||
|
||||
**У реальных клиентов из РФ работает ТОЛЬКО plain VLESS 32030.** Reality (2053), MTProto (8443), SOCKS5 (47020) — **не поднялись** на боевых клиентах:
|
||||
|
||||
- **Reality 2053:** в изолированных тестах (отдельный `xray.exe` + `curl -x socks5h` с того же ПК) — проходил, exit NL, 5 МБ/с. Боевой v2rayN на том же ПК и телефон — **нет**. Причина не установлена. Версионная/PQ-несовместимость исключена (PQ снят, flow на месте, конфиг сверен — идентичен рабочему тесту). Вероятная гипотеза (НЕ доказана) — обработка TLS-хендшейка к этому IP в сети пользователя.
|
||||
- **MTProto 8443:** Telegram (Desktop и телефон) висит «соединение…». tcpdump (телефон изолирован, Desktop закрыт): телефон шлёт 1288-б ClientHello → mtg отвечает 0 байт, телефон долбится десятками повторов. Локальный FakeTLS (`openssl` на сервере) проходит — но это лишь камуфляж-путь, реальную MTProto-сессию mtg здесь не держит. «Доступен (пинг N мс)» в Telegram — пассивная проверка, не равна рабочей сессии.
|
||||
- **SOCKS5 47020:** на сервере исправен (внешний curl с auth проходил), но у пользователя режется DPI.
|
||||
|
||||
**Грубые ошибки в процессе (см. урок ниже):**
|
||||
- Несколько раз заявлял «работает» по своим коротким curl/standalone-тестам, тогда как боевые клиенты не работали. Тесты были нерепрезентативны (часто шли не через тот путь, что у пользователя; либо через системный прокси v2rayN; либо это был мой отдельный процесс, не v2rayN).
|
||||
- Влепил **MSS-clamp 1360** на сервер по неверной MTU-гипотезе (пользователь сразу сказал, что дело не в MTU) — clamp **ломал** соединения; после снятия комп-телега через системный прокси заработала.
|
||||
|
||||
## Рабочее решение для друзей (раздаётся)
|
||||
|
||||
Клиент **Hiddify** (Win/Mac/Android, один на все платформы) либо v2rayN/v2rayNG. Импорт по `vless://`-ссылке или QR, профиль 32030. При включённом VPN — в Telegram прокси не настраивать (моб. «отключить прокси», desktop «системный прокси», кастомные удалить).
|
||||
|
||||
Каждому другу — **свой** клиентский UUID (на момент сессии на 32030 один общий `a6fa7965-…`; рекомендовано завести отдельных клиентов под каждого для возможности отзыва — не сделано, ждёт решения user).
|
||||
|
||||
## Открытые хвосты
|
||||
|
||||
- **Reality/MTProto у реальных клиентов из РФ не работают** — причина не доведена до конца. Если нужен DPI-стойкий канал (не plain VLESS) — отдельная задача (возможно: другой IP/провайдер, CDN-fronting, или клиент с TUN вместо встроенного Telegram-прокси).
|
||||
- Per-friend UUID на 32030 — завести отдельных клиентов.
|
||||
- Опционально: убрать неработающие инбаунды (2053 Reality / 47020 SOCKS / 8443 mtg), чтобы не торчали лишние порты.
|
||||
126
.wiki/sources/vds-kzntsv-incident-2026-05-28.md
Normal file
126
.wiki/sources/vds-kzntsv-incident-2026-05-28.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
title: vds-kzntsv outage incident — session chronology 2026-05-28
|
||||
type: source
|
||||
tags: [vds, rusonyx, network, dhcp, outage, session-trace]
|
||||
ingested: 2026-05-28
|
||||
raw_path: (none — session-live, no external raw)
|
||||
updated: 2026-05-28
|
||||
---
|
||||
|
||||
# vds-kzntsv incident chronology 2026-05-28
|
||||
|
||||
Live-session трасса диагностики и восстановления отказа [[../entities/vds-kzntsv]]. Полный разбор паттерна + recovery runbook — в [[../concepts/vds-kzntsv-dhcp-outage-2026-05-28]].
|
||||
|
||||
## Timeline (MSK)
|
||||
|
||||
| Время | Событие | Источник |
|
||||
|---|---|---|
|
||||
| 2026-05-28 05:25 | Daily backup pipeline `vds-backup-rsync-kreknin` стартует | ntfy-уведомление пользователю |
|
||||
| 2026-05-28 05:45 | Backup завершён успешно — 19m51s, 69G, 7 snapshots передано на kreknin | ntfy + backup log |
|
||||
| 2026-05-28 ~06:00–08:00 | (окно отказа DHCP — точное время неизвестно) | — |
|
||||
| 2026-05-28 ~08:15 | User обнаруживает что vds-kzntsv недоступен по сети, пинг падает на хостер-gateway | user message |
|
||||
| 2026-05-28 08:20 | Сессия с агентом начинается, запрос креды на VDS | conversation |
|
||||
| 2026-05-28 08:25 | Probe с workstation и с [[../entities/books-vds]] (same hoster) — оба `Destination Host Unreachable` на `89.253.192.40` | bash output |
|
||||
| 2026-05-28 08:30 | User открывает Rusonyx panel → VNC console, шлёт скриншот журнала | screenshot |
|
||||
| 2026-05-28 08:35–08:42 | Диагностика через VNC: `ip -br link` → eth0 UP без IPv4; `networkctl status` → degraded (configuring), LLDP видит `hw80.rusonyx.ru` | screenshots |
|
||||
| 2026-05-28 08:43 | Recovery через статику (3 команды + DNS) — IP получен, маршруты добавлены | user confirms "Заработало!" |
|
||||
| 2026-05-28 08:44 | SSH с workstation работает; uptime 1h (VDS был ребутнут ~07:44, но IP DHCP всё равно не пришёл) | `ssh vitya@89.253.255.94 hostname` |
|
||||
| 2026-05-28 08:44 | 18 docker контейнеров поднялись через restart policy — gitea, registry, verdaccio, postgres, mariadb, mongo, owncloud, oCIS, modulair-rag стэк и пр. | `docker ps` |
|
||||
| 2026-05-28 08:50 | Disk cleanup: docker builder prune (10.6G) + image prune (0.3G) + truncate container logs (~3G); disk 89% → 79% | `df -h /` before/after |
|
||||
| 2026-05-28 09:00 | Тикет в Rusonyx отправлен | user sends |
|
||||
| 2026-05-28 ~12:20 | Rusonyx отвечает: «возможно потребуется перезагрузка», запрашивают разрешение | helpdesk |
|
||||
| 2026-05-28 ~12:25 | Даём разрешение с условиями (5-10мин уведомление + готовность VNC к recovery) | helpdesk |
|
||||
| 2026-05-28 ~13:00 | Rusonyx: «настройте дефолт — `mask netplan + systemd-networkd`, `enable networking`» | helpdesk |
|
||||
| 2026-05-28 ~13:20 | После уточнений согласовали что они сами пропишут конфиг через свой `start/ipadd` | helpdesk |
|
||||
| 2026-05-28 ~13:30 | Выполнен `systemctl disable+mask netplan + systemd-networkd.{service,socket,wait-online}` через SSH. IP и SSH остались живые. | session bash |
|
||||
| 2026-05-28 ~13:52 | Rusonyx ребутает VM с reset-конфига через их provisioning | их сторона |
|
||||
| 2026-05-28 14:00 | Smoke: SSH + 24 контейнера up + `api.ipify.org → 89.253.255.94`. Сервер полностью восстановлен. | session bash |
|
||||
|
||||
## Что сломалось
|
||||
|
||||
- DHCP lease для VM `vps534388` (MAC `52:54:00:9c:63:01`) на гипервизоре `hw80.rusonyx.ru` не возобновлялся.
|
||||
- VM выпала из L3-сети полностью: своя сторона корректна (link UP, networkd шлёт DHCP discover), хостер не отвечает offer'ом.
|
||||
- Reboot VM не помог.
|
||||
|
||||
Подробные подтверждающие сигналы — в [[../concepts/vds-kzntsv-dhcp-outage-2026-05-28]] § Симптомокартина.
|
||||
|
||||
## Что починили
|
||||
|
||||
Статика на eth0 через 3 ip-команды:
|
||||
|
||||
```bash
|
||||
sudo ip addr add 89.253.255.94/24 dev eth0
|
||||
sudo ip route add 89.253.192.1 dev eth0
|
||||
sudo ip route add default via 89.253.192.1
|
||||
```
|
||||
|
||||
DNS: `nameserver 89.253.252.30/.31` (Rusonyx).
|
||||
|
||||
**Эфемерно.** Persistence в netplan **отложен** до ответа хостера — если они починят DHCP, persistent статика конфликтнёт.
|
||||
|
||||
## Gateway discovery — как нашли 89.253.192.1
|
||||
|
||||
Gateway у Rusonyx сидит в /18 надсети `89.253.192.0/18`, а не в нашей /24. Источник — `ip route` на [[../entities/books-vds]] (sibling VPS у того же хостера):
|
||||
|
||||
```
|
||||
$ ssh root@89.253.255.133 'ip route'
|
||||
default via 89.253.192.1 dev eth0 metric 400
|
||||
89.253.192.0/18 via 89.253.192.1 dev eth0 src 89.253.255.133 metric 400
|
||||
89.253.192.1 dev eth0 scope link metric 400
|
||||
89.253.255.0/24 dev eth0 proto kernel scope link src 89.253.255.133
|
||||
```
|
||||
|
||||
См. также quirk #9 в [[../concepts/rusonyx-vps-onboarding-quirks]].
|
||||
|
||||
## Финальная конфигурация (после resolution хостером 13:52 MSK)
|
||||
|
||||
`/etc/network/interfaces.d/ifcfg-eth0` (положен их `start/ipadd` procedure):
|
||||
|
||||
```
|
||||
auto eth0
|
||||
allow-hotplug eth0
|
||||
iface eth0 inet static
|
||||
address 89.253.255.94
|
||||
netmask 255.255.192.0
|
||||
post-up ip ro add 169.254.0.0/16 dev eth0 metric 400
|
||||
post-up ip ro add 89.253.192.1 dev eth0 metric 400
|
||||
post-up ip ro add default via 89.253.192.1 dev eth0 metric 400
|
||||
post-up ip ro add 89.253.192.0/18 via 89.253.192.1 dev eth0 src 89.253.255.94 metric 400
|
||||
post-up ip ro del 89.253.192.0/18 dev eth0 proto kernel scope link src 89.253.255.94
|
||||
```
|
||||
|
||||
Изменение по сравнению с тем что было до инцидента:
|
||||
- **Сетевой стек** — был mixed (netplan + networkd поверх ifupdown), стал чистый ifupdown
|
||||
- **Netmask** — стал /18 явно (`255.255.192.0`) вместо неявного /24 + link-route trick
|
||||
- **systemd unit state:** `netplan` masked, `systemd-networkd*` masked, `networking` enabled
|
||||
|
||||
Kernel cmdline без изменений: `net.ifnames=0 biosdevname=0` (Rusonyx force eth0 naming).
|
||||
|
||||
## Тикет в Rusonyx — итоговый текст
|
||||
|
||||
Тикет отправлен через myvm.rusonyx.ru helpdesk, account `21075162`, тема: «VPS 534388 (vps-21075162-534388) — нет DHCP lease ~05:45 28.05.2026 MSK».
|
||||
|
||||
Основная часть:
|
||||
- Подтверждённое окно простоя ~2.5ч (с 05:45 — последний успешный backup — до 08:30 recovery).
|
||||
- Отсутствие уведомления с их стороны.
|
||||
- Корневая причина на стороне хостера, с конкретикой (LLDP, networkd journal, отсутствие DHCPv4 offer'ов).
|
||||
- Диагностика клиентом, не support'ом — что не норма для production-уровня услуги.
|
||||
- Запрос: (a) технический RCA, (b) SLA-компенсация, (c) procedural followup по их мониторингу VM-уровня, (d) статус DHCP — починен или фиксируем статику постоянно.
|
||||
|
||||
Полный текст — в сессионной переписке `~/.claude/projects/C--Users-vitya-projects--admin/` (conversation log).
|
||||
|
||||
Ответ хостера ещё не получен (на момент ingest этого source).
|
||||
|
||||
## Что обнаружили попутно
|
||||
|
||||
1. **Диск 89% → 79% после GC** — освободили 14G (build cache 10.6G + dangling images 0.3G + container logs ~3G). Disk-GC pending давно, см. `vds-kzntsv` § Open issues. Поставлена task [[../../.tasks/vds-kzntsv-disk-gc-followup]] на полный registry-GC (20G возможный reclaim).
|
||||
2. **`board-viewer-build` unhealthy** — отдельная история, не блокер сейчас.
|
||||
3. **`ping` не установлен** на VDS — Ubuntu 24.04 base не включает iputils-ping. Использовали curl для проверки сети. Можно установить `apt install iputils-ping` при следующем maintenance.
|
||||
|
||||
## Cross-refs
|
||||
|
||||
- [[../concepts/vds-kzntsv-dhcp-outage-2026-05-28]] — runbook + RCA-pattern.
|
||||
- [[../entities/vds-kzntsv]] — host details.
|
||||
- [[../entities/books-vds]] — sibling, source для gw discovery.
|
||||
- [[../concepts/rusonyx-vps-onboarding-quirks]] — vendor quirks, дополнен quirk #9.
|
||||
- [[vds-kzntsv-bootstrap-2026-05-20]] — оригинальный bootstrap.
|
||||
28
.wiki/sources/vdsina-outline-3xui-inventory-2026-06-24.md
Normal file
28
.wiki/sources/vdsina-outline-3xui-inventory-2026-06-24.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: VDSina Outline + 3x-UI — inventory session
|
||||
type: source
|
||||
tags: [vds, vdsina, outline, 3x-ui, netherlands, inventory]
|
||||
date: 2026-06-24
|
||||
---
|
||||
|
||||
# VDSina (46.151.25.64) — inventory 2026-06-24
|
||||
|
||||
Пользователь сообщил о ранее недокументированном VDS: «46.151.25.64 — мой публичный api, он же основной VPN; `v1621967.hosted-by-vdsina.ru`; root/Pryakhin10~; там Outline и 3xui — сохрани секреты и собери информацию». Сессия: достал host-key, инвентаризировал по SSH, сохранил в `pass vdsina-outline/full-env`, завёл [[vdsina-outline-3xui]].
|
||||
|
||||
## Метод
|
||||
|
||||
- Host-key через `ssh-keyscan -t ed25519` → `SHA256:yRFesrmfNhsIeTSQ0ZWM3MWMjWxO0m3JaZsdbmy6Nx4`.
|
||||
- SSH через `plink -batch -hostkey ... -pw 'Pryakhin10~' -m <script>` (инлайн с кавычками PowerShell корёжит).
|
||||
- `sqlite3` на сервере отсутствует → x-ui.db читал через `python3` (модуль sqlite3).
|
||||
|
||||
## Что нашли (сырьё)
|
||||
|
||||
- **OS:** Ubuntu 20.04.6 LTS (kernel 5.4.0-216), Hyper-V VM, 1 vCPU «Common KVM», 965 MiB RAM, swap 138 MiB, disk 30 G (26% used). Up 17 дней. Provider-панель показывала «Ubuntu 22» — расхождение, реально 20.04.
|
||||
- **Outline:** docker `shadowbox:stable` + `watchtower` (оба Up 2 недели). `serverId cfb9ef54-…`, created `1710964189814` = **2024-03-20**. data-port 443 (chacha20-ietf-poly1305), mgmt API :8080, путь `/Kv5DD_ZHm9X3VC_InFY2ww`, cert `2CB0FB47…F919D`. **11 access-keys** с именами (vitya/natasha/mi box/router/NFS/alexey/планшет/s/tescha…).
|
||||
- **3x-UI:** systemd `x-ui` (since 2026-06-06), Xray 25.10.15. Panel webPort 50806, webBasePath `/ecCqrtFVl4zl5kFjdj/`, secret `JQJt1b…`, user `kIqmxpsisp`, пароль — **только bcrypt-хеш** (plaintext недоступен). sub :2096 `/sub/`.
|
||||
- **Xray inbounds:** id2 VLESS/xhttp :43666 (6 клиентов), id4 VLESS/grpc :46743 (1 клиент), id3 mixed :1420 (local), id1 VLESS :33980 disabled. Все `security=none`.
|
||||
- **Listen:** 22, 443, 8080, 9090/9091 (prometheus/node, localhost), 9092 (outline-ss localhost), 50806, 2096, 1420, 43666, 46743. ufw inactive.
|
||||
|
||||
## Resolved
|
||||
|
||||
Пароль 3x-UI панели изначально не извлекался (bcrypt). Пользователь получил его через `x-ui` меню на сервере и передал: `kIqmxpsisp` / `6usTSHFddG`, access URL `http://46.151.25.64:50806/ecCqrtFVl4zl5kFjdj`. Обновлён в `pass vdsina-outline/full-env`. (Строка `Start migrating database...` в выводе — старый мусор, не свежий рестарт.)
|
||||
@@ -13,6 +13,11 @@ delegate to interns when allowed
|
||||
recommend, don't menu
|
||||
we're on Windows
|
||||
|
||||
# Secrets rule
|
||||
|
||||
Все креды (SSH, БД, panel, BA) лежат в `pass` (password-store). **Перед поиском доступов — `pass ls` / `pass show <path>`, а не grep по вики или `~/.ssh/config`.**
|
||||
Серверы СТО Стайер: `stostayer/client` (`new.stostayer.ru:20435` — машина клиента, файлы 1С в `/var/from_1c/`), `stostayer/rusonyx`, `stostayer/client-wireguard`.
|
||||
|
||||
# VDS ops rule
|
||||
|
||||
Все docker-compose stacks на VDS управляются через Portainer (`https://portainer.vds.kzntsv.site`).
|
||||
|
||||
71
host-stacks/books-vds/ops-mcp.compose.yml
Normal file
71
host-stacks/books-vds/ops-mcp.compose.yml
Normal file
@@ -0,0 +1,71 @@
|
||||
# books-ops — host-level observability stack на books VDS (89.253.255.133).
|
||||
#
|
||||
# Ideology: books VDS = наш host, slovo/bookva = клиентские tenant-стэки.
|
||||
# Один host-wide ops-mcp видит все контейнеры через `tcp://books-docker-proxy-ro:2375`.
|
||||
#
|
||||
# Tenant DB scope: per-tenant pools (slovo + bookva). Tools принимают optional
|
||||
# `tenant: 'slovo'|'bookva'` (default = config.defaultTenant). См. config/default.json
|
||||
# в `packages/ops-mcp/` (mirrored на host'е через mount).
|
||||
#
|
||||
# Lifecycle:
|
||||
# - Managed manually через Portainer UI (как traefik + portainer — management plane exception
|
||||
# из портейнер-rule, не deploy'ится из tenant CI pipeline).
|
||||
# - Update image tag: Portainer → Stack → Edit compose → change image tag → Update.
|
||||
# - Container names books-docker-proxy-ro + books-ops-mcp **сохраняются** —
|
||||
# .claude.json использует `ssh vps-books docker exec -i books-ops-mcp ...`.
|
||||
#
|
||||
# Если нужно поднять второй MCP instance (per-tenant DB observability) — отдельный stack,
|
||||
# отдельные container names (bookva-ops-mcp / slovo-ops-mcp), отдельный config volume.
|
||||
|
||||
services:
|
||||
books-docker-proxy-ro:
|
||||
container_name: books-docker-proxy-ro
|
||||
image: tecnativa/docker-socket-proxy:0.3
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Только read-методы на /containers/* — list, inspect, logs, stats.
|
||||
# POST=0 (default) — никаких prune/start/stop/rm. Все остальные
|
||||
# ресурсы (images, build, volumes, networks, exec, system, info) — 0.
|
||||
CONTAINERS: 1
|
||||
POST: 0
|
||||
IMAGES: 0
|
||||
BUILD: 0
|
||||
EXEC: 0
|
||||
VOLUMES: 0
|
||||
NETWORKS: 0
|
||||
SERVICES: 0
|
||||
SYSTEM: 0
|
||||
INFO: 0
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
books-ops-mcp:
|
||||
container_name: books-ops-mcp
|
||||
image: registry.kzntsv.site/books-ops-mcp:master
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- books-docker-proxy-ro
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
DOCKER_HOST: tcp://books-docker-proxy-ro:2375
|
||||
# MARIADB_PASSWORD = slovo's books-db ops_ro; BOOKVA_MARIADB_PASSWORD = bookva-db's
|
||||
# (bookva-db = cp -a of books-db, ops_ro password идентичен; rotate отдельно).
|
||||
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
|
||||
BOOKVA_MARIADB_PASSWORD: ${BOOKVA_MARIADB_PASSWORD}
|
||||
volumes:
|
||||
# ВНИМАНИЕ: на VPS файл /opt/books/books-ops-mcp/config/default.json
|
||||
# должен существовать ДО первого старта стека, иначе Docker создаст
|
||||
# пустую directory вместо файла.
|
||||
- /opt/books/books-ops-mcp/config/default.json:/usr/src/app/packages/ops-mcp/config/default.json
|
||||
networks:
|
||||
- proxy
|
||||
# Контейнер всегда живой; реальный MCP запускается per-session через
|
||||
# ssh + docker exec -i books-ops-mcp node lib/server.js. CMD = idle.
|
||||
command: ["tail", "-f", "/dev/null"]
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
BIN
host-stacks/books-vds/tandemmebel-watermark-logo.png
Normal file
BIN
host-stacks/books-vds/tandemmebel-watermark-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
57
host-stacks/vds-kzntsv/emspb.compose.yml
Normal file
57
host-stacks/vds-kzntsv/emspb.compose.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
# emspb — прод-фронт emspb.ru (snolla-приложение, server-side Liquid, node) на vds-kzntsv (89.253.255.94).
|
||||
#
|
||||
# STAGING-FIRST. Source-of-truth этого compose — admin-репо; применяется через Portainer
|
||||
# API/UI (VDS-rule: стеки через Portainer, не ssh+compose).
|
||||
#
|
||||
# Модель: НЕ pilonuxt/Nuxt-contentApi. Это монолит @snollajs/snolla — читает БД-контент MoreThenCms
|
||||
# (mssql.kzntsv.site) + server-side Liquid render. Ассеты из MinIO (minio.kzntsv.site) через imgproxy
|
||||
# (imgproxy.kzntsv.site). node-config: NODE_ENV=production → default(dev,gitignored)+production.json+ENV-override.
|
||||
#
|
||||
# Образ собирается АДМИНОМ на VDS из victor/emspb.ru/deploy/Dockerfile (build-arg VERDACCIO_TOKEN,
|
||||
# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha монорепы emspb.ru.
|
||||
#
|
||||
# Домен: STAGING — emspb.vds.kzntsv.site (под wildcard *.vds.kzntsv.site, cert LE HTTP-01 авто).
|
||||
# Cutover live www.emspb.ru — ОТДЕЛЬНЫЙ gated шаг: бой живой на текущем хостинге, НЕ выводить.
|
||||
# DNS переключает ОПЕРАТОР по сигналу «staging green». План отката = revert DNS.
|
||||
#
|
||||
# Runtime egress (наружу из proxy-сети): mssql.kzntsv.site:1433, minio.kzntsv.site:443,
|
||||
# imgproxy.kzntsv.site:443, smtp.yandex.ru:465.
|
||||
#
|
||||
# Секреты — env на VDS (Portainer stack env), НЕ в образ, НЕ в git. Значения ${...} инжектит Portainer
|
||||
# из stack-переменных. Контракт (custom-environment-variables.json):
|
||||
# DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD
|
||||
# siteId=96EBC481-D26A-47BE-B660-13D49E7D0A61 — non-secret, в production.json (не env).
|
||||
|
||||
services:
|
||||
emspb:
|
||||
container_name: emspb
|
||||
image: registry.kzntsv.site/emspb:95a5c42 # git-sha victor/emspb.ru master (snolla 0.42.1, bump 2026-07-05; prev b6e361a=0.28.4=rollback)
|
||||
restart: unless-stopped
|
||||
mem_limit: 512m # guardrail от runaway: snolla baseline ~100-200M; OOM в контейнере → restart:unless-stopped поднимет. standalone-compose → mem_limit (НЕ deploy.resources — то swarm-only).
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
# PORT не переопределяем — образ дефолтит 5000 (EXPOSE 5000, healthcheck на $PORT)
|
||||
# --- runtime-секреты (значения из Portainer stack env; НЕ хранить тут) ---
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
IMGPROXY_KEY: ${IMGPROXY_KEY}
|
||||
IMGPROXY_SALT: ${IMGPROXY_SALT}
|
||||
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
||||
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.emspb.entrypoints: websecure
|
||||
# CUTOVER 2026-07-02: DNS emspb.ru/www флипнут на VDS (89.253.255.94) оператором. Боевые хосты.
|
||||
# staging-хост emspb.vds.kzntsv.site УБРАН post-cutover (2026-07-02). RUVDS IIS 80.64.31.36 — rollback, не тронут.
|
||||
traefik.http.routers.emspb.rule: Host(`emspb.ru`) || Host(`www.emspb.ru`)
|
||||
traefik.http.routers.emspb.tls.certresolver: letsEncrypt
|
||||
traefik.http.services.emspb.loadbalancer.server.port: "5000"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
62
host-stacks/vds-kzntsv/kupimknigi.compose.yml
Normal file
62
host-stacks/vds-kzntsv/kupimknigi.compose.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
# kupimknigi — прод-фронт kupimknigi.spb.ru (snolla-приложение, server-side Liquid, node) на vds-kzntsv (89.253.255.94).
|
||||
#
|
||||
# STAGING-FIRST. Source-of-truth этого compose — admin-репо; применяется через Portainer
|
||||
# API/UI (VDS-rule: стеки через Portainer, не ssh+compose).
|
||||
#
|
||||
# Модель: монолит @snollajs/snolla (0.42.0) / core (0.24.0) / data (0.14.1) — читает БД-контент
|
||||
# MoreThenCms (mssql.kzntsv.site) + server-side Liquid render. Ассеты из MinIO (minio.kzntsv.site)
|
||||
# через imgproxy (imgproxy.kzntsv.site). ПРОСТОЙ ОДНОСТРАНИЧНИК: Pages=1 (`/`), Forms=1 (`/callback-order` POST).
|
||||
# Каталога/стора/блога/фида/редиректов НЕТ. node-config: NODE_ENV=production → production.json + ENV-override.
|
||||
# siteId=E924A354-0377-4E1E-80C6-2EB0194AA55F, siteUrl=https://kupimknigi.spb.ru (БЕЗ www — www мёртв) — в production.json (запечён).
|
||||
#
|
||||
# Образ собирается АДМИНОМ на VDS из victor/kupimknigi.spb.ru/deploy/Dockerfile (build-arg VERDACCIO_TOKEN,
|
||||
# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha монорепы kupimknigi.spb.ru (9608ff6).
|
||||
#
|
||||
# Домен: STAGING — kupimknigi.vds.kzntsv.site (под wildcard *.vds.kzntsv.site, cert LE HTTP-01 авто).
|
||||
# Cutover live kupimknigi.spb.ru — ОТДЕЛЬНЫЙ gated шаг: бой сейчас на RUVDS IIS (80.64.31.36, майская iis-migration).
|
||||
# DNS/порядок cutover разрешает ОПЕРАТОР отдельно. План отката = revert DNS → RUVDS. RUVDS не тронут.
|
||||
# ⚠️ Боевой Host-rule добавлять ТОЛЬКО ПОСЛЕ flip DNS (иначе LE HTTP-01 упадёт на RUVDS → rate-limit).
|
||||
#
|
||||
# Runtime egress (наружу из proxy-сети): mssql.kzntsv.site:1433, minio.kzntsv.site:443,
|
||||
# imgproxy.kzntsv.site:443, smtp.yandex.ru:465 (форма callback-order).
|
||||
#
|
||||
# Секреты — env на VDS (Portainer stack env), НЕ в образ, НЕ в git. Значения ${...} инжектит Portainer
|
||||
# из stack-переменных (те же 8 общего snolla-тенанта, verbatim из labtools/tandemmebel). Контракт
|
||||
# (custom-environment-variables.json): DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT
|
||||
# S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD
|
||||
|
||||
services:
|
||||
kupimknigi:
|
||||
container_name: kupimknigi
|
||||
image: registry.kzntsv.site/kupimknigi:9608ff6 # git-sha victor/kupimknigi.spb.ru (snolla 0.42.0 / core 0.24.0 / data 0.14.1)
|
||||
restart: unless-stopped
|
||||
mem_limit: 512m # guardrail от runaway: snolla baseline ~100-200M; OOM в контейнере → restart:unless-stopped поднимет. standalone-compose → mem_limit (НЕ deploy.resources — то swarm-only).
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
# PORT не переопределяем — образ дефолтит 5000 (EXPOSE 5000, healthcheck на robots.txt)
|
||||
# --- runtime-секреты (значения из Portainer stack env; НЕ хранить тут) ---
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
IMGPROXY_KEY: ${IMGPROXY_KEY}
|
||||
IMGPROXY_SALT: ${IMGPROXY_SALT}
|
||||
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
||||
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.kupimknigi.entrypoints: websecure
|
||||
# CUTOVER 2026-07-05: DNS kupimknigi.spb.ru флипнут на VDS (89.253.255.94) оператором (reg.ru).
|
||||
# Проверено ОБА авторитетных reg.ru-NS (ns1+ns2) согласованно → 89.253.255.94 (было 80.64.31.36 RUVDS).
|
||||
# Боевой хост (БЕЗ www — мёртв). staging-хост kupimknigi.vds.kzntsv.site УБРАН post-cutover.
|
||||
# RUVDS IIS 80.64.31.36 — rollback (revert DNS), не тронут.
|
||||
traefik.http.routers.kupimknigi.rule: Host(`kupimknigi.spb.ru`)
|
||||
traefik.http.routers.kupimknigi.tls.certresolver: letsEncrypt
|
||||
traefik.http.services.kupimknigi.loadbalancer.server.port: "5000"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
60
host-stacks/vds-kzntsv/labtools-pro.compose.yml
Normal file
60
host-stacks/vds-kzntsv/labtools-pro.compose.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
# labtools-pro — прод-фронт labtools.pro (snolla-приложение, server-side Liquid, node) на vds-kzntsv (89.253.255.94).
|
||||
#
|
||||
# STAGING-FIRST. Source-of-truth этого compose — admin-репо; применяется через Portainer
|
||||
# API/UI (VDS-rule: стеки через Portainer, не ssh+compose).
|
||||
#
|
||||
# Модель: НЕ pilonuxt/Nuxt-contentApi. Это монолит @snollajs/snolla (0.28.7) — читает БД-контент
|
||||
# MoreThenCms (mssql.kzntsv.site) + server-side Liquid render. Ассеты из MinIO (minio.kzntsv.site)
|
||||
# через imgproxy (imgproxy.kzntsv.site). node-config: NODE_ENV=production → default(dev,gitignored)+
|
||||
# production.json+ENV-override. КАТАЛОЖНЫЙ сайт (secции /products/*, редиректы).
|
||||
#
|
||||
# Образ собирается АДМИНОМ на VDS из victor/labtools.pro/deploy/Dockerfile (build-arg VERDACCIO_TOKEN,
|
||||
# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha монорепы labtools.pro.
|
||||
# ⚠️ Имя образа labtools-pro (НЕ labtools — тот у labtools.ru).
|
||||
#
|
||||
# Домен: STAGING — labtools-pro.vds.kzntsv.site (под wildcard *.vds.kzntsv.site, cert LE HTTP-01 авто).
|
||||
# Cutover live labtools.pro/www.labtools.pro — ОТДЕЛЬНЫЙ gated шаг: бой живой на RUVDS IIS, НЕ выводить.
|
||||
# DNS переключает ОПЕРАТОР. ⚠️ НЕ путать с labtools.ru — тот флипается отдельно. План отката = revert DNS.
|
||||
#
|
||||
# Runtime egress (наружу из proxy-сети): mssql.kzntsv.site:1433, minio.kzntsv.site:443,
|
||||
# imgproxy.kzntsv.site:443, smtp.yandex.ru:465.
|
||||
#
|
||||
# Секреты — env на VDS (Portainer stack env), НЕ в образ, НЕ в git. Значения ${...} инжектит Portainer
|
||||
# из stack-переменных (переиспользованы verbatim из labtools stack Id 17). Контракт:
|
||||
# DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD
|
||||
# siteId=663F9410-A6CC-4651-9A5C-62844A313957 — non-secret, в production.json (не env).
|
||||
|
||||
services:
|
||||
labtools-pro:
|
||||
container_name: labtools-pro
|
||||
image: registry.kzntsv.site/labtools-pro:0610432 # git-sha victor/labtools.pro master (snolla 0.42.1, bump 2026-07-05; prev 7bd9fae=0.28.7=rollback)
|
||||
restart: unless-stopped
|
||||
mem_limit: 512m # guardrail от runaway: snolla baseline ~100-200M; OOM в контейнере → restart:unless-stopped поднимет. standalone-compose → mem_limit (НЕ deploy.resources — то swarm-only).
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
# PORT не переопределяем — образ дефолтит 5000 (EXPOSE 5000, healthcheck на $PORT)
|
||||
# --- runtime-секреты (значения из Portainer stack env; НЕ хранить тут) ---
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
IMGPROXY_KEY: ${IMGPROXY_KEY}
|
||||
IMGPROXY_SALT: ${IMGPROXY_SALT}
|
||||
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
||||
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.labtools-pro.entrypoints: websecure
|
||||
# CUTOVER 2026-07-02: DNS labtools.pro/www флипнут на VDS (89.253.255.94) оператором.
|
||||
# Проверено авторитетным ns1.reg.ru + 8.8.8.8 → 89.253.255.94 (НЕ labtools.ru!). Боевые хосты.
|
||||
# staging-хост labtools-pro.vds.kzntsv.site УБРАН post-cutover. RUVDS IIS 80.64.31.36 — rollback, не тронут.
|
||||
traefik.http.routers.labtools-pro.rule: Host(`labtools.pro`) || Host(`www.labtools.pro`)
|
||||
traefik.http.routers.labtools-pro.tls.certresolver: letsEncrypt
|
||||
traefik.http.services.labtools-pro.loadbalancer.server.port: "5000"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
57
host-stacks/vds-kzntsv/labtools.compose.yml
Normal file
57
host-stacks/vds-kzntsv/labtools.compose.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
# labtools — prod-фронт labtools.ru (snolla-приложение, server-side Liquid, node) на vds-kzntsv (89.253.255.94).
|
||||
#
|
||||
# ЧЕРНОВИК / STAGING-FIRST. Source-of-truth этого compose — admin-репо; применяется через Portainer
|
||||
# API/UI (VDS-rule: стеки через Portainer, не ssh+compose).
|
||||
#
|
||||
# Модель: НЕ pilonuxt/Nuxt-contentApi. Это монолит @snollajs/snolla — читает БД-контент MoreThenCms
|
||||
# (mssql.kzntsv.site) + server-side Liquid render. Ассеты из MinIO (minio.kzntsv.site) через imgproxy
|
||||
# (imgproxy.kzntsv.site). node-config: NODE_ENV=production → default(dev,gitignored)+production.json+ENV-override.
|
||||
#
|
||||
# Образ собирается АДМИНОМ на VDS из victor/labtools.ru/deploy/Dockerfile (build-arg VERDACCIO_TOKEN,
|
||||
# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha монорепы labtools.ru. [ТЕГ TBD — ждём Dockerfile имплементера]
|
||||
#
|
||||
# Домен: STAGING — labtools.vds.kzntsv.site (под wildcard *.vds.kzntsv.site, cert LE HTTP-01 авто).
|
||||
# Cutover live www.labtools.ru — ОТДЕЛЬНЫЙ gated шаг: бой живой на RUVDS IIS (80.64.31.36), НЕ выводить.
|
||||
# DNS переключает ОПЕРАТОР по сигналу «staging green». План отката = revert DNS на RUVDS.
|
||||
#
|
||||
# Runtime egress (наружу из proxy-сети): mssql.kzntsv.site:1433, minio.kzntsv.site:443,
|
||||
# imgproxy.kzntsv.site:443, smtp.yandex.ru:465.
|
||||
#
|
||||
# Секреты — env на VDS (Portainer stack env), НЕ в образ, НЕ в git. Значения ${...} инжектит Portainer
|
||||
# из stack-переменных. Контракт (custom-environment-variables.json):
|
||||
# DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD
|
||||
|
||||
services:
|
||||
labtools:
|
||||
container_name: labtools
|
||||
image: registry.kzntsv.site/labtools:566d41c # git-sha victor/labtools.ru master (snolla 0.42.1, bump 2026-07-05; prev 43e28ba=0.28.2=rollback)
|
||||
restart: unless-stopped
|
||||
mem_limit: 512m # guardrail от runaway: snolla baseline ~100-200M; OOM в контейнере → restart:unless-stopped поднимет. standalone-compose → mem_limit (НЕ deploy.resources — то swarm-only).
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
# PORT не переопределяем — образ дефолтит 5000 (EXPOSE 5000, healthcheck на $PORT)
|
||||
# --- runtime-секреты (значения из Portainer stack env; НЕ хранить тут) ---
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
IMGPROXY_KEY: ${IMGPROXY_KEY}
|
||||
IMGPROXY_SALT: ${IMGPROXY_SALT}
|
||||
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
||||
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.labtools.entrypoints: websecure
|
||||
# CUTOVER 2026-07-02: DNS labtools.ru/www флипнут на VDS (89.253.255.94) оператором (Yandex DNS).
|
||||
# Проверено ОБА авторитетных Yandex-NS (dns1+dns2) согласованно → 89.253.255.94 (НЕ labtools.pro!). Боевые хосты.
|
||||
# staging-хост labtools.vds.kzntsv.site УБРАН post-cutover. RUVDS IIS 80.64.31.36 — rollback, не тронут.
|
||||
traefik.http.routers.labtools.rule: Host(`labtools.ru`) || Host(`www.labtools.ru`)
|
||||
traefik.http.routers.labtools.tls.certresolver: letsEncrypt
|
||||
traefik.http.services.labtools.loadbalancer.server.port: "5000"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
61
host-stacks/vds-kzntsv/on-snolla.compose.yml
Normal file
61
host-stacks/vds-kzntsv/on-snolla.compose.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
# on-snolla — посадочная on.snolla.com (snolla-app, server-side Liquid, node) на vds-kzntsv (89.253.255.94).
|
||||
#
|
||||
# STAGING-FIRST. Source-of-truth этого compose — admin-репо; применяется через Portainer API/UI
|
||||
# (VDS-rule: стеки через Portainer, не ssh+compose).
|
||||
#
|
||||
# Модель: монолит @snollajs/snolla (0.42.1) / core / liquid / data — читает БД-контент MoreThenCms
|
||||
# (mssql.kzntsv.site) + server-side Liquid render. Ассеты из MinIO (minio.kzntsv.site). In-process sharp,
|
||||
# delivery=serve-bytes, cache=local-variant. imgproxy в путях нет (как tandemmebel — сырой /imgproxy -> 404 норма).
|
||||
# node-config: NODE_ENV=production -> default(dev,gitignored)+production.json+ENV-override.
|
||||
# ЛЕНДИНГ (1 Page / + 2 StaticPages), БЕЗ блога/каталога/e-commerce.
|
||||
#
|
||||
# Образ собирается АДМИНОМ на VDS из victor/on.snolla.com/deploy/Dockerfile (build-arg VERDACCIO_TOKEN,
|
||||
# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha репо on.snolla.com (текущий 473923e494db).
|
||||
# Homepage render byte-identical прод-HTML (verified direct diff + completeness-gate 3/3 loc parity 2026-07-20).
|
||||
#
|
||||
# Домен: LIVE — on.snolla.com (cert LE HTTP-01, CN=on.snolla.com, issued 2026-07-20, until 2026-10-18).
|
||||
# Cutover завершён 2026-07-20: DNS reg.ru on.snolla.com A 80.64.31.36 -> 89.253.255.94 (operator flip,
|
||||
# авторит. NS ns1/ns2.reg.ru verified -> VDS), traefik rule staging->боевой (LE-порядок соблюдён),
|
||||
# LE-серт issued on first hit. Live-smoke GREEN, homepage byte-identical прод-HTML, sitemap 3/3 parity.
|
||||
# Предыстория: жил на RUVDS IIS (80.64.31.36) до 2026-07-20. RUVDS IIS не тронут (rollback = revert DNS).
|
||||
# План отката = revert DNS reg.ru -> 80.64.31.36 (RUVDS IIS жив) ИЛИ PUT стека 22 назад на тег (пока
|
||||
# единственный тег 473923e494db = :latest — первый deploy; образных rollback-тегов пока нет).
|
||||
#
|
||||
# Runtime egress: mssql.kzntsv.site:1433, minio.kzntsv.site:443 (оригиналы + variant-cache), smtp.yandex.ru:465.
|
||||
#
|
||||
# Секреты — env на VDS (Portainer stack env), НЕ в образ, НЕ в git. Значения ${...} инжектит Portainer
|
||||
# из stack-переменных (переиспользованы verbatim из tandemmebel стека 20 — общая snolla-инфра). Контракт:
|
||||
# DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD
|
||||
# siteId=B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7 — non-secret, в production.json (не env). Culture en. siteUrl=https://on.snolla.com
|
||||
|
||||
services:
|
||||
on-snolla:
|
||||
container_name: on-snolla
|
||||
image: registry.kzntsv.site/on-snolla:473923e494db # git-sha victor/on.snolla.com master 473923e494db (snolla 0.42.1). rollback-теги в registry: 473923e494db (:latest он же — первый deploy, отката пока нет, revert DNS -> RUVDS).
|
||||
restart: unless-stopped
|
||||
mem_limit: 512m # guardrail: snolla baseline ~100-200M; OOM -> restart. standalone-compose -> mem_limit (НЕ deploy.resources, swarm-only).
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
# --- runtime-секреты (значения из Portainer stack env; НЕ хранить тут) ---
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
IMGPROXY_KEY: ${IMGPROXY_KEY}
|
||||
IMGPROXY_SALT: ${IMGPROXY_SALT}
|
||||
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
||||
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.on-snolla.entrypoints: websecure
|
||||
# LIVE (cutover 2026-07-20). Rollback = Host(`on-snolla.vds.kzntsv.site`) + revert DNS -> 80.64.31.36.
|
||||
traefik.http.routers.on-snolla.rule: Host(`on.snolla.com`)
|
||||
traefik.http.routers.on-snolla.tls.certresolver: letsEncrypt
|
||||
traefik.http.services.on-snolla.loadbalancer.server.port: "5000"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
49
host-stacks/vds-kzntsv/pilonuxt.compose.yml
Normal file
49
host-stacks/vds-kzntsv/pilonuxt.compose.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
# pilonuxt — prod-фронт ПИЛОРАМА98 (Nuxt 4 / Nitro node-server) на vds-kzntsv (89.253.255.94).
|
||||
#
|
||||
# Деплой: Portainer-стек за Traefik. Source-of-truth этого compose — admin-репо;
|
||||
# применяется через Portainer API/UI (VDS-rule: стеки через Portainer, не ssh+compose).
|
||||
#
|
||||
# Образ собирается на workstation из МОНОРЕПЫ victor/pilorama98.ru (workspace-build:
|
||||
# контекст = корень монорепы, -f apps/web/Dockerfile, build secret VERDACCIO_TOKEN живой JWT).
|
||||
# runner несёт @img/sharp-* нативы (sharp приехал с snolla 0.17, imageResize). Тег = git-sha
|
||||
# монорепы. Push: с дома .output-слой ловит traefik-499 → docker save | ssh vds 'docker load'
|
||||
# + docker push С VDS. См. apps/web/.wiki/concepts/docker-deploy.md.
|
||||
#
|
||||
# Домен: на smoke-фазе — временный поддомен pilonuxt.vds.kzntsv.site (под wildcard
|
||||
# *.vds.kzntsv.site, cert LE HTTP-01 авто). Cutover www.pilorama98.ru — отдельным
|
||||
# шагом после зелёного smoke (правка rule + DNS reg.ru).
|
||||
#
|
||||
# Runtime egress (наружу из proxy-сети, по умолчанию открыт):
|
||||
# mssql.kzntsv.site:1433 (живые CMS-запросы, cacheTimeout=0) — обязателен;
|
||||
# smtp.yandex.ru:465 (письма форм contacts/checkout);
|
||||
# собственный origin https://pilonuxt.vds.kzntsv.site (SSR self-call на /snolla).
|
||||
#
|
||||
# Секреты (MSSQL/SMTP/imgproxy key+salt/siteId) ЗАПЕЧЕНЫ в config/default.json образа
|
||||
# (решение vitya — образ только в приватном registry). Env-override не настраивали.
|
||||
|
||||
services:
|
||||
pilonuxt:
|
||||
container_name: pilonuxt
|
||||
image: registry.kzntsv.site/pilonuxt:c4e34d4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
NITRO_HOST: 0.0.0.0
|
||||
NITRO_PORT: "3000"
|
||||
# CUTOVER 2026-06-15: боевой домен www.pilorama98.ru — аналитика ВКЛ
|
||||
# (дефолт GTM-MNNXFJ6, env не задаём). На smoke-поддомене гасили `""`.
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.pilonuxt.entrypoints: websecure
|
||||
# Боевые хосты pilorama98 (DNS → 89.253.255.94, cutover с RUVDS IIS) +
|
||||
# pilonuxt.vds.kzntsv.site оставлен для прямого smoke/диагностики.
|
||||
traefik.http.routers.pilonuxt.rule: Host(`www.pilorama98.ru`) || Host(`pilorama98.ru`) || Host(`pilonuxt.vds.kzntsv.site`)
|
||||
traefik.http.routers.pilonuxt.tls.certresolver: letsEncrypt
|
||||
traefik.http.services.pilonuxt.loadbalancer.server.port: "3000"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
67
host-stacks/vds-kzntsv/tandemmebel.compose.yml
Normal file
67
host-stacks/vds-kzntsv/tandemmebel.compose.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# tandemmebel — прод-фронт tandemmebel.ru (snolla-приложение, server-side Liquid, node) на vds-kzntsv (89.253.255.94).
|
||||
#
|
||||
# STAGING-FIRST. Source-of-truth этого compose — admin-репо; применяется через Portainer
|
||||
# API/UI (VDS-rule: стеки через Portainer, не ssh+compose).
|
||||
#
|
||||
# Модель: монолит @snollajs/snolla (0.42.1) / @snollajs/core (0.24.1) / liquid (0.10.2) / data (0.14.1) — читает БД-контент
|
||||
# MoreThenCms (mssql.kzntsv.site) + server-side Liquid render. Ассеты из MinIO (minio.kzntsv.site).
|
||||
# МЕДИА: in-process sharp (resize+data-driven watermark), delivery=serve-bytes, cache=local-variant→
|
||||
# бакет variant-cache в MinIO. imgproxy ИЗ ПУТИ tandem УБРАН (сырой /imgproxy → 404 норма).
|
||||
# node-config: NODE_ENV=production → default(dev,gitignored)+production.json+ENV-override.
|
||||
# КОНТЕНТ+БЛОГ-ПОРТФОЛИО+category сайт, БЕЗ e-commerce каталога.
|
||||
#
|
||||
# Образ собирается АДМИНОМ на VDS из victor/tandemmebel.ru/deploy/Dockerfile (build-arg VERDACCIO_TOKEN,
|
||||
# @snollajs/* из verdaccio.kzntsv.site). Тег = git-sha монорепы tandemmebel.ru (текущий 8df10ee).
|
||||
#
|
||||
# Домен: LIVE — tandemmebel.ru / www.tandemmebel.ru (cert LE HTTP-01, SAN оба, issued 2026-07-12).
|
||||
# Cutover завершён 2026-07-12: DNS reg.ru → 89.253.255.94 подтверждён на ns1/ns2.reg.ru →
|
||||
# traefik Host-rule staging→боевой (LE-порядок соблюдён), LE-серт issued on first hit.
|
||||
# In-place bump 2026-07-12: 0.42.0→0.42.1 (order-tag Drop-field fix), completeness-gate 184/184 parity GREEN.
|
||||
# In-place bump 2026-07-13: 0.42.1→0.42.1 (template-only, sha 0cd9351→8df10ee): remove FB/Twitter/Google+ share
|
||||
# buttons (extremist-icon compliance), keep VK+OK. Completeness-gate 184/184 parity GREEN, live-smoke 4
|
||||
# share-block pages vk+ok present / fb/tw/gp=0, TLS-серт не дёрнут. Same snolla 0.42.1 base, only
|
||||
# apps/web/views/social_buttons.liquid changed (1 file, 12 deletions).
|
||||
# Предыстория: жил на RUVDS IIS (80.64.31.36) до 2026-07-12.
|
||||
# План отката = revert DNS → 80.64.31.36 (RUVDS IIS жив, не тронут) ИЛИ PUT стека 20 назад на тег
|
||||
# 0cd9351 (0.42.1, {% order %} fix) / ed96b18 (0.42.0) / b02ca18 (rollback-теги в registry). Образ 8df10ee — latest.
|
||||
#
|
||||
# Runtime egress (наружу из proxy-сети): mssql.kzntsv.site:1433, minio.kzntsv.site:443
|
||||
# (оригиналы + запись вариантов в bucket variant-cache), smtp.yandex.ru:465. imgproxy НЕ нужен (sharp in-process).
|
||||
#
|
||||
# Секреты — env на VDS (Portainer stack env), НЕ в образ, НЕ в git. Значения ${...} инжектит Portainer
|
||||
# из stack-переменных (переиспользованы verbatim из labtools stack Id 17). Контракт:
|
||||
# DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD
|
||||
# siteId=78080707-F6E0-4330-BA30-7922354C2CEF — non-secret, в production.json (не env). Culture ru-RU. siteUrl=https://www.tandemmebel.ru
|
||||
|
||||
services:
|
||||
tandemmebel:
|
||||
container_name: tandemmebel
|
||||
image: registry.kzntsv.site/tandemmebel:8df10ee # git-sha victor/tandemmebel.ru master 8df10ee (snolla 0.42.1 / core 0.24.1 / liquid 0.10.2 / data 0.14.1 — template-only: remove FB/Twitter/Google+ share buttons, keep VK+OK; in-place bump 2026-07-13). rollback-теги 0cd9351 (0.42.1, {% order %} Drop-field fix) + ed96b18 (0.42.0) + b02ca18 в registry.
|
||||
restart: unless-stopped
|
||||
mem_limit: 512m # guardrail от runaway: snolla baseline ~100-200M; OOM в контейнере → restart:unless-stopped поднимет. standalone-compose → mem_limit (НЕ deploy.resources — то swarm-only).
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
# PORT не переопределяем — образ дефолтит 5000 (EXPOSE 5000, healthcheck на $PORT)
|
||||
# --- runtime-секреты (значения из Portainer stack env; НЕ хранить тут) ---
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
IMGPROXY_KEY: ${IMGPROXY_KEY}
|
||||
IMGPROXY_SALT: ${IMGPROXY_SALT}
|
||||
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
||||
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.tandemmebel.entrypoints: websecure
|
||||
# LIVE (cutover 2026-07-12). Rollback = Host(`tandemmebel.vds.kzntsv.site`) + revert DNS → 80.64.31.36.
|
||||
traefik.http.routers.tandemmebel.rule: Host(`tandemmebel.ru`) || Host(`www.tandemmebel.ru`)
|
||||
traefik.http.routers.tandemmebel.tls.certresolver: letsEncrypt
|
||||
traefik.http.services.tandemmebel.loadbalancer.server.port: "5000"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
34
scripts/books-vds-backup-daily-kreknin/.env.example
Normal file
34
scripts/books-vds-backup-daily-kreknin/.env.example
Normal file
@@ -0,0 +1,34 @@
|
||||
# /opt/stacks/backup/.env on books VDS — sourced by run.sh
|
||||
# Live values are NOT in git; sourced from pass entries:
|
||||
# pass show books-vds/full-env → BOOKS_DB_* + BOOKS_MONGO_*
|
||||
# pass show vds-kzntsv/full-env → NTFY_*
|
||||
# pass show snolla-smtp/full-env → SMTP_* + OPS_NOTIFY_EMAIL
|
||||
# pass show kreknin/full-env → DEST_USER (vitya), DEST_HOST (195.19.90.188)
|
||||
|
||||
# === Backup destination ===
|
||||
DEST_USER=vitya
|
||||
DEST_HOST=195.19.90.188
|
||||
DEST_BASE=/volume1/NetBackup/books-vds
|
||||
RETENTION_DAYS=7
|
||||
SSH_KEY=/opt/stacks/backup/kreknin-key
|
||||
|
||||
# === DB creds (books VDS containers) ===
|
||||
BOOKS_DB_ROOT_PASSWORD=...
|
||||
BOOKS_MONGO_USERNAME=root
|
||||
BOOKS_MONGO_PASSWORD=...
|
||||
|
||||
# === Elasticsearch snapshot repo (file-based, registered 2026-05-25) ===
|
||||
ES_SNAPSHOT_REPO=kreknin
|
||||
|
||||
# === NTFY (shared channel — vds-backup topic; ntfy.vds.kzntsv.site) ===
|
||||
NTFY_URL=https://ntfy.vds.kzntsv.site
|
||||
NTFY_USER=...
|
||||
NTFY_PASS=...
|
||||
|
||||
# === SMTP (Yandex relay, noreply@snolla.com) ===
|
||||
SMTP_HOST=smtp.yandex.ru
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=noreply@snolla.com
|
||||
SMTP_PASS=...
|
||||
SMTP_FROM=noreply@snolla.com
|
||||
OPS_NOTIFY_EMAIL=...
|
||||
115
scripts/books-vds-backup-daily-kreknin/README.md
Normal file
115
scripts/books-vds-backup-daily-kreknin/README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# books-vds-backup-daily-kreknin
|
||||
|
||||
Daily backup pipeline: **books VDS** (`89.253.255.133` / host4g.ru CentOS 7) → **kreknin Synology** (`195.19.90.188:/volume1/NetBackup/books-vds/<date>/`) via SSH + rsync.
|
||||
|
||||
Schedule: `0 6 * * *` Europe/Moscow (after VDS-infra at 05:00, after windows-host at 05:30). Pattern mirrors `scripts/vds-backup-rsync-kreknin/run.sh`.
|
||||
|
||||
## What's backed up
|
||||
|
||||
| Component | Method | Source |
|
||||
|---|---|---|
|
||||
| MariaDB (books-db) | `mariadb-dump --all-databases --single-transaction` | container `books-db` |
|
||||
| Mongo (shared) | `mongodump --archive` w/ root auth | container `mongo` |
|
||||
| Mongo (job-scheduler) | `mongodump --archive` no-auth | container `books-job-scheduler-mongo` |
|
||||
| Elasticsearch | REST snapshot API → fs repo `kreknin` → rsync | container `elasticsearch` (path.repo=/snapshots) |
|
||||
| MinIO blobs | rsync direct (immutable content-addressed) | `/usr/docker/minio/data` |
|
||||
| App configs | rsync | `/opt/books/{api,job-scheduler,task-runner,ntfy,books-ops-mcp}` |
|
||||
| Traefik state | rsync | `/usr/docker/traefik/{letsencrypt,data}` (acme.json + config) |
|
||||
| System config | rsync | `/etc/{ssh,hosts,cron.d}`, `/root/.ssh` |
|
||||
|
||||
Retention: **7 daily snapshots** on kreknin (+ on ES repo). Older — pruned at end of each run.
|
||||
|
||||
## Notifications
|
||||
|
||||
Dual-channel after every run:
|
||||
|
||||
- **ntfy** → topic `vds-backup` on `ntfy.vds.kzntsv.site` (shared с VDS + RUVDS + windows-host backups).
|
||||
- Success: title `BOOKS-VDS backup OK <date>`, tag `green_circle`.
|
||||
- Fail: title `BOOKS-VDS backup FAILED <date>`, tag `red_circle`, priority `high`.
|
||||
- **Email** via Yandex SMTP (`noreply@snolla.com` → `OPS_NOTIFY_EMAIL`).
|
||||
- Sent via `curl --ssl-reqd --url smtp://...:587` (no msmtp dep on CentOS 7).
|
||||
- Subject: `[BOOKS-VDS] backup OK <date>` / `[BOOKS-VDS] backup FAILED <date>`.
|
||||
|
||||
If notify-channels fail — backup itself does NOT fail (`|| true`).
|
||||
|
||||
## Files
|
||||
|
||||
- `run.sh` — backup logic. Synced to `/opt/stacks/backup/run.sh` on books VDS.
|
||||
- `.env.example` — placeholder. Live `.env` built from `pass show` entries during install.
|
||||
|
||||
## Initial install (rebuild from scratch)
|
||||
|
||||
```bash
|
||||
# From workstation, with id_ed25519_books_ops authorized:
|
||||
ssh root@89.253.255.133 'mkdir -p /opt/stacks/backup'
|
||||
scp scripts/books-vds-backup-daily-kreknin/run.sh root@89.253.255.133:/opt/stacks/backup/
|
||||
|
||||
# Build .env locally from pass entries:
|
||||
{
|
||||
echo "DEST_USER=vitya"
|
||||
echo "DEST_HOST=195.19.90.188"
|
||||
echo "DEST_BASE=/volume1/NetBackup/books-vds"
|
||||
echo "RETENTION_DAYS=7"
|
||||
echo "SSH_KEY=/opt/stacks/backup/kreknin-key"
|
||||
echo "ES_SNAPSHOT_REPO=kreknin"
|
||||
pass show books-vds/full-env | grep -E '^(BOOKS_DB_ROOT_PASSWORD|BOOKS_MONGO_)'
|
||||
pass show vds-kzntsv/full-env | grep -E '^NTFY_'
|
||||
pass show snolla-smtp/full-env | grep -E '^(SMTP_|OPS_NOTIFY_EMAIL)'
|
||||
} | scp /dev/stdin root@89.253.255.133:/opt/stacks/backup/.env
|
||||
ssh root@89.253.255.133 'chmod 600 /opt/stacks/backup/.env && chmod 755 /opt/stacks/backup/run.sh'
|
||||
|
||||
# Generate kreknin SSH key on books VDS:
|
||||
ssh root@89.253.255.133 'ssh-keygen -t ed25519 -f /opt/stacks/backup/kreknin-key -N "" -C "books-vds-backup"'
|
||||
|
||||
# Get pubkey and authorize it on kreknin (workstation pivots — books VDS has no kreknin SSH yet):
|
||||
PUBKEY=$(ssh root@89.253.255.133 cat /opt/stacks/backup/kreknin-key.pub)
|
||||
ssh -i ~/.ssh/id_ed25519_kreknin vitya@195.19.90.188 \
|
||||
"echo '$PUBKEY' >> ~/.ssh/authorized_keys && mkdir -p /volume1/NetBackup/books-vds"
|
||||
|
||||
# Verify SSH books → kreknin
|
||||
ssh root@89.253.255.133 'ssh -i /opt/stacks/backup/kreknin-key -o StrictHostKeyChecking=accept-new vitya@195.19.90.188 hostname'
|
||||
|
||||
# Smoke run (manual)
|
||||
ssh root@89.253.255.133 '/opt/stacks/backup/run.sh'
|
||||
|
||||
# Install cron
|
||||
ssh root@89.253.255.133 'echo "0 6 * * * root /opt/stacks/backup/run.sh" > /etc/cron.d/books-vds-backup && chmod 644 /etc/cron.d/books-vds-backup'
|
||||
```
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **ES snapshot via REST API**, not data-dir rsync. Filesystem snapshot repo (`type=fs`, `location=/snapshots`, `compress=true`) registered as `kreknin` on 2026-05-25. Required one-time stack edit (add `path.repo=/snapshots` env + `./snapshots:/snapshots` bind in `/usr/docker/elasticsearch/docker-compose.yml`).
|
||||
- **`curl --ssl-reqd` SMTP** instead of msmtp — avoid CentOS 7 msmtp install (EOL repo issues). Native `curl` works on STARTTLS port 587.
|
||||
- **No `tasks.yml`-style auth on `books-job-scheduler-mongo`** — container has no `MONGO_INITDB_ROOT_*` env. `mongodump --archive` без `--uri` достаточен.
|
||||
- **rsync /opt/books + /usr/docker/{minio,elasticsearch/snapshots,traefik}** — bind paths из inspect 2026-05-25; не `/var/lib/docker/volumes/` (named volumes для контейнеров пустые, кроме portainer/scheduler-mongo).
|
||||
- **`books-vds-portainer/full-env` consolidated** into `books-vds/full-env` for one-file-per-host pattern parity with `vds-kzntsv/full-env`.
|
||||
- **No `BackupConfig` snapshot/git pre-commit-like step** — VDS-infra `run.sh` тоже не делает, и нет аналогичной системы на books VDS.
|
||||
|
||||
## Smoke run (manual)
|
||||
|
||||
```bash
|
||||
ssh root@89.253.255.133 '/opt/stacks/backup/run.sh'
|
||||
# Watch log:
|
||||
ssh root@89.253.255.133 'tail -F /var/log/books-vds-backup/$(date +%Y-%m-%d).log'
|
||||
```
|
||||
|
||||
Verify on kreknin:
|
||||
```bash
|
||||
ssh vitya@195.19.90.188 'du -sh /volume1/NetBackup/books-vds/*/'
|
||||
```
|
||||
|
||||
## Atomic revert (uninstall)
|
||||
|
||||
```bash
|
||||
# On books VDS:
|
||||
ssh root@89.253.255.133 'rm /etc/cron.d/books-vds-backup; rm -rf /opt/stacks/backup /var/log/books-vds-backup'
|
||||
|
||||
# On kreknin (via SSH):
|
||||
ssh vitya@195.19.90.188 'rm -rf /volume1/NetBackup/books-vds'
|
||||
# Remove the books-vds pubkey line from authorized_keys (one line with 'books-vds-backup' comment).
|
||||
|
||||
# On books VDS — revert ES path.repo change:
|
||||
ssh root@89.253.255.133 'cd /usr/docker/elasticsearch && \
|
||||
cp docker-compose.yml.bak-pre-snapshots-2026-05-25 docker-compose.yml && \
|
||||
docker rm -f elasticsearch && docker-compose up -d'
|
||||
```
|
||||
258
scripts/books-vds-backup-daily-kreknin/run.sh
Normal file
258
scripts/books-vds-backup-daily-kreknin/run.sh
Normal file
@@ -0,0 +1,258 @@
|
||||
#!/bin/bash
|
||||
# Daily books VDS → kreknin backup. Triggered by /etc/cron.d/books-vds-backup at 06:00 MSK.
|
||||
# Pattern: mirror VDS scripts/vds-backup-rsync-kreknin/run.sh, adapted for books VDS hybrid stack.
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE=/opt/stacks/backup/.env
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
LOG_DIR=/var/log/books-vds-backup
|
||||
LOG="$LOG_DIR/$TODAY.log"
|
||||
LOCK=/tmp/books-vds-backup.lock
|
||||
DUMP_DIR=/tmp/books-vds-backup-dumps/$TODAY
|
||||
DEST_PATH="$DEST_BASE/$TODAY"
|
||||
LATEST="$DEST_BASE/latest"
|
||||
|
||||
mkdir -p "$LOG_DIR" "$DUMP_DIR"
|
||||
exec >> "$LOG" 2>&1
|
||||
echo "===== BOOKS-VDS backup start $TODAY $(date -Is) ====="
|
||||
|
||||
# single-instance lock
|
||||
exec 9>"$LOCK"
|
||||
if ! flock -n 9; then echo "already running, exiting"; exit 0; fi
|
||||
|
||||
START=$(date +%s)
|
||||
SSH_OPTS="-i $SSH_KEY -o StrictHostKeyChecking=yes -o ConnectTimeout=30 -o LogLevel=ERROR"
|
||||
# Note: kreknin host key must be pre-populated in /root/.ssh/known_hosts (CentOS 7 OpenSSH 7.4 has no accept-new option)
|
||||
|
||||
HOST_LABEL=BOOKS-VDS
|
||||
SOURCE_DISPLAY="89.253.255.133 (books VDS)"
|
||||
|
||||
notify() {
|
||||
local title="$1" msg="$2" tags="${3:-}" prio="${4:-default}"
|
||||
curl -sS --max-time 10 -u "$NTFY_USER:$NTFY_PASS" \
|
||||
-H "Title: $title" -H "Tags: $tags" -H "Priority: $prio" \
|
||||
-d "$msg" "$NTFY_URL/vds-backup" > /dev/null || true
|
||||
}
|
||||
|
||||
email_send() {
|
||||
local subj="$1" body="$2"
|
||||
local tmpf
|
||||
tmpf=$(mktemp)
|
||||
# RFC 5322: CRLF headers + Date required by Yandex
|
||||
{
|
||||
printf 'From: %s\r\n' "$SMTP_FROM"
|
||||
printf 'To: %s\r\n' "$OPS_NOTIFY_EMAIL"
|
||||
printf 'Subject: %s\r\n' "$subj"
|
||||
printf 'Date: %s\r\n' "$(date -R)"
|
||||
printf 'MIME-Version: 1.0\r\n'
|
||||
printf 'Content-Type: text/plain; charset=UTF-8\r\n'
|
||||
printf '\r\n'
|
||||
echo "$body" | sed 's/$/\r/'
|
||||
} > "$tmpf"
|
||||
# smtps:// (implicit TLS) for Yandex port 465; smtp:// (STARTTLS) would be port 587.
|
||||
# SMTP_PORT in pass is 465 — use smtps:// scheme.
|
||||
curl -sS --max-time 30 \
|
||||
--ssl-reqd \
|
||||
--url "smtps://$SMTP_HOST:$SMTP_PORT" \
|
||||
--user "$SMTP_USER:$SMTP_PASS" \
|
||||
--mail-from "$SMTP_FROM" \
|
||||
--mail-rcpt "$OPS_NOTIFY_EMAIL" \
|
||||
--upload-file "$tmpf" || true
|
||||
rm -f "$tmpf"
|
||||
}
|
||||
|
||||
fmt_duration() {
|
||||
local s=$1
|
||||
printf "%dm%02ds" $((s/60)) $((s%60))
|
||||
}
|
||||
|
||||
on_error() {
|
||||
local line=$1 rc=$2
|
||||
local err="line $line, exit $rc"
|
||||
local now
|
||||
now=$(date +%s)
|
||||
local dur=$((now - ${START:-now}))
|
||||
local dur_h
|
||||
dur_h=$(fmt_duration "$dur")
|
||||
local tail_log
|
||||
tail_log=$(tail -40 "$LOG" 2>/dev/null || true)
|
||||
|
||||
echo "FATAL: $HOST_LABEL backup FAILED — $err"
|
||||
|
||||
notify "$HOST_LABEL backup FAILED $TODAY" \
|
||||
"After $dur_h: $err. See $LOG" \
|
||||
red_circle high
|
||||
|
||||
email_send "[$HOST_LABEL] backup FAILED $TODAY" \
|
||||
"$HOST_LABEL daily backup FAILED.
|
||||
|
||||
Date: $TODAY
|
||||
Duration: $dur_h
|
||||
Error: $err
|
||||
Source: $SOURCE_DISPLAY
|
||||
Log: $LOG
|
||||
|
||||
Tail (last 40 lines):
|
||||
$tail_log"
|
||||
exit "$rc"
|
||||
}
|
||||
trap 'on_error $LINENO $?' ERR
|
||||
|
||||
# === Step 1: DB dumps ===
|
||||
echo "--- DB dumps to $DUMP_DIR ---"
|
||||
|
||||
docker exec books-db mariadb-dump -uroot -p"$BOOKS_DB_ROOT_PASSWORD" \
|
||||
--all-databases --single-transaction --quick 2>/dev/null \
|
||||
| gzip > "$DUMP_DIR/mariadb.sql.gz"
|
||||
echo "mariadb: $(stat -c %s "$DUMP_DIR/mariadb.sql.gz") bytes"
|
||||
|
||||
docker exec mongo mongodump --archive --quiet \
|
||||
--uri="mongodb://$BOOKS_MONGO_USERNAME:$BOOKS_MONGO_PASSWORD@localhost:27017/?authSource=admin" 2>/dev/null \
|
||||
| gzip > "$DUMP_DIR/mongo.archive.gz"
|
||||
echo "mongo (shared): $(stat -c %s "$DUMP_DIR/mongo.archive.gz") bytes"
|
||||
|
||||
docker exec books-job-scheduler-mongo mongodump --archive --quiet 2>/dev/null \
|
||||
| gzip > "$DUMP_DIR/job-scheduler-mongo.archive.gz"
|
||||
echo "job-scheduler-mongo: $(stat -c %s "$DUMP_DIR/job-scheduler-mongo.archive.gz") bytes"
|
||||
|
||||
# bookva tenant (separate cloned stack): bookva-db shares BOOKS_DB_ROOT_PASSWORD;
|
||||
# bookva-mongo is no-auth. Containers are named volumes (not bind paths) → logical dumps.
|
||||
docker exec bookva-db mariadb-dump -uroot -p"$BOOKS_DB_ROOT_PASSWORD" \
|
||||
--all-databases --single-transaction --quick 2>/dev/null \
|
||||
| gzip > "$DUMP_DIR/bookva-mariadb.sql.gz"
|
||||
echo "bookva-db (mariadb): $(stat -c %s "$DUMP_DIR/bookva-mariadb.sql.gz") bytes"
|
||||
|
||||
docker exec bookva-mongo mongodump --archive --quiet 2>/dev/null \
|
||||
| gzip > "$DUMP_DIR/bookva-mongo.archive.gz"
|
||||
echo "bookva-mongo: $(stat -c %s "$DUMP_DIR/bookva-mongo.archive.gz") bytes"
|
||||
|
||||
# === Step 2: ES snapshot via REST ===
|
||||
echo "--- ES snapshot daily-$TODAY in repo $ES_SNAPSHOT_REPO ---"
|
||||
ES_SNAP="daily-$TODAY"
|
||||
SNAP_RESULT=$(docker exec elasticsearch curl -sS -X PUT \
|
||||
"http://localhost:9200/_snapshot/$ES_SNAPSHOT_REPO/$ES_SNAP?wait_for_completion=true" \
|
||||
-H 'Content-Type: application/json')
|
||||
echo "ES snapshot result: $SNAP_RESULT"
|
||||
if ! echo "$SNAP_RESULT" | grep -q '"state":"SUCCESS"'; then
|
||||
echo "ES snapshot did NOT report SUCCESS state — failing"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
echo "--- prune ES snapshots older than $RETENTION_DAYS ---"
|
||||
# Pure bash parse — no jq/python dependency
|
||||
ALL_SNAPS=$(docker exec elasticsearch curl -sS "http://localhost:9200/_snapshot/$ES_SNAPSHOT_REPO/_all" \
|
||||
| grep -oE '"snapshot":"daily-[0-9-]+"' \
|
||||
| sed -E 's/.*"(daily-[0-9-]+)".*/\1/' \
|
||||
| sort)
|
||||
SNAP_COUNT=$(echo "$ALL_SNAPS" | wc -l)
|
||||
PRUNE_COUNT=$((SNAP_COUNT - RETENTION_DAYS))
|
||||
if [ $PRUNE_COUNT -gt 0 ]; then
|
||||
echo "$ALL_SNAPS" | head -n "$PRUNE_COUNT" | while read -r snap; do
|
||||
[ -z "$snap" ] && continue
|
||||
echo "deleting ES snapshot: $snap"
|
||||
docker exec elasticsearch curl -sS -X DELETE \
|
||||
"http://localhost:9200/_snapshot/$ES_SNAPSHOT_REPO/$snap" || true
|
||||
echo
|
||||
done
|
||||
fi
|
||||
|
||||
# === Step 2b: bookva-es snapshot (separate ES, own /snapshots repo, added 2026-06-12) ===
|
||||
echo "--- bookva-es snapshot $ES_SNAP in repo $ES_SNAPSHOT_REPO ---"
|
||||
BSNAP_RESULT=$(docker exec bookva-es curl -sS -X PUT \
|
||||
"http://localhost:9200/_snapshot/$ES_SNAPSHOT_REPO/$ES_SNAP?wait_for_completion=true" \
|
||||
-H 'Content-Type: application/json')
|
||||
echo "bookva-es snapshot result: $BSNAP_RESULT"
|
||||
if ! echo "$BSNAP_RESULT" | grep -q '"state":"SUCCESS"'; then
|
||||
echo "bookva-es snapshot did NOT report SUCCESS state — failing"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
echo "--- prune bookva-es snapshots older than $RETENTION_DAYS ---"
|
||||
BALL_SNAPS=$(docker exec bookva-es curl -sS "http://localhost:9200/_snapshot/$ES_SNAPSHOT_REPO/_all" \
|
||||
| grep -oE '"snapshot":"daily-[0-9-]+"' \
|
||||
| sed -E 's/.*"(daily-[0-9-]+)".*/\1/' \
|
||||
| sort)
|
||||
BSNAP_COUNT=$(echo "$BALL_SNAPS" | wc -l)
|
||||
BPRUNE_COUNT=$((BSNAP_COUNT - RETENTION_DAYS))
|
||||
if [ $BPRUNE_COUNT -gt 0 ]; then
|
||||
echo "$BALL_SNAPS" | head -n "$BPRUNE_COUNT" | while read -r snap; do
|
||||
[ -z "$snap" ] && continue
|
||||
echo "deleting bookva-es snapshot: $snap"
|
||||
docker exec bookva-es curl -sS -X DELETE \
|
||||
"http://localhost:9200/_snapshot/$ES_SNAPSHOT_REPO/$snap" || true
|
||||
echo
|
||||
done
|
||||
fi
|
||||
|
||||
# === Step 3: rsync to kreknin ===
|
||||
echo "--- rsync to $DEST_USER@$DEST_HOST:$DEST_PATH ---"
|
||||
ssh $SSH_OPTS "$DEST_USER@$DEST_HOST" "mkdir -p '$DEST_PATH'"
|
||||
|
||||
LINK_DEST_ARG=""
|
||||
if ssh $SSH_OPTS "$DEST_USER@$DEST_HOST" "test -L '$LATEST' || test -d '$LATEST'" 2>/dev/null; then
|
||||
LINK_DEST_ARG="--link-dest=$LATEST"
|
||||
fi
|
||||
|
||||
rsync -aHh --info=stats2 --delete $LINK_DEST_ARG \
|
||||
-e "ssh $SSH_OPTS" \
|
||||
/opt/books \
|
||||
/usr/docker/elasticsearch/snapshots \
|
||||
/usr/docker/bookva-es \
|
||||
/usr/docker/minio/data \
|
||||
/var/lib/docker/volumes/bookva-minio-data \
|
||||
/usr/docker/traefik/letsencrypt \
|
||||
/usr/docker/traefik/data \
|
||||
/etc/ssh \
|
||||
/etc/hosts \
|
||||
/etc/cron.d \
|
||||
/root/.ssh \
|
||||
"$DUMP_DIR" \
|
||||
"$DEST_USER@$DEST_HOST:$DEST_PATH/" || true
|
||||
|
||||
ssh $SSH_OPTS "$DEST_USER@$DEST_HOST" "rm -f '$LATEST' && ln -s '$DEST_PATH' '$LATEST'"
|
||||
|
||||
# === Step 4: retention prune on kreknin ===
|
||||
echo "--- prune snapshots older than $RETENTION_DAYS days on kreknin ---"
|
||||
ssh $SSH_OPTS "$DEST_USER@$DEST_HOST" \
|
||||
"cd '$DEST_BASE' && ls -1d 20*-*-* 2>/dev/null | sort | head -n -$RETENTION_DAYS | xargs -r rm -rf"
|
||||
|
||||
# === Step 5: local cleanup ===
|
||||
rm -rf "$DUMP_DIR"
|
||||
|
||||
# === Step 6: report ===
|
||||
END=$(date +%s)
|
||||
DURATION=$((END - START))
|
||||
SIZE=$(ssh $SSH_OPTS "$DEST_USER@$DEST_HOST" "du -sh '$DEST_PATH' 2>/dev/null | cut -f1" || echo "?")
|
||||
SNAPSHOT_COUNT=$(ssh $SSH_OPTS "$DEST_USER@$DEST_HOST" "ls -1d $DEST_BASE/20*-*-* 2>/dev/null | wc -l" || echo "?")
|
||||
DURATION_HUMAN=$(fmt_duration "$DURATION")
|
||||
|
||||
NTFY_BODY="$DURATION_HUMAN, size=$SIZE, snapshots=$SNAPSHOT_COUNT, dest=kreknin:$DEST_PATH"
|
||||
notify "$HOST_LABEL backup OK $TODAY" "$NTFY_BODY" green_circle
|
||||
|
||||
EMAIL_BODY="$HOST_LABEL daily backup completed successfully.
|
||||
|
||||
Date: $TODAY
|
||||
Duration: $DURATION_HUMAN
|
||||
Size: $SIZE
|
||||
Snapshots: $SNAPSHOT_COUNT
|
||||
Source: $SOURCE_DISPLAY
|
||||
Dest: kreknin:$DEST_PATH
|
||||
|
||||
Components:
|
||||
- /opt/books/{api,job-scheduler,task-runner,ntfy,books-ops-mcp}
|
||||
- /usr/docker/{minio/data, elasticsearch/snapshots, traefik/{letsencrypt,data}}
|
||||
- /etc/{ssh,hosts,cron.d}
|
||||
- /root/.ssh
|
||||
- DB dumps: books-db (mariadb), mongo (shared), books-job-scheduler-mongo, bookva-db (mariadb), bookva-mongo
|
||||
- bookva-minio: raw volume (bookva-minio-data)
|
||||
- ES snapshots: $ES_SNAP (slovo elasticsearch + bookva-es, separate repos)
|
||||
|
||||
Log: $LOG"
|
||||
email_send "[$HOST_LABEL] backup OK $TODAY" "$EMAIL_BODY"
|
||||
|
||||
echo "$HOST_LABEL backup OK: $NTFY_BODY"
|
||||
echo "===== $HOST_LABEL backup done $(date -Is) ====="
|
||||
84
scripts/books-vds-portainer-migration/README.md
Normal file
84
scripts/books-vds-portainer-migration/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# books-vds-portainer-migration
|
||||
|
||||
Migrates SSH-compose stacks at `/usr/docker/<stack>/` on books VDS (`89.253.255.133`) to Portainer-managed via API. Adapted from canonical pattern: [`.wiki/concepts/portainer-stack-management-vds.md`](../../.wiki/concepts/portainer-stack-management-vds.md).
|
||||
|
||||
Related task: [`books-vds-stacks-to-portainer`](../../.tasks/books-vds-stacks-to-portainer.md).
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# 1. Get PAT from pass
|
||||
export BOOKS_PORTAINER_API_KEY=$(pass show books-vds/full-env BOOKS_PORTAINER_API_KEY)
|
||||
|
||||
# 2. Copy script to VDS
|
||||
scp -i ~/.ssh/id_ed25519_books_ops migrate.sh root@89.253.255.133:/tmp/
|
||||
|
||||
# 3. Migrate one stack at a time, verify between
|
||||
ssh -i ~/.ssh/id_ed25519_books_ops root@89.253.255.133 \
|
||||
"BOOKS_PORTAINER_API_KEY='$BOOKS_PORTAINER_API_KEY' bash /tmp/migrate.sh proxy-chain"
|
||||
```
|
||||
|
||||
## Ladder (lowest → highest blast radius)
|
||||
|
||||
1. **proxy-chain** — internal-only, no traefik exposure → smoke for the script
|
||||
2. **imgproxy** — 2-container compose, serves `imgproxy.kzntsv.site`
|
||||
3. **minio** — read-mostly, books-api retries OK
|
||||
4. **mongo** — shared, used by books-api + books-task-runner. **VERIFY retry-tolerance first**
|
||||
5. **books-db** — MariaDB, books-api connection-pool reconnects
|
||||
6. **elasticsearch** — currently "not used" per user. Verify before recreate.
|
||||
|
||||
## Smoke per stack (after each migration)
|
||||
|
||||
```bash
|
||||
# proxy-chain — internal smoke (no public endpoint)
|
||||
docker exec proxy-chain wget -qO- localhost:8000 || echo "$?"
|
||||
|
||||
# imgproxy — serves imgproxy.kzntsv.site
|
||||
curl -sS -o /dev/null -w "%{http_code}\n" https://imgproxy.kzntsv.site/
|
||||
|
||||
# minio
|
||||
curl -sS -o /dev/null -w "%{http_code}\n" https://minio.kzntsv.site/minio/health/live
|
||||
|
||||
# mongo
|
||||
docker exec mongo mongo --quiet --eval 'db.adminCommand({ping:1}).ok' \
|
||||
-u root -p "$(pass show books-vds/full-env | grep MONGO_PWD)"
|
||||
|
||||
# books-db
|
||||
docker exec books-db mariadb -uroot -p"$(pass show books-vds/full-env | grep BOOKS_DB_PWD)" -e 'SELECT 1'
|
||||
|
||||
# elasticsearch
|
||||
docker exec elasticsearch curl -sS localhost:9200/_cluster/health | jq .status
|
||||
```
|
||||
|
||||
## What the script does
|
||||
|
||||
1. **Backup** `docker-compose.yml.bak-pre-portainer-migration-<date>` (idempotent — keeps first backup).
|
||||
2. **Transform** compose: strip `env_file:` (defensive — books VDS has none), absolutize `./` paths to `/usr/docker/<stack>/`.
|
||||
3. **Parse** `.env` into API env array (no-op for books VDS — no `.env` files).
|
||||
4. **Delete** existing Portainer stack with same name on endpoint 1 (idempotency for re-runs).
|
||||
5. **Down** ad-hoc containers via `docker rm -f` by `com.docker.compose.project` label (bypasses docker-compose v1 `ContainerConfig` bug).
|
||||
6. **POST** `/api/stacks/create/standalone/string?endpointId=1` — Portainer pulls images, creates network attachments, applies traefik labels through docker socket events.
|
||||
|
||||
## Diffs from VDS-infra script (`portainer-stack-management-vds.md`)
|
||||
|
||||
| Aspect | VDS-infra | books VDS |
|
||||
|---|---|---|
|
||||
| Portainer URL | `portainer.vds.kzntsv.site` | `portainer.kzntsv.site` |
|
||||
| Endpoint ID | 1 (same) | 1 |
|
||||
| Stack dir prefix | `/opt/stacks/<stack>/` | `/usr/docker/<stack>/` |
|
||||
| Auth | JWT (PAT 401'd) | **X-API-Key** (PAT works) |
|
||||
| Compose down | `cd $DIR && docker compose down` | **`docker rm -f` by label** (skips compose binary entirely) |
|
||||
| `env_file:` strip | required (most stacks had .env) | defensive (books VDS has none) |
|
||||
|
||||
## Safety
|
||||
|
||||
- **Re-runnable.** Delete-then-create idempotency means re-running for the same stack just rebuilds it (data binds preserved, names preserved, network re-attached).
|
||||
- **Compose file preserved on disk** (in `/usr/docker/<stack>/` + dated backup). Portainer manages lifecycle but doesn't move the file.
|
||||
- **Bind paths preserved** — `/usr/docker/<stack>/data` etc. stay at the same disk path. Backup pipeline (`scripts/books-vds-backup-daily-kreknin/`) doesn't need adjustment.
|
||||
- **Skips traefik / portainer** by name — refuses to migrate management plane.
|
||||
|
||||
## Post-migration
|
||||
|
||||
- Update [`.wiki/entities/books-vds.md`](../../.wiki/entities/books-vds.md) — flip each stack from "SSH-managed" to "Portainer-managed" table.
|
||||
- Extend or fork [`.wiki/concepts/portainer-stack-management-vds.md`](../../.wiki/concepts/portainer-stack-management-vds.md) with books VDS section + gotchas if new ones encountered.
|
||||
- Close task as 🟢.
|
||||
109
scripts/books-vds-portainer-migration/migrate.sh
Normal file
109
scripts/books-vds-portainer-migration/migrate.sh
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
# books-vds-portainer-migration/migrate.sh
|
||||
#
|
||||
# Migrates one SSH-compose stack at /usr/docker/<STACK>/ to Portainer-managed via API.
|
||||
# Pattern source: .wiki/concepts/portainer-stack-management-vds.md § Migration script.
|
||||
# Diffs from VDS-infra version:
|
||||
# PORTAINER_URL = https://portainer.kzntsv.site
|
||||
# ENDPOINT_ID = 1 (books VDS local docker daemon)
|
||||
# DIR prefix = /usr/docker/<stack> (not /opt/stacks/<stack>)
|
||||
# Auth = X-API-Key header (PAT works, no JWT fallback)
|
||||
# Down step = direct `docker rm -f` by compose-project label
|
||||
# (bypasses both v1 standalone quirks and Portainer recreate edge cases)
|
||||
#
|
||||
# Usage (run on books VDS):
|
||||
# scp migrate.sh root@89.253.255.133:/tmp/
|
||||
# ssh root@89.253.255.133
|
||||
# export BOOKS_PORTAINER_API_KEY="ptr_..." # from `pass show books-vds/full-env`
|
||||
# bash /tmp/migrate.sh <stack-name>
|
||||
#
|
||||
# Targets (recommended ladder, lowest → highest blast radius):
|
||||
# proxy-chain → imgproxy → minio → mongo → books-db → elasticsearch
|
||||
#
|
||||
# Skip (management plane — would lose access):
|
||||
# traefik, portainer
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STACK="${1:?stack name required — e.g. proxy-chain | imgproxy | minio | mongo | books-db | elasticsearch}"
|
||||
DIR="/usr/docker/$STACK"
|
||||
PORTAINER_URL="https://portainer.kzntsv.site"
|
||||
ENDPOINT_ID=1
|
||||
API_KEY="${BOOKS_PORTAINER_API_KEY:?env var required — pass show books-vds/full-env BOOKS_PORTAINER_API_KEY}"
|
||||
|
||||
# Refuse to migrate management-plane stacks
|
||||
case "$STACK" in
|
||||
traefik|portainer)
|
||||
echo "✗ refusing to migrate '$STACK' — management plane (recreate breaks access)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -d "$DIR" ] || { echo "✗ missing $DIR"; exit 1; }
|
||||
[ -f "$DIR/docker-compose.yml" ] || { echo "✗ missing $DIR/docker-compose.yml"; exit 1; }
|
||||
|
||||
echo "=== migrate '$STACK' from $DIR → Portainer endpoint $ENDPOINT_ID ==="
|
||||
|
||||
# 1. Backup compose file (idempotent — keeps first backup if re-run)
|
||||
BACKUP="$DIR/docker-compose.yml.bak-pre-portainer-migration-$(date +%Y-%m-%d)"
|
||||
if [ ! -f "$BACKUP" ]; then
|
||||
cp "$DIR/docker-compose.yml" "$BACKUP"
|
||||
echo "→ backup: $BACKUP"
|
||||
else
|
||||
echo "→ backup exists: $BACKUP (keeping original)"
|
||||
fi
|
||||
|
||||
# 2. Transform compose:
|
||||
# - strip env_file directives (defensive — Portainer string-mode can't materialize .env)
|
||||
# - absolutize ./ paths (Portainer working_dir = /data/compose/<id>/, not stack dir)
|
||||
COMPOSE=$(sed \
|
||||
-e '/^\s*env_file:/d' \
|
||||
-e '/^\s*-\s*\.env\s*$/d' \
|
||||
-e "s|\./|$DIR/|g" \
|
||||
"$DIR/docker-compose.yml")
|
||||
|
||||
# 3. Parse .env into Portainer API env array (no-op if absent — books VDS has no .env files)
|
||||
ENV_ARRAY="[]"
|
||||
if [ -f "$DIR/.env" ]; then
|
||||
ENV_ARRAY=$(grep -vE '^\s*(#|$)' "$DIR/.env" | jq -Rs '
|
||||
split("\n") | map(select(length>0) | split("=") | {name: .[0], value: (.[1:] | join("="))})')
|
||||
fi
|
||||
|
||||
PAYLOAD=$(jq -n --arg name "$STACK" --arg compose "$COMPOSE" --argjson env "$ENV_ARRAY" \
|
||||
'{name:$name, stackFileContent:$compose, env:$env, fromAppTemplate:false}')
|
||||
|
||||
# 4. Idempotency: delete existing Portainer stack with same name on same endpoint
|
||||
EXISTING_ID=$(curl -ksS -H "X-API-Key: $API_KEY" "$PORTAINER_URL/api/stacks" | \
|
||||
jq -r ".[] | select(.Name==\"$STACK\" and .EndpointId==$ENDPOINT_ID) | .Id" | head -1)
|
||||
if [ -n "$EXISTING_ID" ]; then
|
||||
echo "→ existing Portainer stack id=$EXISTING_ID — deleting"
|
||||
curl -ksS -X DELETE "$PORTAINER_URL/api/stacks/$EXISTING_ID?endpointId=$ENDPOINT_ID" \
|
||||
-H "X-API-Key: $API_KEY" >/dev/null
|
||||
fi
|
||||
|
||||
# 5. Down ad-hoc containers (direct `docker rm -f` by compose-project label —
|
||||
# bypasses docker-compose v1 ContainerConfig bug + any v2 recreate quirks)
|
||||
CONTAINERS=$(docker ps -aq --filter "label=com.docker.compose.project=$STACK" || true)
|
||||
if [ -n "$CONTAINERS" ]; then
|
||||
COUNT=$(echo "$CONTAINERS" | wc -l)
|
||||
echo "→ removing $COUNT ad-hoc container(s) (by compose-project label)"
|
||||
docker rm -f $CONTAINERS
|
||||
fi
|
||||
|
||||
# 6. Create via Portainer API
|
||||
echo "→ POST /api/stacks/create/standalone/string"
|
||||
RESP=$(curl -ksS -X POST "$PORTAINER_URL/api/stacks/create/standalone/string?endpointId=$ENDPOINT_ID" \
|
||||
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" --data "$PAYLOAD")
|
||||
NEW_ID=$(echo "$RESP" | jq -r '.Id // empty')
|
||||
|
||||
if [ -n "$NEW_ID" ]; then
|
||||
echo "✓ stack '$STACK' created, Portainer Id=$NEW_ID"
|
||||
echo
|
||||
echo "→ containers now running:"
|
||||
docker ps --filter "label=com.docker.compose.project=$STACK" \
|
||||
--format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
else
|
||||
echo "✗ FAILED:"
|
||||
echo "$RESP" | jq .
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,85 @@
|
||||
<#
|
||||
finish-elevated.ps1 - final decommission of windows-recovery-host.
|
||||
RUN IN AN ELEVATED PowerShell (Run as Administrator).
|
||||
Requires working `pass` (gpg-agent) for the VDS sa password.
|
||||
|
||||
Steps (with a safety gate):
|
||||
F1. Fix orphaned user `snolla` in DB `stostayer` on mssql.kzntsv.site.
|
||||
F2. Repoint stostayer.old web.config localhost -> mssql.kzntsv.site (+backup).
|
||||
F3. Restart stostayer.old pool, smoke :8091. If 5xx -> STOP, do NOT touch MSSQL.
|
||||
E. Remove IIS site snolla + C:\sites\snolla.
|
||||
M. Remove local MSSQL (docker) + volume + data dir (~30 GB).
|
||||
Idempotent - safe to re-run. ASCII-only on purpose (PS 5.1 codepage safety).
|
||||
#>
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) {
|
||||
Write-Error "Need an elevated shell (Run as Administrator)."; return
|
||||
}
|
||||
Import-Module WebAdministration
|
||||
|
||||
# ---------- F1: orphan fix on VDS ----------
|
||||
# ALREADY DONE 2026-06-08 (agent fixed snolla@stostayer orphan via sa; verified mapped).
|
||||
# This script only needs the elevated IIS/web.config/docker steps. No password needed.
|
||||
|
||||
# pre-gate: verify snolla can open VDS stostayer before we touch anything
|
||||
Write-Host "`n=== pre-check: snolla -> VDS stostayer ===" -ForegroundColor Cyan
|
||||
$wcPath = "C:\sites\stostayer.old\web.config"
|
||||
$src = ([xml](Get-Content $wcPath)).configuration.connectionStrings.add.connectionString
|
||||
$verify = ($src -replace '(?i)Data Source=localhost(,1433)?','Data Source=mssql.kzntsv.site,1433')
|
||||
if ($verify -notmatch 'TrustServerCertificate') { $verify = $verify.TrimEnd(';') + ';TrustServerCertificate=True' }
|
||||
$cn = New-Object System.Data.SqlClient.SqlConnection $verify; $cn.Open()
|
||||
$c = $cn.CreateCommand(); $c.CommandText = "SELECT DB_NAME()"; $db = $c.ExecuteScalar(); $cn.Close()
|
||||
Write-Host " verify snolla->VDS stostayer OK (db=$db)" -ForegroundColor Green
|
||||
|
||||
# ---------- F2: repoint web.config (idempotent) ----------
|
||||
Write-Host "`n=== F2. repoint stostayer.old web.config ===" -ForegroundColor Cyan
|
||||
if ($src -match '(?i)Data Source=localhost') {
|
||||
Copy-Item $wcPath "$wcPath.bak-pre-vds-$(Get-Date -Format yyyyMMdd)" -Force
|
||||
$xml = [xml](Get-Content $wcPath)
|
||||
$node = $xml.configuration.connectionStrings.add | Where-Object { $_.name -eq 'MoreThenCmsEntities' }
|
||||
$new = ($node.connectionString -replace '(?i)Data Source=localhost(,1433)?','Data Source=mssql.kzntsv.site,1433')
|
||||
if ($new -notmatch 'TrustServerCertificate') { $new = $new.TrimEnd(';') + ';TrustServerCertificate=True' }
|
||||
$node.connectionString = $new
|
||||
$xml.Save($wcPath)
|
||||
Write-Host " web.config -> mssql.kzntsv.site (backup alongside)" -ForegroundColor Green
|
||||
} else { Write-Host " already pointed at VDS - skip (original backup preserved)" -ForegroundColor Green }
|
||||
|
||||
# ---------- F3: restart pool + smoke (proxy-bypassed) ----------
|
||||
Write-Host "`n=== F3. restart pool stostayer.old + smoke :8091 ===" -ForegroundColor Cyan
|
||||
Restart-WebAppPool -Name 'stostayer.old'
|
||||
Start-Sleep -Seconds 5
|
||||
# system VPN proxy swallows localhost (wiki bug #7) -> curl.exe --noproxy bypasses it
|
||||
$code = (& curl.exe -s -o NUL -w "%{http_code}" --noproxy "*" --max-time 30 http://localhost:8091/)
|
||||
Write-Host " :8091 -> HTTP $code"
|
||||
if ((-not ($code -match '^\d+$')) -or ([int]$code -eq 0) -or ([int]$code -ge 500)) {
|
||||
Write-Error "stostayer.old returned '$code' - repoint FAILED. Leaving MSSQL untouched. Rollback: restore web.config.bak-pre-vds-*."
|
||||
return
|
||||
}
|
||||
Write-Host " stostayer.old is alive on the VDS DB" -ForegroundColor Green
|
||||
|
||||
# ---------- E: snolla source IIS teardown ----------
|
||||
Write-Host "`n=== E. remove IIS site snolla + content ===" -ForegroundColor Cyan
|
||||
if (Get-Website -Name snolla -ErrorAction SilentlyContinue) {
|
||||
Stop-Website -Name snolla -ErrorAction SilentlyContinue
|
||||
Remove-Website -Name snolla
|
||||
Write-Host " IIS site snolla removed"
|
||||
}
|
||||
if (Get-WebAppPoolState -Name snolla -ErrorAction SilentlyContinue) { Remove-WebAppPool -Name snolla; Write-Host " app pool snolla removed" }
|
||||
# (local traefik already removed 2026-06-08 - no CMS routes to clean)
|
||||
if (Test-Path "C:\sites\snolla") { Remove-Item "C:\sites\snolla" -Recurse -Force; Write-Host " C:\sites\snolla removed (8.66 GB)" -ForegroundColor Green }
|
||||
|
||||
# ---------- M: local MSSQL teardown ----------
|
||||
Write-Host "`n=== M. remove local MSSQL ===" -ForegroundColor Cyan
|
||||
$ErrorActionPreference = 'Continue' # native docker stderr must not abort here
|
||||
docker stop mssql 2>$null | Out-Null
|
||||
docker rm mssql 2>$null | Out-Null
|
||||
docker volume rm mssql_mssql_data 2>$null | Out-Null
|
||||
$mssqlDir = "C:\Users\vitya\projects\docker\diskstation\mssql"
|
||||
if (Test-Path $mssqlDir) { Remove-Item $mssqlDir -Recurse -Force; Write-Host " diskstation/mssql removed (~28 GB)" -ForegroundColor Green }
|
||||
|
||||
Write-Host "`n=== DONE. Free C: ===" -ForegroundColor Cyan
|
||||
Get-PSDrive C | ForEach-Object { "Free={0:N0}GB" -f ($_.Free/1GB) }
|
||||
Write-Host "`nSeparately (when convenient) - compact the docker WSL vhdx to return ~176 GB to C::"
|
||||
Write-Host ' wsl --shutdown'
|
||||
Write-Host ' Get-ChildItem $env:LOCALAPPDATA\Docker\wsl -Recurse -Filter *.vhdx # find the path'
|
||||
Write-Host ' Optimize-VHD -Path <docker_data.vhdx> -Mode Full # needs Hyper-V module; else diskpart compact vdisk'
|
||||
159
scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1
Normal file
159
scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1
Normal file
@@ -0,0 +1,159 @@
|
||||
# 03-ruvds-winacme.ps1 — win-acme setup + HTTP-01 challenge-path probe on RUVDS IIS host.
|
||||
# Idempotent. Run elevated on RUVDS (80.64.31.36) over SSH.
|
||||
# Phase switch: -Phase download|probe|cleanprobe
|
||||
param([string]$Phase = 'probe')
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$dest = 'C:\win-acme'
|
||||
$root = 'C:\sites\snolla'
|
||||
$chDir = Join-Path $root '.well-known\acme-challenge'
|
||||
|
||||
function Download-WinAcme {
|
||||
if (Test-Path (Join-Path $dest 'wacs.exe')) { Write-Output "wacs already present: $dest\wacs.exe"; return }
|
||||
$api = 'https://api.github.com/repos/win-acme/win-acme/releases/latest'
|
||||
$rel = Invoke-RestMethod -Uri $api -Headers @{ 'User-Agent' = 'ruvds-setup' }
|
||||
$asset = $rel.assets | Where-Object { $_.name -match 'x64\.pluggable\.zip$' } | Select-Object -First 1
|
||||
if (-not $asset) { throw "no x64.pluggable.zip asset in $($rel.tag_name)" }
|
||||
$zip = Join-Path $env:TEMP $asset.name
|
||||
Write-Output "downloading $($rel.tag_name): $($asset.name)"
|
||||
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zip -Headers @{ 'User-Agent' = 'ruvds-setup' }
|
||||
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||||
Expand-Archive -Path $zip -DestinationPath $dest -Force
|
||||
Remove-Item $zip -Force
|
||||
Write-Output ("installed: " + (Get-Item (Join-Path $dest 'wacs.exe')).VersionInfo.ProductVersion)
|
||||
}
|
||||
|
||||
function Test-ChallengePath {
|
||||
New-Item -ItemType Directory -Force -Path $chDir | Out-Null
|
||||
$token = 'acme-probe-' + (Get-Random)
|
||||
$tokenFile = Join-Path $chDir $token
|
||||
Set-Content -Path $tokenFile -Value $token -NoNewline -Encoding ascii
|
||||
Write-Output "token file: $tokenFile (content='$token')"
|
||||
|
||||
$hosts = 'snolla.com','tandemmebel.ru','maljarka.tandemmebel.ru','rimiz.ru'
|
||||
Write-Output "--- WITHOUT challenge web.config (baseline CMS behaviour) ---"
|
||||
Probe-Hosts $hosts $token
|
||||
|
||||
# Mirror the proven /admin escape: remove the catch-all Owin handler so OWIN stops
|
||||
# intercepting this path, add extensionless-token mime, drop managed-module overhead.
|
||||
$webcfg = @'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<remove name="Owin" />
|
||||
</handlers>
|
||||
<staticContent>
|
||||
<remove fileExtension="." />
|
||||
<mimeMap fileExtension="." mimeType="text/plain" />
|
||||
</staticContent>
|
||||
<modules runAllManagedModulesForAllRequests="false" />
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
'@
|
||||
Set-Content -Path (Join-Path $chDir 'web.config') -Value $webcfg -Encoding utf8
|
||||
Write-Output "--- WITH challenge web.config (win-acme style) ---"
|
||||
Probe-Hosts $hosts $token
|
||||
}
|
||||
|
||||
function Probe-Hosts($hosts, $token) {
|
||||
foreach ($h in $hosts) {
|
||||
try {
|
||||
$r = Invoke-WebRequest "http://localhost/.well-known/acme-challenge/$token" -Headers @{ Host = $h } -UseBasicParsing -TimeoutSec 15 -MaximumRedirection 0
|
||||
$ok = ($r.Content.Trim() -eq $token)
|
||||
Write-Output (" [{0}] HTTP {1} match={2} body='{3}'" -f $h, $r.StatusCode, $ok, ($r.Content -replace '\s+',' ').Substring(0,[Math]::Min(40,$r.Content.Length)))
|
||||
} catch {
|
||||
$resp = $_.Exception.Response
|
||||
if ($resp) { Write-Output (" [{0}] HTTP {1} loc={2}" -f $h, [int]$resp.StatusCode, $resp.Headers['Location']) }
|
||||
else { Write-Output (" [{0}] ERR: {1}" -f $h, $_.Exception.Message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Setup-ChallengeApp {
|
||||
Import-Module WebAdministration
|
||||
$pool = 'acme-challenge'
|
||||
if (-not (Test-Path "IIS:\AppPools\$pool")) { New-WebAppPool -Name $pool | Out-Null; Write-Output "created app pool $pool" }
|
||||
Set-ItemProperty "IIS:\AppPools\$pool" -Name managedRuntimeVersion -Value '' # No Managed Code
|
||||
Set-ItemProperty "IIS:\AppPools\$pool" -Name autoStart -Value $true
|
||||
New-Item -ItemType Directory -Force -Path $chDir | Out-Null
|
||||
# No-Managed-Code pool + drop the inherited catch-all Owin handler (else it 500s in an
|
||||
# unmanaged pool), serve extensionless ACME tokens as text/plain. Verified combo.
|
||||
$appcfg = @'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<remove name="Owin" />
|
||||
</handlers>
|
||||
<staticContent>
|
||||
<remove fileExtension="." />
|
||||
<mimeMap fileExtension="." mimeType="text/plain" />
|
||||
</staticContent>
|
||||
<modules runAllManagedModulesForAllRequests="false" />
|
||||
<directoryBrowse enabled="false" />
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
'@
|
||||
Set-Content -Path (Join-Path $chDir 'web.config') -Value $appcfg -Encoding utf8
|
||||
$vpath = '/.well-known/acme-challenge'
|
||||
$app = Get-WebApplication -Site 'snolla' | Where-Object { $_.path -eq $vpath }
|
||||
if (-not $app) {
|
||||
New-WebApplication -Site 'snolla' -Name '.well-known/acme-challenge' -PhysicalPath $chDir -ApplicationPool $pool -Force | Out-Null
|
||||
Write-Output "created IIS application $vpath (pool=$pool, No Managed Code)"
|
||||
} else {
|
||||
Set-ItemProperty "IIS:\Sites\snolla$vpath" -Name applicationPool -Value $pool
|
||||
Write-Output "IIS application $vpath exists (pool ensured=$pool)"
|
||||
}
|
||||
}
|
||||
|
||||
function Probe-Once {
|
||||
New-Item -ItemType Directory -Force -Path $chDir | Out-Null
|
||||
$token = 'acme-probe-' + (Get-Random)
|
||||
Set-Content -Path (Join-Path $chDir $token) -Value $token -NoNewline -Encoding ascii
|
||||
Write-Output "probe token: $token"
|
||||
Probe-Hosts ('snolla.com','tandemmebel.ru','www.tandemmebel.ru','maljarka.tandemmebel.ru','rimiz.ru','kupimknigi.spb.ru') $token
|
||||
}
|
||||
|
||||
function Clean-Probe {
|
||||
if (Test-Path $chDir) { Get-ChildItem $chDir -Filter 'acme-probe-*' | Remove-Item -Force -EA SilentlyContinue; Write-Output "removed probe tokens" }
|
||||
}
|
||||
|
||||
function Setup-RenewTask {
|
||||
$wacs = 'C:\win-acme\wacs.exe'
|
||||
$taskName = 'win-acme-renew-snolla'
|
||||
$action = New-ScheduledTaskAction -Execute $wacs -Argument '--renew --baseuri https://acme-v02.api.letsencrypt.org/'
|
||||
$trigger = New-ScheduledTaskTrigger -Daily -At 9am
|
||||
try { $trigger.RandomDelay = 'PT4H' } catch {}
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'Auto-renew Let''s Encrypt cert for IIS site snolla (25 SNI bindings) via HTTP-01.' -Force | Out-Null
|
||||
$t = Get-ScheduledTask -TaskName $taskName
|
||||
$info = $t | Get-ScheduledTaskInfo
|
||||
Write-Output ("task '{0}' state={1} nextRun={2}" -f $taskName, $t.State, $info.NextRunTime)
|
||||
|
||||
# remove the staging renewal so a stray --renew against staging does nothing
|
||||
$stagingRenewals = 'C:\ProgramData\win-acme\acme-staging-v02.api.letsencrypt.org\Renewals'
|
||||
if (Test-Path $stagingRenewals) { Remove-Item "$stagingRenewals\*" -Recurse -Force -EA SilentlyContinue; Write-Output "cleared staging renewals" }
|
||||
if (Test-Path 'C:\win-acme\stagingtest') { Remove-Item 'C:\win-acme\stagingtest' -Recurse -Force -EA SilentlyContinue; Write-Output "removed stagingtest pemfiles" }
|
||||
}
|
||||
|
||||
function Verify-Certs {
|
||||
Import-Module WebAdministration
|
||||
# show the cert now bound (thumbprint -> subject/expiry)
|
||||
$b = Get-WebBinding -Port 443 | Select-Object -First 1
|
||||
$hash = (Get-Item "IIS:\SslBindings\*!443!*" -EA SilentlyContinue | Select-Object -First 1).Thumbprint
|
||||
Get-ChildItem Cert:\LocalMachine\WebHosting | Sort-Object NotAfter -Descending | Select-Object -First 3 |
|
||||
ForEach-Object { Write-Output ("WebHosting cert: {0} | NotAfter={1} | Issuer={2} | SAN-count via DnsNameList={3}" -f $_.Thumbprint, $_.NotAfter, $_.Issuer, $_.DnsNameList.Count) }
|
||||
}
|
||||
|
||||
switch ($Phase) {
|
||||
'download' { Download-WinAcme }
|
||||
'probe' { Download-WinAcme; Test-ChallengePath }
|
||||
'app' { Download-WinAcme; Setup-ChallengeApp; Probe-Once }
|
||||
'task' { Setup-RenewTask; Verify-Certs }
|
||||
'verify' { Verify-Certs }
|
||||
'cleanprobe' { Clean-Probe }
|
||||
default { throw "unknown phase $Phase" }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Перенос labtools.ru на новый хостинг — инструкция
|
||||
|
||||
> Текущий A: `94.19.247.14` → целевой: `80.64.31.36` (RUVDS IIS, [[../../.wiki/entities/ruvds-iis-host.md]]). DNS-хостинг: Яндекс 360.
|
||||
|
||||
Сайт переезжает с домашнего сервера на облачный VDS. Нужно поменять две A-записи в DNS Яндекс 360.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Открой **https://admin.yandex.ru/domains** (админка Яндекс 360 для бизнеса).
|
||||
2. В списке доменов выбери `labtools.ru`.
|
||||
3. Нажми **Управлять DNS-записями** — откроется DNS-редактор с таблицей записей.
|
||||
4. Найди две записи типа **A**:
|
||||
|
||||
| Хост | Тип | Значение |
|
||||
|---|---|---|
|
||||
| `@` | A | `94.19.247.14` |
|
||||
| `www` | A | `94.19.247.14` |
|
||||
|
||||
5. У обеих **открой на редактирование** и поменяй **поле «Адрес IPv4»** на:
|
||||
|
||||
```
|
||||
80.64.31.36
|
||||
```
|
||||
|
||||
Сохрани. TTL не трогай.
|
||||
|
||||
⚠ **Важно:** редактируй существующие записи, **не создавай новые** — иначе будут дубли.
|
||||
|
||||
В итоге в таблице должно быть:
|
||||
|
||||
| Хост | Тип | Значение |
|
||||
|---|---|---|
|
||||
| `@` | A | `80.64.31.36` |
|
||||
| `www` | A | `80.64.31.36` |
|
||||
|
||||
## Проверка
|
||||
|
||||
Через 10–15 минут открой https://labtools.ru и https://www.labtools.ru в инкогнито-вкладке. У части пользователей сайт может ещё резолвиться на старый адрес — это нормально, продлится до нескольких часов (пока DNS-кеш у их провайдеров не обновится).
|
||||
|
||||
Если на новом адресе что-то не открывается — напиши, **не откатывай** старое значение. Разберусь со стороны сервера.
|
||||
|
||||
## Что НЕ трогать
|
||||
|
||||
- **NS-записи** (`dns1.yandex.net`, `dns2.yandex.net`).
|
||||
- **MX, TXT (SPF/DKIM/DMARC), CNAME, любые другие записи.**
|
||||
- Только две A-записи. Больше ничего.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Перенос tandemmebel.ru на новый хостинг — инструкция
|
||||
|
||||
> Текущий A: `94.19.247.14` → целевой: `80.64.31.36` (RUVDS IIS, [[../../.wiki/entities/ruvds-iis-host.md]]). DNS-хостинг: reg.ru. Зона построена через apex+CNAME — поэтому меняется одна A-запись на корне, все subdomain'ы (`www`, `maljarka`, и пр.) переедут автоматически.
|
||||
|
||||
Сайт переезжает с домашнего сервера на облачный VDS. Нужно поменять одну A-запись в зоне `tandemmebel.ru` в личном кабинете reg.ru.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Зайди в личный кабинет reg.ru: **https://www.reg.ru/user/account/#/**
|
||||
2. Раздел **Домены** → кликни по строке `tandemmebel.ru`.
|
||||
3. В блоке **«DNS-серверы и управление зоной»** нажми **Изменить** — откроется редактор зоны со списком записей.
|
||||
4. Найди запись типа **A**:
|
||||
|
||||
| Поддомен | Тип | Значение |
|
||||
|---|---|---|
|
||||
| `@` (или пусто / `tandemmebel.ru`) | A | `94.19.247.14` |
|
||||
|
||||
5. Нажми **иконку карандаша** рядом с этой записью и поменяй **поле «IP-адрес»** на:
|
||||
|
||||
```
|
||||
80.64.31.36
|
||||
```
|
||||
|
||||
Сохрани. TTL не трогай.
|
||||
|
||||
⚠ **Важно:** редактируй существующую запись (карандаш), **не добавляй новую** — иначе будут дубли и зона начнёт отдавать оба адреса в round-robin.
|
||||
|
||||
В итоге в зоне должно быть:
|
||||
|
||||
| Поддомен | Тип | Значение |
|
||||
|---|---|---|
|
||||
| `@` | A | `80.64.31.36` |
|
||||
|
||||
CNAME-записи на `www`, `maljarka` и прочие подобные — **не трогай**, они автоматически последуют за новым адресом apex'а.
|
||||
|
||||
## Проверка
|
||||
|
||||
Через 15–60 минут открой https://tandemmebel.ru и https://www.tandemmebel.ru в инкогнито-вкладке. У части пользователей сайт может ещё резолвиться на старый адрес — это нормально, продлится до нескольких часов (пока DNS-кеш у их провайдеров не обновится).
|
||||
|
||||
Если на новом адресе что-то не открывается — напиши, **не откатывай** старое значение. Разберусь со стороны сервера.
|
||||
|
||||
## Что НЕ трогать
|
||||
|
||||
- **NS-записи** (`ns1.reg.ru`, `ns2.reg.ru`).
|
||||
- **MX, TXT (SPF/DKIM/DMARC), CNAME, любые другие записи.**
|
||||
- Только одну A-запись на корне зоны. Больше ничего.
|
||||
23
scripts/iis-migration-to-ruvds/winacme-Web_Config.xml
Normal file
23
scripts/iis-migration-to-ruvds/winacme-Web_Config.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
win-acme HTTP-01 challenge web.config template (overwrites C:\win-acme\Web_Config.xml on RUVDS).
|
||||
Patched for the MoreThenCms / OWIN catch-all: the .well-known/acme-challenge folder is a
|
||||
separate IIS application in a "No Managed Code" app pool (acme-challenge). The inherited
|
||||
catch-all Owin *managed* handler 500s in an unmanaged pool, so we <remove> it; tokens are
|
||||
served by the native StaticFileModule with an extensionless mimeMap. See task decisions log.
|
||||
-->
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<httpRedirect enabled="false" />
|
||||
<validation validateIntegratedModeConfiguration="false" />
|
||||
<handlers>
|
||||
<remove name="Owin" />
|
||||
</handlers>
|
||||
<modules runAllManagedModulesForAllRequests="false" />
|
||||
<staticContent>
|
||||
<clear />
|
||||
<mimeMap fileExtension="." mimeType="text/json" />
|
||||
<mimeMap fileExtension=".*" mimeType="text/json" />
|
||||
</staticContent>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
19
scripts/local-snolla-admin-restore/ruvds-verify.ps1
Normal file
19
scripts/local-snolla-admin-restore/ruvds-verify.ps1
Normal file
@@ -0,0 +1,19 @@
|
||||
$ErrorActionPreference='SilentlyContinue'
|
||||
Write-Output "=== IIS sites ==="
|
||||
Import-Module WebAdministration
|
||||
Get-Website | ForEach-Object {
|
||||
$b = (Get-WebBinding -Name $_.Name).bindingInformation -join ' ; '
|
||||
"{0} | {1} | {2} | {3}" -f $_.Name, $_.State, $_.PhysicalPath, $b
|
||||
}
|
||||
Write-Output "=== C:\sites\snolla ==="
|
||||
if (Test-Path 'C:\sites\snolla') {
|
||||
$f = Get-ChildItem 'C:\sites\snolla' -File
|
||||
"exists; top-files=" + $f.Count
|
||||
$wc = 'C:\sites\snolla\Web.config'
|
||||
if (Test-Path $wc) {
|
||||
"Web.config size=" + (Get-Item $wc).Length
|
||||
Write-Output "=== Web.config storageType / minio lines ==="
|
||||
Select-String -Path $wc -Pattern 'storageType|minio\.kzntsv|FileStorage\.S3|Initial Catalog|Data Source' | ForEach-Object { $_.Line.Trim() }
|
||||
} else { "Web.config MISSING" }
|
||||
} else { "C:\sites\snolla MISSING" }
|
||||
Write-Output "=== DONE ==="
|
||||
@@ -0,0 +1,75 @@
|
||||
# setup-local-snolla-admin.ps1 - restore local catch-all IIS site 'snolla' (admin for tirazh + on.snolla.com)
|
||||
# RUN FROM elevated PowerShell: Set-ExecutionPolicy -Scope Process Bypass; & 'C:\Users\vitya\projects\.admin\scripts\local-snolla-admin-restore\setup-local-snolla-admin.ps1'
|
||||
# Source: C:\sites\snolla\ already copied from RUVDS (selective ~100MB, Web.config -> mssql.kzntsv.site + MinIO S3).
|
||||
|
||||
$logPath = 'C:\Users\vitya\projects\.admin\.tmp\setup-log.txt'
|
||||
Start-Transcript -Path $logPath -Force | Out-Null
|
||||
$ErrorActionPreference = 'Stop'
|
||||
try {
|
||||
Import-Module WebAdministration
|
||||
|
||||
$siteName = 'snolla'
|
||||
$poolName = 'snolla'
|
||||
$physPath = 'C:\sites\snolla'
|
||||
$aliases = @('tandemmebel.snolla.com','labtools.snolla.com','labtoolspro.snolla.com','emspb.snolla.com','kupimknigi.snolla.com','internal.snolla.com')
|
||||
|
||||
Write-Host "== 1. AppPool $poolName =="
|
||||
if (Test-Path "IIS:\AppPools\$poolName") {
|
||||
Write-Host " exists, ensuring config"
|
||||
} else {
|
||||
New-WebAppPool -Name $poolName | Out-Null
|
||||
}
|
||||
Set-ItemProperty "IIS:\AppPools\$poolName" -Name managedRuntimeVersion -Value 'v4.0'
|
||||
Set-ItemProperty "IIS:\AppPools\$poolName" -Name processModel.identityType -Value 'ApplicationPoolIdentity'
|
||||
Set-ItemProperty "IIS:\AppPools\$poolName" -Name recycling.periodicRestart.memory -Value 200
|
||||
|
||||
Write-Host "== 2. IIS site $siteName (catch-all *:80) =="
|
||||
if (Test-Path "IIS:\Sites\$siteName") {
|
||||
Write-Host " exists - removing to recreate cleanly"
|
||||
Remove-Website -Name $siteName
|
||||
}
|
||||
# catch-all HTTP on *:80 (no HostHeader = matches any Host header)
|
||||
New-Website -Name $siteName -PhysicalPath $physPath -ApplicationPool $poolName -Port 80 -Force | Out-Null
|
||||
Set-ItemProperty "IIS:\Sites\$siteName" -Name physicalPath -Value $physPath
|
||||
|
||||
Write-Host "== 3. ACL: IIS AppPool\$poolName (OI)(CI)(M) on $physPath =="
|
||||
icacls $physPath /grant "IIS AppPool\${poolName}:(OI)(CI)(M)" /T /C | Out-Null
|
||||
|
||||
Write-Host "== 4. hosts-override (127.0.0.1 aliases) =="
|
||||
$hostsFile = "$env:windir\System32\drivers\etc\hosts"
|
||||
$hostsContent = Get-Content $hostsFile -Raw -ErrorAction SilentlyContinue
|
||||
$marker = '# snolla-local-admin'
|
||||
if ($hostsContent -notmatch [regex]::Escape($marker)) {
|
||||
$line = "`r`n$marker`r`n127.0.0.1 $($aliases -join ' ')`r`n"
|
||||
Add-Content -Path $hostsFile -Value $line -Encoding ASCII
|
||||
Write-Host " added hosts entries"
|
||||
} else {
|
||||
Write-Host " hosts entries already present (marker found)"
|
||||
}
|
||||
|
||||
Write-Host "== 5. FW: block inbound TCP 80 (local-only guard; loopback not filtered) =="
|
||||
$fwName = 'block-inbound-80-snolla-local'
|
||||
if (-not (Get-NetFirewallRule -DisplayName $fwName -ErrorAction SilentlyContinue)) {
|
||||
New-NetFirewallRule -DisplayName $fwName -Direction Inbound -Protocol TCP -LocalPort 80 -Action Block -Profile Any | Out-Null
|
||||
Write-Host " block rule added"
|
||||
} else { Write-Host " block rule already present" }
|
||||
|
||||
Write-Host "== 6. Start =="
|
||||
Start-WebAppPool $poolName
|
||||
Start-Website $siteName
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host " pool state: $((Get-WebAppPoolState $poolName).Text)"
|
||||
Write-Host " site state: $((Get-WebsiteState $siteName).Text)"
|
||||
|
||||
Write-Host "== 7. Smoke (curl --noproxy bypasses VPN-proxy localhost swallow) =="
|
||||
foreach ($a in $aliases) {
|
||||
$code = & curl.exe --noproxy '*' -s -o NUL -w "%{http_code}" -H "Host: $a" "http://127.0.0.1/admin/account/login" 2>$null
|
||||
Write-Host " http://$a/admin/account/login -> $code"
|
||||
}
|
||||
Write-Host "== DONE - open http://tandemmebel.snolla.com/admin in browser =="
|
||||
Write-Host "(if login form not 200 - check C:\Logs\snolla\log*.txt and eventvwr IIS)"
|
||||
} catch {
|
||||
Write-Host "FATAL: $_"
|
||||
Write-Host $_.ScriptStackTrace
|
||||
}
|
||||
Stop-Transcript | Out-Null
|
||||
@@ -100,6 +100,21 @@ docker cp redis:/data/dump.rdb "$DUMP_DIR/redis.rdb"
|
||||
gzip -f "$DUMP_DIR/redis.rdb"
|
||||
echo "redis: $(stat -c %s "$DUMP_DIR/redis.rdb.gz") bytes"
|
||||
|
||||
# MSSQL: backup 5 prod DBs via sqlcmd into bind-mounted /var/opt/mssql/backups/
|
||||
# WITH FORMAT (not INIT): Express can't write to a media set previously formatted
|
||||
# WITH COMPRESSION (e.g. migration-era .bak); FORMAT recreates the media set fresh.
|
||||
source /opt/stacks/databases/mssql/.env # injects MSSQL_SA_PASSWORD
|
||||
MSSQL_BACKUP_DIR=/opt/stacks/databases/mssql/backups
|
||||
for DB in MoreThenCms StayerCalculator StayerPrice TireService stostayer; do
|
||||
docker exec mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -b \
|
||||
-Q "BACKUP DATABASE [$DB] TO DISK='/var/opt/mssql/backups/${DB}.bak' WITH COPY_ONLY, FORMAT" \
|
||||
> /dev/null
|
||||
mv "$MSSQL_BACKUP_DIR/${DB}.bak" "$DUMP_DIR/mssql-${DB}.bak"
|
||||
echo "mssql $DB: $(stat -c %s "$DUMP_DIR/mssql-${DB}.bak") bytes"
|
||||
done
|
||||
unset MSSQL_SA_PASSWORD
|
||||
|
||||
# === Step 2: rsync to kreknin ===
|
||||
echo "--- rsync to $DEST_USER@$DEST_HOST:$DEST_PATH ---"
|
||||
ssh $SSH_OPTS "$DEST_USER@$DEST_HOST" "mkdir -p '$DEST_PATH'"
|
||||
|
||||
Reference in New Issue
Block a user