Compare commits
51 Commits
d558ccfaed
...
14ee2ef85a
| Author | SHA1 | Date | |
|---|---|---|---|
| 14ee2ef85a | |||
| 38f1d3358a | |||
| c55cb11948 | |||
| 3cdcdad048 | |||
| 85b3ee847f | |||
| c40418239e | |||
| 4837fb32c8 | |||
| 98bcc37d32 | |||
| d25c0577c8 | |||
| 6d8d75474d | |||
| cbdc2b39aa | |||
| c727aaa1b6 | |||
| 53847a7618 | |||
| 24661c10fb | |||
| 115c6549f5 | |||
| a5e96432bc | |||
| 349c4a6d53 | |||
| cf840c04fb | |||
| 61826cb1ef | |||
| aedfddb348 | |||
| 059e8ed9ff | |||
| 13c23b9bc5 | |||
| d0defa8e8e | |||
| dd7c56742e | |||
| f9bdb97be3 | |||
| ec9177dddb | |||
| 6f846d0495 | |||
| 5d7a14c7d4 | |||
| 5f8b2dfaf7 | |||
| 71c790063a | |||
| 75b828754c | |||
| b3f9526bb2 | |||
| 648159d84f | |||
| 3d5d61d5a5 | |||
| 2e2290a709 | |||
| acb1c8e126 | |||
| f3ded74674 | |||
| 5d8d017766 | |||
| 2d307f2dd0 | |||
| 3d6a3e8c15 | |||
| 117376656d | |||
| 5000ebe0a3 | |||
| ae56813c8d | |||
| 1f4a581ea0 | |||
| c86750b75d | |||
| a74d971c2b | |||
| 19422352ba | |||
| d0f4d1f4ab | |||
| 8ea0268df2 | |||
| a130bd61d3 | |||
| b56ee0ec11 |
57
.tasks/NEXT-SESSION-PROMPT.md
Normal file
57
.tasks/NEXT-SESSION-PROMPT.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Prompt для следующей сессии
|
||||
|
||||
Скопируй этот блок и вставь как первое сообщение:
|
||||
|
||||
---
|
||||
|
||||
Привет! Возвращаемся к **iis-on-host-migration**. **Перед началом обязательно прочти**:
|
||||
1. `.wiki/concepts/iis-migration-2026-05-19-postmortem.md` — почему предыдущая попытка сломала prod
|
||||
2. `.wiki/sources/iis-host-migration-2026-05-19.md` (Phase 9 в конце — про rollback)
|
||||
3. `.wiki/concepts/recovery-architecture-snapshot.md` (актуальный VM-chain)
|
||||
|
||||
**Кратко где мы сейчас (после rollback):**
|
||||
- Prod снова через VM `snolla-recovery` (как было до session 2026-05-19). 11 main routes + 2 stayer routes в traefik → `host.docker.internal:18080/18180/18181` → VM IIS.
|
||||
- На хосте есть **inert** артефакты предыдущей попытки миграции — НЕ удалять без явного решения:
|
||||
- `C:\sites\{snolla, stostayer, stostayer.old, snolla-identity-manager}` — Web.config'и уже patched (sitePath, conn-strings).
|
||||
- IIS sites/pools `snolla, stostayer, stostayer.old` готовы (.NET v4.0 Integrated, ApplicationPoolIdentity, ACL).
|
||||
- Traefik backup-stamps `*.bak-phase3-2026-05-19`, `*.bak-stayer-switch-2026-05-19`, `*.bak-hostip-2026-05-19`.
|
||||
|
||||
**Цель повторной попытки** — выполнить миграцию по recipe из post-mortem, без повторения 10 ошибок:
|
||||
|
||||
1. **Backend port — НЕ :80.** Использовать `:18080` на хосте (старый VM NAT port forward, теперь свободен после `unregistervm` или после остановки VM). Или другой свободный (НЕ совпадающий с traefik publish-ports `:8000, :4443, :8080`). Это избегает Docker NAT loop.
|
||||
2. **Smoke test с `-MaximumRedirection 0` / `--max-redirs 0`** + read first response. Если `Location` == request URL или близко → loop, fail fast.
|
||||
3. **Тест из НЕ-LAN сети** (телефон через мобильный интернет, VPS curl) до commit'a — не доверять local smoke.
|
||||
4. **Параллельный run VM x 24h+** — НЕ savestate'ить VM на следующий же день. Держать как hot fallback хотя бы сутки реального трафика.
|
||||
5. **Atomic revert plan ДО старта** — backup всех traefik yml, написать "если что — paste this" revert-команду заранее.
|
||||
|
||||
**Не делай без моего "да":**
|
||||
- Любые traefik patches (меняет prod-трафик).
|
||||
- Любые destructive операции на `C:\sites\`, `C:\nas-recovery\vm-sites\`, snolla.ova.
|
||||
- Глушить VM до подтверждённой 24h+ стабильности host'а.
|
||||
- Push в git (gitea пока на мёртвой синке, и autopush не разрешён в любом случае).
|
||||
|
||||
**Контекстные пароли** (в Web.config / .env, не в чате):
|
||||
- MSSQL SA: `C:\Users\vitya\projects\docker\diskstation\mssql\.env`
|
||||
- snolla CMS conn: user `snolla`, password в `C:\sites\snolla\Web.config` (`fXkH4@8O%3pc`)
|
||||
- stostayer (новый): user `stayer_site`, password в `C:\sites\stostayer\Web.config` (`^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF`, XML-escape!)
|
||||
- VM SSH: `~/.ssh/id_ed25519_snolla_vm` → `vitya@127.0.0.1:8022` (NAT-forward)
|
||||
- VM admin password: `Pryakhin9`
|
||||
|
||||
Поехали — но **сначала прочти post-mortem полностью**.
|
||||
|
||||
---
|
||||
|
||||
## Что почитать AI-агенту перед началом (для самопроверки контекста)
|
||||
|
||||
- `.wiki/overview.md` — точки входа
|
||||
- **`.wiki/concepts/iis-migration-2026-05-19-postmortem.md`** ← critical (10 ошибок + recipe)
|
||||
- `.wiki/sources/iis-host-migration-2026-05-19.md` (включая Phase 9 в конце)
|
||||
- `.wiki/sources/nas-recovery-session-2026-05-18.md` — почему вообще этот хост
|
||||
- `.wiki/concepts/recovery-architecture-snapshot.md` — актуальный VM-chain
|
||||
- `.wiki/entities/snolla-recovery-vm.md` — VM active prod
|
||||
- `.wiki/entities/windows-recovery-host.md` — host inert artifacts
|
||||
- `.wiki/concepts/cms-config-rewrite-pattern.md` — UTF-8 BOM
|
||||
- `.wiki/concepts/webconfig-password-xml-escape.md` — `&` → `&` в conn-string
|
||||
- `.tasks/STATUS.md`
|
||||
- `.tasks/iis-on-host-migration.md` — Phase 1-9 история
|
||||
- Это сообщение
|
||||
101
.tasks/STATUS.md
101
.tasks/STATUS.md
@@ -37,11 +37,12 @@ Do **not** invent migration recipes, file lists, taxonomies, or scope decisions
|
||||
|
||||
---
|
||||
|
||||
## ⚪ [morecms-subtree-split] — Шаг 2 миграции: в `~/projects/MoreThenCms` создать 4 subtree-split ветки, каждая хранит per-file history своего dir'а. Эти ветки потом импортируются в admin (см. `admin-subtree-import-and-cleanup`).
|
||||
## 🟢 [morecms-subtree-split] — Шаг 2 миграции: в `~/projects/MoreThenCms` создать 4 subtree-split ветки, каждая хранит per-file history своего dir'а. Эти ветки потом импортируются в admin (см. `admin-subtree-import-and-cleanup`).
|
||||
|
||||
См. `.wiki/concepts/admin-infra-project.md` §Migration mechanics шаг 2 для полного контекста.
|
||||
|
||||
**Status:** ready
|
||||
**Status:** done
|
||||
**Closed:** 2026-05-21 — 4 split branches созданы в `~/projects/MoreThenCms` (split-wiki-entities @24661c10, split-wiki-sources @a5e96432, split-wiki-concepts @c727aaa1, split-tasks @cbdc2b39). Acceptance: per-file history preserved (verified via subsequent subtree-add in admin). Не push'ились — служебные.
|
||||
**Where I stopped:** (not started)
|
||||
**Next action:** ```powershell
|
||||
cd ~/projects/MoreThenCms
|
||||
@@ -59,11 +60,12 @@ Done — пометить 🟢 + переходить к `admin-subtree-import-a
|
||||
|
||||
---
|
||||
|
||||
## 🔵 [admin-subtree-import-and-cleanup] — Шаги 3-7 миграции: импорт 4 split-веток из MoreThenCms в admin (clean prefixes через `git subtree add`, populated prefixes через `git read-tree --prefix` merge), cleanup CMS-следов из mixed splits, STATUS.md merge, index.md regen, push на Gitea.
|
||||
## 🟢 [admin-subtree-import-and-cleanup] — Шаги 3-7 миграции: импорт 4 split-веток из MoreThenCms в admin (clean prefixes через `git subtree add`, populated prefixes — temp-prefix dance с `git subtree add` + `git mv` для history-preservation через merge-commit + rename; read-tree вариант spec'a отвергнут — не сохраняет parent-link к morecms history → `git log --follow` не доходит до morecms-era коммитов), cleanup CMS-следов из mixed splits, STATUS.md merge, index.md regen, push на Gitea.
|
||||
|
||||
Объединено в одну таску потому что: тот же agent, та же session, sequential steps без natural review-point между ними. См. `.wiki/concepts/admin-infra-project.md` §Migration mechanics шаги 3-7.
|
||||
|
||||
**Status:** blocked
|
||||
**Status:** done
|
||||
**Closed:** 2026-05-21 — все 5 шагов отработаны (3a clean prefixes subtree-add для 6 entities + 3 sources; 3b mixed temp-prefix dance для 18 concepts + 8 tasks files с git mv → renames preserve history через merge-commit; 4 cleanup 10 CMS files + 1 stale bootstrap-manifest.md; 5 STATUS.md merge — 7 admin done blocks imported, 4 CMS excluded; 6 index.md regen — 6 entities, 18 concepts, 3 sources; 7 push). **Finding для verify-migration:** `git log --follow .wiki/concepts/<slug>.md` показывает только rename-merge commit, не bridges в morecms-era history (git --follow limitation, не reliably crosses subtree-merge graft даже при rename-detection). Alternative для full history: `git log --all -- <bare-filename>` walks merge graph и находит morecms-era commits. Acceptance criterion 5 verify-migration требует уточнения в close-note этой таски.
|
||||
**Where I stopped:** (not started)
|
||||
**Next action:** **3a. Clean prefixes:**
|
||||
```powershell
|
||||
@@ -123,19 +125,18 @@ Acceptance:
|
||||
- Gitea web shows updated tree
|
||||
|
||||
Done — 🟢; unblock `morecms-cleanup-and-breadcrumb` + `resilience-roadmap-design`.
|
||||
**Blocker:** morecms-subtree-split
|
||||
**Branch:** n/a
|
||||
**Branch:** master
|
||||
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-21T10:23:14.254Z -->
|
||||
|
||||
---
|
||||
|
||||
## 🔵 [morecms-cleanup-and-breadcrumb] — Шаги 8-10 миграции: в `~/projects/MoreThenCms` удалить мигрированные файлы (6 entities + 17 concepts + 3 sources + 7 tasks), обновить STATUS.md/index.md, добавить `.wiki/concepts/migrated-infra-to-admin.md` breadcrumb со списком «что куда уехало», push на Gitea.
|
||||
## ⚪ [morecms-cleanup-and-breadcrumb] — Шаги 8-10 миграции: в `~/projects/MoreThenCms` удалить мигрированные файлы (6 entities + 17 concepts + 3 sources + 7 tasks), обновить STATUS.md/index.md, добавить `.wiki/concepts/migrated-infra-to-admin.md` breadcrumb со списком «что куда уехало», push на Gitea.
|
||||
|
||||
См. `.wiki/concepts/admin-infra-project.md` §Migration mechanics шаги 8-10 + полный шаблон breadcrumb'а в §9.
|
||||
|
||||
**Очерёдность:** ТОЛЬКО после успешного push'а admin'а с импортированным контентом (`admin-subtree-import-and-cleanup` 🟢). Иначе риск удалить из MoreThenCms то что ещё не зафиксировано в admin.
|
||||
|
||||
**Status:** blocked
|
||||
**Status:** ready
|
||||
**Where I stopped:** (not started)
|
||||
**Next action:** **8. Cleanup commit (drop migrated):**
|
||||
```powershell
|
||||
@@ -181,7 +182,6 @@ Acceptance:
|
||||
- Gitea web для MoreThenCms показывает обновлённую `.wiki/concepts/migrated-infra-to-admin.md`
|
||||
|
||||
Done — 🟢; unblock `verify-migration`.
|
||||
**Blocker:** admin-subtree-import-and-cleanup
|
||||
**Branch:** n/a
|
||||
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-21T10:23:37.967Z -->
|
||||
|
||||
@@ -217,7 +217,7 @@ Done — 🟢 с close-note типа `verified end-to-end: tasks+wiki visible in
|
||||
|
||||
---
|
||||
|
||||
## 🔵 [resilience-roadmap-design] — Развернуть placeholder `.wiki/concepts/future-resilient-architecture-goals.md` (мигрирует из MoreThenCms в `admin-subtree-import-and-cleanup`) в полноценный fault-tolerance roadmap. Естественная отправная точка для глобальной цели пользователя: «создать отказоустойчивое решение, чтобы такие проблемы как 2026-05-18 не повторялись».
|
||||
## ⚪ [resilience-roadmap-design] — Развернуть placeholder `.wiki/concepts/future-resilient-architecture-goals.md` (мигрирует из MoreThenCms в `admin-subtree-import-and-cleanup`) в полноценный fault-tolerance roadmap. Естественная отправная точка для глобальной цели пользователя: «создать отказоустойчивое решение, чтобы такие проблемы как 2026-05-18 не повторялись».
|
||||
|
||||
Это **design task** (не impl): продукт — обновлённая `concepts/future-resilient-architecture-goals.md` + потенциально цепочка impl-тасок-children. См. `concepts/admin-infra-project.md` §Initial admin agenda E.1.
|
||||
|
||||
@@ -242,7 +242,6 @@ Done — 🟢 с close-note типа `verified end-to-end: tasks+wiki visible in
|
||||
4. **Constraints:** roadmap **не** review автоматически — это living document. Acceptance: user (vitya) подтвердил план, есть concrete impl-tasks для top-3 priority items, документ committed.
|
||||
|
||||
Done — 🟢 с close-note: «roadmap expanded; N impl-tasks created for first wave».
|
||||
**Blocker:** admin-subtree-import-and-cleanup
|
||||
**Branch:** n/a
|
||||
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-21T10:24:24.685Z -->
|
||||
|
||||
@@ -385,8 +384,86 @@ Done — 🟢; secret-management dyra closed.
|
||||
**Status:** blocked
|
||||
**Where I stopped:** (not started)
|
||||
**Next action:** Дождаться 🟢 у всех blocker-тасок (включая `admin-infra-project-pointers` — без него pointers в `.wiki/CLAUDE.md` не залиты, и review будет читать stub). Прочитать спецификацию (см. путь в description). Для каждой импл-таски: `git show <commit>`, прогнать acceptance criteria из её next_action, сверить с design-decisions в спецификации. Findings → новые follow-up tasks через `mcp__projects-meta__tasks_create`.
|
||||
**Blocker:** migration: morecms-subtree-split, admin-subtree-import-and-cleanup, morecms-cleanup-and-breadcrumb, verify-migration; agenda: resilience-roadmap-design, secrets-out-of-common, secrets-manager-adopt
|
||||
**Blocker:** migration: morecms-cleanup-and-breadcrumb, verify-migration; agenda: resilience-roadmap-design, secrets-out-of-common, secrets-manager-adopt
|
||||
**Branch:** n/a
|
||||
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-21T10:25:43.430Z -->
|
||||
|
||||
---
|
||||
|
||||
# Imported from MoreThenCms — 2026-05-21 subtree-split migration
|
||||
|
||||
Полное содержимое `.tasks/STATUS.md` из MoreThenCms на момент 2026-05-21, **CMS-domain блоки исключены** (остались в MoreThenCms): `cms-admin-assets-root-folders-seed`, `cms-port-leak-fix`, `traefik-maljarka-502-bug`, `cms-maljarka-https-mode-bug-fix`. История per-file сохранена через subtree-split + temp-prefix merge.
|
||||
|
||||
## 🟢 [vds-kzntsv-bootstrap] — VDS поднят, gitea/verdaccio/registry мигрированы (3/3 phases done 2026-05-20)
|
||||
**Status:** done (2026-05-20 вечер — все 3 заявленные фазы закрыты)
|
||||
**Where I stopped:** Phase 1 + Phase 2 завершены ✅. VDS `89.253.255.94 / vds.kzntsv.site` Ubuntu 24.04 upgraded через VNC. sudo vitya (NOPASSWD) + ssh-key + hardened sshd (key-only) + ufw 22/80/443 + DB-порты + fail2ban + docker 29.5.1 + buildx + compose. Traefik v2.11 на `traefik.vds.kzntsv.site` (basicAuth vitya:Pryakhin9). Portainer CE 2.21.5 на `portainer.vds.kzntsv.site` — **админ-пароль был принудительно изменён на `Pryakhin9-VDS-2026` (18 chars)** из-за hard min-12-char policy в Portainer 2.21+ (regression от 2.20). API key получен и сохранён в `vds-kzntsv.env`. 4 DB-стека (Postgres 16, MariaDB 11.4, Mongo 7, Redis 7) подняты с self-signed TLS, доступны снаружи через traefik raw-TCP passthrough на `<db>.vds.kzntsv.site:<port>` (HostSNI(*) — traefik tls.passthrough+SNI не работает с STARTTLS-протоколами PG/MariaDB; rawTCP forward, DBs терминируют TLS сами). Все DB passwords (PG/Maria/Mongo/Redis) — strong random hex16, сохранены в `vds-kzntsv.env`.
|
||||
**Open questions:** LE certs для DBs (сейчас self-signed → клиент verify-skip; permanent fix позже через lego sidecar extract из traefik acme.json).
|
||||
**Next action:** Phase 3 миграция с kreknin **ВСЯ DONE ✅**. 3.1 gitea (132 repos, 4 users, v1.25.5 на git.kzntsv.site). 3.2 verdaccio (8.6G storage, 2063 packages, secret 32 chars). 3.3 registry — **отказались от миграции** старых 35G images (user: «новых наделать могу»), fresh install на registry.kzntsv.site + Joxit UI на registry-ui.vds.kzntsv.site (DELETE_IMAGES=true для GUI cleanup, REGISTRY_STORAGE_DELETE_ENABLED для API delete). Auth vitya:Pryakhin9 (см. vds-kzntsv.env). Follow-up tasks: vds-gc-cron, vds-backup-rsync-kreknin, vds-ntfy-push.
|
||||
**Branch:** master
|
||||
|
||||
---
|
||||
|
||||
## 🟢 [iis-on-host-migration] — attempt 2 close-out: 36h soak passed, 8/8 наших sites зеленые
|
||||
**Status:** done (2026-05-21 close-out). Detail: Phase 11 в [iis-host-migration-2026-05-19](../.wiki/sources/iis-host-migration-2026-05-19.md).
|
||||
**Close-out evidence:** traefik `Up 38h` (zero restarts), w3wp 8.8h (scheduled pool recycle ~29h default, не crash), 8/8 наших sites вернули `Microsoft-IIS/10.0` через full traefik HTTPS chain (`emspb/snolla/on.snolla/pilorama98/labtools.ru/labtools.pro/tandemmebel/kupimknigi`). Маljarka/sestech/ics-artmaterials оказались **off-infra** (DNS снят / parked / external nginx) — не наша инфра, не regression. Новая follow-up task ⚪ `iis-traefik-dead-routes-cleanup` (низкий приоритет — почистить yml для 3 dead доменов).
|
||||
**VM savestate:** **done 2026-05-21 05:13 MSK** (45.6s, VMState=saved). Savestate file 1.73 GB (`C:\Users\vitya\VirtualBox VMs\snolla-recovery\Snapshots\2026-05-21T05-12-43-636491200Z.sav`). VBoxHeadless процессы освободили ~4 GB private memory. Resume: `VBoxManage startvm snolla-recovery --type headless` (~30 сек если понадобится). Disk delete (`unregistervm --delete`, ~95 GB) — позже через неделю uptime.
|
||||
**Atomic revert (unchanged):** ```Get-ChildItem C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\*.yml.bak-pre-attempt2-2026-05-19 | %{ Copy-Item $_.FullName -Destination ($_.FullName -replace '\.bak-pre-attempt2-2026-05-19$','') -Force }; Move-Item C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\stostayer.yml.disabled C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\stostayer.yml -Force -EA SilentlyContinue; Move-Item C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\oldstostayer.yml.disabled C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\oldstostayer.yml -Force -EA SilentlyContinue; docker restart traefik```
|
||||
**Branch:** master
|
||||
|
||||
---
|
||||
|
||||
## 🟢 [iis-traefik-dead-routes-cleanup] — sestech + isc-artmaterials disabled, maljarka оказалась live
|
||||
**Status:** done (2026-05-21 ~05:42 MSK; ~20 мин работы).
|
||||
**Result:**
|
||||
- `sestech.yml` → `.disabled` (backup `.bak-pre-dead-routes-cleanup-2026-05-21`). DNS `sestech.ru/www.sestech.ru → 31.31.205.163` = parking provider, наш traefik больше не отвечает (public requests идут на parking host через DNS).
|
||||
- `isc-artmaterials.yml` → `.disabled` (backup created). DNS `ics-artmaterials.com/www → 87.236.16.28 = external nginx-reuseport WP-хостинг`, public traffic не через нас.
|
||||
- **Maljarka — НЕ disabled** — оказалось yml route'ит `maljarka.tandemmebel.ru` (subdomain тандеммебели), не голый `maljarka.ru`. DNS → **94.19.247.14 = наш IP**, IIS backend отвечает 200 OK ("Малярка от Тандеммебель", 43KB). Live route, не dead. Изначальная task'а ошиблась с hostname.
|
||||
**Smoke regression:** 4 live hosts (emspb/on.snolla/www.tandemmebel/kupimknigi) → Microsoft-IIS/10.0 без regression. File-provider auto-reload, traefik restart не понадобился.
|
||||
**Side finding:** `maljarka.tandemmebel.ru` через traefik HTTPS → **502 Bad Gateway**, хотя backend (host.docker.internal:8089 + Host header) возвращает 200 OK. Конфиг yml identical к работающему `tandemmebel.yml`. Live bug. Новая follow-up ⚪ task `traefik-maljarka-502-bug` (CMS-domain — остаётся в MoreThenCms).
|
||||
**Atomic revert:** ```Move-Item C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\sestech.yml.disabled C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\sestech.yml -Force; Move-Item C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\isc-artmaterials.yml.disabled C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\isc-artmaterials.yml -Force```
|
||||
**Branch:** master
|
||||
|
||||
---
|
||||
|
||||
## 🟢 [vds-gc-cron] — weekly GC armed для verdaccio + registry на VDS
|
||||
**Status:** done (2026-05-21 00:21 MSK; ~1ч работы). Детали в [vds-gc-cron.md](vds-gc-cron.md).
|
||||
**Result:** `/etc/cron.d/vds-gc` armed — Sun 03:00 MSK registry-gc.sh, Sun 03:30 MSK verdaccio-prune.sh. Scripts в `/opt/stacks/gc/scripts/` (`notify.sh`, `registry-gc.sh`, `verdaccio-prune.sh`+`verdaccio-prune.js`). Logs в `/var/log/vds-gc/`. Notify через ntfy `vds-ops`.
|
||||
**Verdaccio prune real run:** 8.5G → 6.8G (freed 1.7G), 7021 packages scanned, 1551 modified, 4524 tgz deleted, 0 errors, 73 locally-published versions защищены (@snollajs/* + similar — detected via `_distfiles[tgzName]` absent). Atomic JSON rewrite (`tmp + rename`) + `_rev` bump.
|
||||
**Registry GC smoke:** alpine v1+v2 pushed → DELETE manifest → GC freed 61M (whole tree, since both tags pointed одну digest). Empty-storage guard added (first run на свежем registry — `/var/lib/registry/docker/registry/v2/repositories` отсутствует → skip+min-prio notify, не FAIL).
|
||||
**Atomic revert:** `ssh vitya@89.253.255.94 'sudo rm /etc/cron.d/vds-gc; sudo systemctl restart cron; sudo rm -rf /opt/stacks/gc /var/log/vds-gc'`
|
||||
**Branch:** master
|
||||
|
||||
---
|
||||
|
||||
## 🟢 [vds-backup-rsync-kreknin] — daily VDS→kreknin live, smoke green, cron armed
|
||||
**Status:** done (2026-05-20 → 2026-05-21 00:00 first-run завершён успешно). Детали в [vds-backup-rsync-kreknin.md](vds-backup-rsync-kreknin.md).
|
||||
**Result:** rsync `--link-dest` pipeline `/opt/stacks/backup/scripts/run.sh` под cron `0 5 * * *` (root) → kreknin `/volume1/NetBackup/vds-kzntsv/<DATE>/` + symlink `latest`. DB dumps (pg/maria/mongo/redis), system config (/etc/ssh, ufw, hosts, docker), ssh keys, /opt/stacks data. Notification dual-channel: email через msmtp+Yandex (`/etc/msmtprc`) + ntfy publish на `vds-backup` topic. Retention 7 daily snapshots. Smoke run done — 71,867 files / 11.74G в 21m27s, exit 0. Cron service installed (apt install cron — Ubuntu 24.04 не имел его) + enabled. Next run автоматически 2026-05-21 05:00 MSK.
|
||||
**SPOF gap status:** closed for VDS — full restore from kreknin возможен (DB dumps + конфиги + ssh keys).
|
||||
**Open:** phone-side smoke (user verifies ntfy push пришёл из реального backup run).
|
||||
**Atomic revert:** `ssh vitya@89.253.255.94 'sudo systemctl stop cron; sudo rm /etc/cron.d/vds-backup; sudo apt-get remove -y cron msmtp msmtp-mta; sudo rm -rf /etc/msmtprc /opt/stacks/backup /var/log/vds-backup'`
|
||||
**Branch:** master
|
||||
|
||||
---
|
||||
|
||||
## 🟢 [vds-ntfy-push] — self-hosted ntfy.vds.kzntsv.site live, phone-verified
|
||||
**Status:** done (2026-05-20 вечер; ~30 min работы). Detail в [vds-ntfy-push.md](vds-ntfy-push.md).
|
||||
**Result:** ntfy v2.11.0 на `ntfy.vds.kzntsv.site`, LE cert (R12, valid до 2026-08-18), admin user `vitya / Pryakhin9` (deny-all дефолт, admin role → rw to all), топики `vds-backup` + `vds-ops`. Phone-verified — оба push'а пришли в Android ntfy app (screenshot 22:54). Creds в `~/projects/.common/secrets/vds-kzntsv.env` + `/opt/stacks/ntfy/.env` (chmod 600).
|
||||
**Integration ready for:** [[vds-backup-rsync-kreknin]] (publish status на `vds-backup`), monitoring scripts (на `vds-ops`).
|
||||
**Atomic revert:** `ssh vitya@89.253.255.94 'cd /opt/stacks/ntfy && docker compose down -v && cd .. && rm -rf ntfy'` + remove ntfy entries из vds-kzntsv.env.
|
||||
**Branch:** master
|
||||
|
||||
---
|
||||
|
||||
## 🟢 [nas-recovery] — клиентские сайты восстановлены, работают из публичного интернета
|
||||
**Status:** done (2026-05-19 ~10:00 MSK — 15 часов работы)
|
||||
**Final state:** OpenWRT NAT → traefik 2.6.6 (host:8000/4443) → VirtualBox VM snolla-recovery (IIS + CMS) + docker-стек на хосте (MSSQL/MinIO/ES/imgproxy/nginx). 13 client routes, 40 LE certs валидны. Проверено: пользовательский клиент из публичного интернета открывает snolla.com, pilorama98.ru, labtools.ru/pro, tandemmebel.ru, emspb.ru.
|
||||
**Backup pull:** C:\nas-recovery\vm-sites\ (~11 GB inetpub/wwwroot + stayer/) — на случай если VM снова станет нестабильной.
|
||||
**Open issues (минор):**
|
||||
- X-Forwarded-Proto/Host: CMS делает redirect с портом 4443 в URL — нужно добавить middleware в traefik или включить trust в IIS.
|
||||
- MinIO/Azure storage — пользователь упомянул "не так всё", ждём пояснение.
|
||||
- docker-сокет проброс в traefik — daemon connection error (некритично, file-provider routing работает).
|
||||
- VM в bridged-WiFi была нестабильной → переехали на NAT + port forwards.
|
||||
- acme.json renewal через HTTP-01 фейлится для доменов с DNS не на нашем IP — нужно переключить на DNS-01 через REGRU (creds в env уже).
|
||||
**Branch:** master
|
||||
|
||||
---
|
||||
|
||||
122
.tasks/iis-on-host-migration.md
Normal file
122
.tasks/iis-on-host-migration.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# iis-on-host-migration
|
||||
|
||||
## Goal
|
||||
Перенести IIS-сайты MoreThenCms из VirtualBox VM (`snolla-recovery`) на нативный IIS Windows-хоста. VM остаётся как hot fallback на первое время; после успешного нативного-запуска — выключаем VM, освобождаем 4 GB RAM + ~92 GB диска.
|
||||
|
||||
**Зачем:** VM на VBox = лишний слой нестабильности (видели один network hang). Нативный IIS на хосте — проще, быстрее, без NAT-форвардов и VBox-капризов. Контейнеры MSSQL/MinIO/etc. уже на этом же хосте — устраняем сетевой роутинг.
|
||||
|
||||
## Что у нас уже есть
|
||||
|
||||
- ✅ IIS установлен на хосте (W3SVC running, default site empty, [`.wiki/entities/windows-recovery-host`](../.wiki/entities/windows-recovery-host.md))
|
||||
- ✅ .NET Framework 4.8.1 на хосте
|
||||
- ✅ Полная копия `C:\inetpub\wwwroot\` из VM → `C:\nas-recovery\vm-sites\wwwroot\` (8.69 GB)
|
||||
- ✅ Полная копия `C:\stayer\` из VM → `C:\nas-recovery\vm-sites\stayer\` (2.21 GB)
|
||||
- ✅ MSSQL контейнер на host:1433 (5 production DB)
|
||||
- ✅ MinIO на host:9000, Elasticsearch на host:9200, imgproxy на host:8787/8788
|
||||
- ✅ Исходники CMS в `C:\Users\vitya\projects\MoreThenCms\` (Git репо)
|
||||
- ✅ Traefik с 13 client routes (сейчас target = `host.docker.internal:18080` = VM)
|
||||
|
||||
## Key files
|
||||
- `C:\nas-recovery\vm-sites\wwwroot\` — IIS-сайты из VM
|
||||
- `C:\nas-recovery\vm-sites\stayer\` — stostayer-проекты
|
||||
- `C:\inetpub\wwwroot\` — целевое расположение на хосте
|
||||
- `C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\*.yml` — backends, нужно переключить с `host.docker.internal:18080` на `localhost:80` (или какие-то local IIS-bindings)
|
||||
- `Web.config` файлы — connection strings уже патчены на `10.0.2.2`, нужно вернуть на `localhost` или `127.0.0.1` для нативного IIS
|
||||
|
||||
## План этапов
|
||||
|
||||
### Phase 1 — Анализ и подготовка
|
||||
- Изучить IIS site bindings в VM (MoreThenCms.Web `*:80`, Snolla.IdentityManager `*:80`, stostayer `*:8080`, stostayer.old `*:8081`)
|
||||
- Решить port-mapping на хосте — оставлять 80? Или другие порты, traefik на 80/443 переадресует?
|
||||
- Изучить, какие IIS modules/features нужны (URL Rewrite, ARR, обязательные .NET runtimes — может потребоваться доставить)
|
||||
|
||||
### Phase 2 — Импорт сайтов на хост
|
||||
- Скопировать `C:\nas-recovery\vm-sites\wwwroot\*` → `C:\inetpub\wwwroot\` (или другой root)
|
||||
- Скопировать `C:\nas-recovery\vm-sites\stayer\` → `C:\stayer\`
|
||||
- Создать application pools (`.NET v4.5` или эквивалент) для каждого сайта в IIS Manager
|
||||
- Создать sites в IIS:
|
||||
- `MoreThenCms.Web` (port 80 / другой)
|
||||
- `Snolla.IdentityManager`
|
||||
- `stostayer` (port 8080)
|
||||
- `stostayer.old` (port 8081)
|
||||
- Web.config patch: `Data Source=10.0.2.2` → `Data Source=localhost` (или `127.0.0.1` / `.\` ; UTF-8 BOM!)
|
||||
|
||||
### Phase 3 — Перенос на хост IIS, тест
|
||||
- Stop VM (но не удалять!)
|
||||
- Update traefik backends в `data/custom/*.yml`:
|
||||
- `http://host.docker.internal:18080/` → `http://host.docker.internal:80/` (или какой порт IIS использует)
|
||||
- `:18180` → `:8080`, `:18181` → `:8081`
|
||||
- traefik restart
|
||||
- Прокликать все 11 клиентских доменов из публичного интернета
|
||||
- Убедиться что .NET Framework 4.8.1 справляется, нет missing assemblies, нет permission issues на App_Data / temp folders
|
||||
|
||||
### Phase 4 — Cleanup VM
|
||||
- Когда стабильно ~неделя на хост-IIS — выключить VM окончательно
|
||||
- `VBoxManage controlvm "snolla-recovery" poweroff`
|
||||
- Удалить port forwards в OpenWRT, которые больше не нужны (host:18080, host:18180, etc.) — это уже не traefik backend
|
||||
- (Опционально) удалить VM из VBox: `VBoxManage unregistervm "snolla-recovery" --delete`. Это освобождает 92 GB на C:.
|
||||
- Удалить `C:\nas-recovery\backup\snolla\snolla.ova` (42.5 GB) и `C:\nas-recovery\vm-sites\` (11 GB) — backup уже не нужен.
|
||||
|
||||
### Phase 5 — Бонусы
|
||||
- Установить **URL Rewrite + ARR** для нормальной обработки X-Forwarded-Proto headers (fix CMS-редиректа с `:4443` в URL — см. [`.wiki/concepts/traefik-on-windows-docker-desktop`](../.wiki/concepts/traefik-on-windows-docker-desktop.md))
|
||||
- Настроить log rotation для C:\inetpub\logs\
|
||||
- Application Initialization (для warm-start CMS) — IIS Optional Feature, autostart сайтов
|
||||
|
||||
## Open questions
|
||||
- [ ] CMS-код использует абсолютные пути типа `C:\inetpub\wwwroot\MoreThenCms.Web\` (видели в Web.config `<add key="sitePath" .../>`)? Если да — путь должен остаться, либо обновить.
|
||||
- [ ] Authentication / IIS Application Identity — какая учётка должна крутить app pool? (LocalSystem? NetworkService? IIS AppPool\<sitename>?)
|
||||
- [ ] Нужен ли .NET Framework Repair / SAC update перед миграцией?
|
||||
- [ ] Что с storage providers (Azure-SDK adapter в CMS)? — Связано с открытым вопросом о MinIO/Azure, что пользователь обещал прояснить.
|
||||
|
||||
## Decisions log
|
||||
- 2026-05-19: задача поставлена после успешного recovery в VM. VM показала себя нестабильной (один network hang за день uptime), решено мигрировать на нативный IIS как более простой и предсказуемый стек.
|
||||
|
||||
## Completed steps
|
||||
- [x] Pulled VM sites to host (49 минут tar+ssh, 11 GB total)
|
||||
- [x] IIS на хосте установлен (в рамках recovery, ещё нативно не использовался)
|
||||
- [x] **Phase 1 — discovery** (2026-05-19): VM сайты/пулы/vdirs/features через SSH + appcmd, host IIS features через elevated DISM, source grep на hardcoded paths. См. `.wiki/sources/iis-host-migration-2026-05-19.md`.
|
||||
- [x] **Phase 2 — миграция MoreThenCms.Web** (2026-05-19): `C:\sites\MoreThenCms.Web` (8.7 GB robocopy), Web.config patch (`sitePath` + conn → localhost, UTF-8 BOM), AppPool + Website на `*:80`, ACL `:(OI)(CI)M` для `IIS AppPool\MoreThenCms.Web`. Default Web Site остановлен. Smoke test через `localhost` с Host header — 10/11 хостов отвечают (rimiz.ru → 404, как issue ниже).
|
||||
- [x] **Phase 2 — stostayer / stostayer.old** (2026-05-19, частично): сайты созданы на `:8090/:8091` → `C:\stayer\MoreThenCms.Web` / `C:\stayer\stostayer.old`, AppPools созданы, ACL поставлен. **Но локально оба отвечают timeout** — рантайм-проблема не разбиралась.
|
||||
- [x] **Phase 3 — traefik switch** (2026-05-19): 11 yml пропатчены `host.docker.internal:18080` → `host.docker.internal:80`, traefik file-provider auto-reload, public smoke через `https://localhost:4443/` — **10/11 хостов → HTTP 200**, rimiz.ru → 404. Stostayer/oldstostayer backend в traefik не трогали (остаются на VM).
|
||||
|
||||
## Решения (decisions log дополнен)
|
||||
- 2026-05-19: **Snolla.IdentityManager не мигрируется** — пользователь подтвердил «не нужен» (Q-C в сессии). Если что-то внутри CMS дёргает его по `localhost:8089` — увидим в логах, тогда вернёмся.
|
||||
- 2026-05-19: **Sub-apps stostayer.old (`/calc`, `/price`, `/price/tireService`) не мигрируются** — пользователь подтвердил «не нужны».
|
||||
- 2026-05-19: **stostayer.connection-string на 89.253.219.2 не трогаем** (Q-B пользователя «не трогай, так надо»). Это внешний production MSSQL для stayer-инфраструктуры, не наш контейнер.
|
||||
- 2026-05-19: **AppPool identity = ApplicationPoolIdentity** для всех 3 пулов (default outside SCM, безопасно). ACL `:(OI)(CI)M` (Modify) рекурсивно на site root.
|
||||
- 2026-05-19: **Path strategy = C:\sites\\** (не `C:\inetpub\wwwroot\`) — пользователь выбрал (б) в Q1.
|
||||
|
||||
## Status — ROLLBACK (2026-05-19 вечер)
|
||||
|
||||
**Миграция сделана, сломала prod, откатили.** См. полный разбор в `.wiki/concepts/iis-migration-2026-05-19-postmortem.md`.
|
||||
|
||||
- ⚠️ Phase 1-8 выполнены технически, но Phase 3 ввёл Docker port-loop (`host.docker.internal:80` резолвится через Docker Desktop NAT обратно в сам traefik, `https.yml` redirect-to-https middleware → 301 → loop). Симптомы появились ~10 мин после моего "done" → 502 → TOO_MANY_REDIRECTS.
|
||||
- ✅ **Revert (Phase 9) успешен:** VM поднята из savestate, traefik backends восстановлены из `.bak-phase3` → trafic снова через VM (как до session).
|
||||
- ✅ Артефакты для следующей попытки **сохранены** на хосте: `C:\sites\{snolla, stostayer, stostayer.old, snolla-identity-manager}` + IIS sites + AppPools + 3 traefik backup-stamp серий.
|
||||
|
||||
## Completed Phase 4-9 (вечер 2026-05-19)
|
||||
|
||||
- [x] **Phase 4 — реорг** `C:\sites\`: rename `MoreThenCms.Web → snolla`, move `C:\stayer\MoreThenCms.Web → C:\sites\stostayer`, move `C:\stayer\stostayer.old → C:\sites\stostayer.old`, move + kebab-rename `C:\stayer\Snolla.IdentityManager → C:\sites\snolla-identity-manager`. Удалены 3 sub-app папки. `C:\stayer\` снёс полностью.
|
||||
- [x] **Phase 4 — IIS rename**: site `MoreThenCms.Web → snolla`, pool пересоздан как `snolla` (rename невозможен in-place), site rebound, физпуть обновлён на `C:\sites\<name>` для всех 3, sitePath в Web.config'ах подправлен под новые пути. ACL re-granted.
|
||||
- [x] **Phase 5 — stostayer.old DB-fix**: `Data Source=10.0.2.2` → `localhost`. После — `:8091` → 200 локально (была наша ошибка в Phase 2 — пропустили этот файл).
|
||||
- [x] **Phase 6 — stostayer DB-migration**: старый `89.253.219.2,1433` не reachable. Новые creds: `www.stostayer.ru,1433`, user `stayer_site`. Patched Web.config. Поймана и зафиксирована **новая gotcha** в [[webconfig-password-xml-escape]] — `&` в пароле нужно XML-escape как `&`.
|
||||
- [x] **Phase 7 — traefik privacy**: `stostayer.yml` и `oldstostayer.yml` переименованы в `.yml.disabled` (потом возвращены в Phase 9). Backup: `*.yml.bak-stayer-switch-2026-05-19`.
|
||||
- [x] **Phase 8 — VM savestate**: `savestate`. VMState=saved (потом возвращена в Phase 9). **БЫЛО ПРЕЖДЕВРЕМЕННЫМ.**
|
||||
- [x] **Phase 9 — ROLLBACK** (после catastrophe): `VBoxManage startvm`, ipconfig release/renew для разморозки network, traefik yml восстановлены из `.bak-phase3-2026-05-19`, stayer .yml.disabled удалены и yml восстановлены из `.bak-stayer-switch`, `docker restart traefik`. Public smoke — 7 хостов через VM-chain ответили (2× 200, 5× 301 CMS-redirect = normal), пользователь подтвердил в браузере.
|
||||
|
||||
## Что НЕ делать в следующей попытке
|
||||
|
||||
См. полный recipe в `.wiki/concepts/iis-migration-2026-05-19-postmortem.md`, кратко:
|
||||
- **НЕ** использовать backend `host.docker.internal:80` (Docker NAT loopback с traefik HTTP entrypoint :80)
|
||||
- **НЕ** тестировать только `-MaximumRedirection 5` (скрывает loop)
|
||||
- **НЕ** тестировать только из LAN (router-hairpin lying)
|
||||
- **НЕ** замораживать VM раньше чем через 24-48h стабильности host-стека
|
||||
- **НЕ** делать reorg/rename in same session as migration (атомность важна)
|
||||
- **НЕ** объявлять "done" до 24h+ uptime + теста из НЕ-LAN сети
|
||||
- При первом anomaly — **STOP, revert на known-good, понять, потом fix** (НЕ каскадные reactive changes)
|
||||
|
||||
## Notes
|
||||
|
||||
- VM не удалять до конца Phase 3 — это working fallback. Только после двух недель стабильной работы host-IIS.
|
||||
- Git push в gitea невозможен пока — gitea был на мёртвой синке. Восстановление gitea — отдельная задача (есть бэкап `/docker/gitea/` 2.6 GB на kreknin-синке, можно поднять локально или временно класть code в другое место).
|
||||
- При работе с Web.config — **обязательно UTF-8 BOM** через `[System.IO.File]::WriteAllText` с `[System.Text.UTF8Encoding]::new($true)`. Иначе IIS 500.19. Детали в [`.wiki/concepts/cms-config-rewrite-pattern`](../.wiki/concepts/cms-config-rewrite-pattern.md).
|
||||
43
.tasks/iis-traefik-dead-routes-cleanup.md
Normal file
43
.tasks/iis-traefik-dead-routes-cleanup.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# iis-traefik-dead-routes-cleanup
|
||||
|
||||
## Goal
|
||||
|
||||
Убрать из traefik (и host IIS bindings, если нужно) routes для 3 hostname'ов которые больше **не указывают на нашу инфраструктуру** — выяснилось в ходе [[iis-on-host-migration]] Phase 11 close-out smoke (2026-05-21):
|
||||
|
||||
| Host | Where it points now | Original task expectation |
|
||||
|---|---|---|
|
||||
| `maljarka.ru` | DNS снят (нет A record) | host IIS `snolla` |
|
||||
| `sestech.ru` | Парковка domainparking.ru (TLS subj `CN=*.domainparking.ru`, cert от 2025-11-04) | host IIS `snolla` |
|
||||
| `ics-artmaterials.com` | A→`87.236.16.28`, отвечает `nginx-reuseport/1.21.1` (внешний WP-хостинг) | host IIS `snolla` |
|
||||
|
||||
## Key files
|
||||
|
||||
- `C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\` — поискать yml где упоминаются эти 3 host'а. Скорее всего отдельный yml на каждый, либо combined.
|
||||
- Possibly host IIS site `snolla` bindings (через `appcmd list site snolla` или `Get-WebBinding`) — если bind на эти hostname'ы есть, тоже убрать.
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] Удалять yml совсем или `.disabled` rename (как сделано с `stostayer.yml.disabled`)? Disabled безопаснее на случай если домен вернётся.
|
||||
- [ ] Нужно ли уведомить владельцев доменов (если внутренние SaaS-клиенты) что routes снимаются?
|
||||
- [ ] `rimiz.ru` (404 CMS-side, не off-infra) — НЕ сюда. Это open issue в [[recovery-architecture-snapshot]].
|
||||
|
||||
## Implementation sketch
|
||||
|
||||
```powershell
|
||||
# 1. Найти yml-ы, упоминающие dead hosts
|
||||
Select-String -Path "C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\*.yml" -Pattern "maljarka|sestech|ics-artmaterials"
|
||||
|
||||
# 2. Backup + rename .disabled (или удалить)
|
||||
# (зависит от того что найдено в шаге 1)
|
||||
|
||||
# 3. docker restart traefik
|
||||
|
||||
# 4. Smoke: tail traefik logs на 30s — никаких errors для удалённых routes
|
||||
docker logs --since 30s traefik 2>&1 | Select-String -Pattern "maljarka|sestech|ics-artmaterials"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Low priority — dead routes генерируют noise в traefik logs (LE cert renewal попытки и т.п.), но не блокируют live trafic.
|
||||
- Атомарный revert: rename `.disabled → .yml` обратно, `docker restart traefik`.
|
||||
- Если у `sestech.ru` истёк parking cert, LE может пытаться renew наш cert тоже (если в acme.json остались записи); это благодарность для отдельной чистки `acme.json`.
|
||||
68
.tasks/nas-recovery.md
Normal file
68
.tasks/nas-recovery.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# nas-recovery
|
||||
|
||||
## Goal
|
||||
Поднять клиентские сайты MoreThenCms на локальной Windows-машине пользователя после краха исходной Synology NAS. Источник данных — Hyper Backup репо `/volume1/NetBackup/diskstation_1.hbk` на удалённой живой синке (195.19.90.188, kreknin.site:5000), без шифрования, 430 GB, забэкаплены `/backup`, `/docker`, `/work` мёртвой синки.
|
||||
|
||||
## Key files
|
||||
- `_Syno_TaskConfig` на удалённой синке — метаданные задачи Hyper Backup (`rsync Server 1`, source `diskstation`).
|
||||
- `C:\Users\vitya\projects\MoreThenCms\Web.config` — connection strings CMS, надо будет переписать на `localhost:<port>`.
|
||||
|
||||
## Decisions log
|
||||
- 2026-05-18: восстанавливаем не через HBE/SFTP-пулл всего `.hbk`, а через **selective restore на самой удалённой синке** → SFTP-пулл только плоских файлов (для 430 GB через узкий канал — единственный реалистичный путь).
|
||||
- 2026-05-18: для БД — путь через `.bak` дамп (если есть в `/backup`) + `RESTORE DATABASE` в новый контейнер MSSQL на Windows. Fallback — extract из docker volume.
|
||||
- 2026-05-18: VM с мёртвой синки не восстанавливаем — она была runtime, не data. CMS гоняем нативно на Windows IIS.
|
||||
|
||||
## Open questions
|
||||
- [ ] **Что такое `snolla.ova`** — OVA-экспорт Windows-VM с MoreThenCms или что-то другое? Если VM-снапшот — план переключается на "импорт OVA в Hyper-V на Windows-PC", всё остальное (IIS, .bak, MSSQL container, MinIO config) становится опциональным fallback.
|
||||
- [ ] Свежесть OVA-экспорта (если это он)?
|
||||
- [ ] Точная мажорная версия MSSQL? (от неё зависит тег image локального контейнера — нельзя восстанавливать .bak в младшую версию)
|
||||
- [ ] Что лежит в `/work` (взят целиком)?
|
||||
|
||||
## Inventory с remote (видно в DSM summary восстановления, версия 09.05.2026):
|
||||
|
||||
**`backup/`:** `SRV-1135520-1`, `books`, `snolla` (← вероятно SQL-дампы для MoreThenCms)
|
||||
|
||||
**`docker/`:** `buildkit`, `cancel-music`, `code-server`, `gitea`, `hermes`, `infrastucture` (sic), `mosquitto`, `openclaw`, `personal`, `portainer`, `traefik`, `zigbee2mqtt`
|
||||
|
||||
**`work/`** — взят целиком, состав смотрим на месте.
|
||||
|
||||
**Выбраны для restore (10.05.2026, в работе):** всё перечисленное выше.
|
||||
|
||||
## Completed steps
|
||||
- [x] Подтверждена структура `.hbk` репо, прочитан `_Syno_TaskConfig`: задача `rsync Server 1`, без шифрования, source = `diskstation`, бэкап папок `/backup`, `/docker`, `/work`.
|
||||
- [x] Подтверждён доступ к DSM удалённой синки через `http://kreknin.site:5000` (HTTPS 5001 наружу не проброшен — пароль идёт по plain HTTP, после восстановления обновить).
|
||||
- [x] На удалённой синке — root в SSH, юзер vitya — DSM admin. 6 TB свободно — selective restore в `/volume1/NetBackup/restore-tmp/` влезает.
|
||||
|
||||
## Notes
|
||||
|
||||
Этапы в правильном порядке (актуальная редакция после находки snolla.ova + ежедневных .bak):
|
||||
|
||||
1. **Phase 1 — DB via .bak dump** (✅ путь определён):
|
||||
- SFTP с `~/sftp-pickup/MoreThenCms202605090301.zip` на Windows.
|
||||
- `Expand-Archive` → получаем `.bak`.
|
||||
- В MSSQL-контейнере (уже работает на `localhost:1433`) → `RESTORE FILELISTONLY` → потом `RESTORE DATABASE ... WITH MOVE`.
|
||||
|
||||
2. **Phase 2 — MinIO data** (после распаковки `/docker/personal/minio/` на синке):
|
||||
- sftp-pickup → tar/zip → SFTP на Windows → распаковка в `C:\Users\vitya\projects\docker\diskstation\minio\data\`.
|
||||
- `docker compose up -d` в `minio/`.
|
||||
|
||||
3. **Phase 3 — OVA в VirtualBox** (главный runtime):
|
||||
- FileZilla тянет `snolla.ova` (42.5 GB).
|
||||
- Импорт в VirtualBox 7.2.8 (уже установлен).
|
||||
- Стартуем VM, проверяем что IIS поднялся.
|
||||
- Правим Web.config: connection strings → MSSQL `<windows-host-ip>:1433` и MinIO `<windows-host-ip>:9000`.
|
||||
- Сайты отвечают локально.
|
||||
|
||||
4. **Phase 4 — delta файлов** (по ситуации):
|
||||
- 3-4 GB файлов между состоянием Oct 2024 (в OVA) и May 2026 (свежие данные).
|
||||
- Источник — `/work/` из restore, или билд из локального git-репо MoreThenCms.
|
||||
|
||||
**Архитектура важно:** IIS **внутри** VirtualBox VM (из OVA), НЕ на Windows-хосте. Host-IIS (W3SVC), который установлен — избыточен и должен быть остановлен (`Stop-Service W3SVC; Set-Service W3SVC -StartupType Manual`) перед запуском traefik, чтобы не было port-конфликта на 80/443. Traefik в Docker на хосте → forwarding на IP VM.
|
||||
|
||||
5. **Phase 5 — traefik + imgproxy + DNS + Let's Encrypt** (production exposure):
|
||||
- SFTP `/volume1/docker/traefik/` (compose + acme.json + data/) на Windows.
|
||||
- SFTP imgproxy-стек (расположение TBD: возможно `docker/infrastucture/imgproxy/` или `docker/personal/imgproxy/` или отдельная папка). Найти через `grep -ril imgproxy /volume1/docker/`.
|
||||
- Поднять traefik + imgproxy контейнеры с теми же middlewares.
|
||||
- imgproxy указывает на локальный MinIO (`http://minio:9000/<bucket>/...`) — connection URL внутри docker network `proxy`.
|
||||
- DNS *.kzntsv.site → новый Windows-host IP.
|
||||
- Acme.json сохраняем — сертификаты валидны до истечения, потом auto-renew через REGRU DNS-01.
|
||||
102
.tasks/vds-backup-rsync-kreknin.md
Normal file
102
.tasks/vds-backup-rsync-kreknin.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# vds-backup-rsync-kreknin
|
||||
|
||||
## Goal
|
||||
|
||||
Ежедневный rsync-бэкап важных директорий с VDS (`89.253.255.94 / vds.kzntsv.site`) → [[kreknin-synology]] (`195.19.90.188 / kreknin.site`) в 05:00 MSK. После каждого прохода — email-нотификация на `vitya.kuznetsov@gmail.com` со статусом (success/fail + transferred bytes + duration).
|
||||
|
||||
## Open questions
|
||||
|
||||
- [x] ~~**Что бэкапить:**~~ Resolved 2026-05-20. Sources в `/opt/stacks/backup/scripts/run.sh`:
|
||||
- `/opt/stacks/{gitea,verdaccio/storage,verdaccio/config,registry,traefik,portainer,ntfy,backup}`
|
||||
- `/etc/{ssh,ufw,hosts,docker}`
|
||||
- `/home/vitya/.ssh`
|
||||
- DB dumps в `$DUMP_DIR` (pg/maria/mongo/redis) — добавляются в rsync source list.
|
||||
- **Не бэкапим:** `/opt/stacks/databases/*/data` (raw datadirs — running container locks; dumps вместо).
|
||||
- [x] ~~**Куда на kreknin:**~~ `/volume1/NetBackup/vds-kzntsv/<YYYY-MM-DD>/` + symlink `latest`. Retention 7 daily snapshots (RETENTION_DAYS env). Weekly/monthly — defer (overkill для текущих ~12G).
|
||||
- [x] ~~**SSH key:**~~ продолжаем `/home/vitya/.ssh/id_ed25519_kreknin` — скрипт читает абсолютным path (work as root too).
|
||||
- [x] ~~**Tool:**~~ rsync `--link-dest` (built-in, hardlink-incremental). Encrypted (restic/borg) defer — VDS↔kreknin link trusted (оба ours), encryption key management = ops overhead. Если канал potentially compromised — переключить позже.
|
||||
- [x] ~~**Email механизм:**~~ `msmtp` + `msmtp-mta` apt-installed на VDS. Config в `/etc/msmtprc` (chmod 600 root:root, system-wide since cron runs as root). SMTP `smtp.yandex.ru:465` + creds `noreply@snolla.com` из `noreply-snolla-smtp.env`. Test send → Yandex 250 OK ✅.
|
||||
- [ ] **Phone confirm:** user verifies ntfy push на `vds-backup` topic пришёл из реального cron run (или manual run).
|
||||
|
||||
## Implementation sketch
|
||||
|
||||
`/etc/cron.d/vds-backup`:
|
||||
|
||||
```cron
|
||||
# 05:00 MSK ежедневно
|
||||
0 5 * * * vitya /opt/stacks/backup/scripts/run.sh
|
||||
```
|
||||
|
||||
`/opt/stacks/backup/scripts/run.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
SOURCE_DIRS=(/opt/stacks/gitea/data /opt/stacks/verdaccio/storage /opt/stacks/verdaccio/config /opt/stacks/registry/docker /opt/stacks/registry/auth /opt/stacks/traefik /opt/stacks/portainer/data /etc/ssh /etc/ufw /etc/hosts /etc/docker /etc/letsencrypt)
|
||||
DEST_BASE=/volume1/NetBackup/vds-kzntsv
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
LATEST=$DEST_BASE/latest
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
LOG=/tmp/backup-$TODAY.log
|
||||
|
||||
# DB dumps первым шагом — pg/maria/mongo в /tmp/db-dumps/$TODAY/, потом включаются в rsync
|
||||
mkdir -p /tmp/db-dumps/$TODAY
|
||||
docker exec postgres pg_dumpall -U postgres > /tmp/db-dumps/$TODAY/postgres.sql
|
||||
docker exec mariadb mariadb-dump --all-databases -u root -p"$MARIA_PASS" > /tmp/db-dumps/$TODAY/mariadb.sql
|
||||
docker exec mongo mongodump --out=/tmp/mongo --uri="mongodb://root:$MONGO_PASS@localhost:27017?tls=true&tlsAllowInvalidCertificates=true"
|
||||
docker exec redis redis-cli --tls --insecure -a "$REDIS_PASS" --rdb /data/dump.rdb
|
||||
|
||||
# rsync с --link-dest для hardlink-incremental
|
||||
rsync -azh --link-dest=$LATEST "${SOURCE_DIRS[@]}" /tmp/db-dumps/$TODAY \
|
||||
-e "ssh -i ~/.ssh/id_ed25519_kreknin" \
|
||||
vitya@195.19.90.188:$DEST_BASE/$TODAY/
|
||||
|
||||
ssh -i ~/.ssh/id_ed25519_kreknin vitya@195.19.90.188 \
|
||||
"rm -f $LATEST && ln -sf $DEST_BASE/$TODAY $LATEST"
|
||||
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
SIZE=$(du -sh $DEST_BASE/$TODAY 2>/dev/null | cut -f1)
|
||||
|
||||
# Email via msmtp
|
||||
echo "Subject: VDS backup $TODAY — SUCCESS
|
||||
|
||||
Size: $SIZE, duration: ${DURATION}s
|
||||
Source: vds.kzntsv.site
|
||||
Dest: kreknin.site:$DEST_BASE/$TODAY/
|
||||
" | msmtp vitya.kuznetsov@gmail.com
|
||||
```
|
||||
|
||||
(скетч; добавить retention prune — `find $DEST_BASE -maxdepth 1 -type d -name "20*" | sort | head -n -7 | xargs rm -rf`, error trap, etc.)
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **2026-05-20** — rsync `--link-dest` chosen over restic/borg. Built-in, no encryption key management, VDS↔kreknin trusted link. Encryption defer if threat model changes.
|
||||
- **2026-05-20** — Cron runs as **root**, not vitya. Initial vitya run hit ~30 Permission Denied (gitea/portainer container-owned files, /etc/ssh host keys, /etc/ufw rules). Trade-off: больше privilege чем нужно, но альтернатива (sudo wrap rsync) — сложнее. Mitigation: `/etc/msmtprc` chmod 600 root:root, `/opt/stacks/backup/.env` chmod 600.
|
||||
- **2026-05-20** — DB dump approach: `docker exec <container>` с CLI-passed credentials, не URI params. Mongo требует **legacy `--ssl --sslAllowInvalidCertificates`** (НЕ `--tls --tlsInsecure` despite --help listing `--tlsInsecure` — fails as unknown option pre-`--ssl`).
|
||||
- **2026-05-20** — Retention начали с 7 daily snapshots (RETENTION_DAYS=7). Weekly/monthly defer — 12G/day @ 7 days = 84G headroom вне hardlinks, на kreknin 5.6T free; нет смысла усложнять.
|
||||
- **2026-05-20** — Cron не был установлен на VDS из-коробки на Ubuntu 24.04 — apt install cron был нужен. Lesson для будущих vds-* tasks: проверять `which cron` в preflight.
|
||||
- **2026-05-20** — msmtp `/etc/msmtprc` system-wide (а не `~/.msmtprc`) потому что cron как root → msmtp picks up /etc/ first.
|
||||
- **2026-05-20** — Smoke run (2026-05-20 23:37 → 00:00) successful: 71,867 files / 11.74G transferred в 21m27s, all 4 DB dumps OK, latest symlink updated, ntfy + email sent.
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] msmtp + msmtp-mta apt-installed; `/etc/msmtprc` written с Yandex SMTP creds (chmod 600 root:root)
|
||||
- [x] `/opt/stacks/backup/.env` создан с DB passwords + kreknin creds + retention (chmod 600)
|
||||
- [x] `/opt/stacks/backup/scripts/run.sh` написан (bash + flock single-instance + ERR trap)
|
||||
- [x] DB dumps protocol: pg_dumpall, mariadb-dump --single-transaction, mongodump --ssl --sslAllowInvalidCertificates, redis-cli SAVE + docker cp
|
||||
- [x] rsync sources finalized: /opt/stacks/*, /etc/{ssh,ufw,hosts,docker}, /home/vitya/.ssh, $DUMP_DIR
|
||||
- [x] `/var/log/vds-backup/` log dir (pre-created via sudo + chown vitya)
|
||||
- [x] `/etc/cron.d/vds-backup` installed (root, `0 5 * * *`)
|
||||
- [x] apt install cron (Ubuntu 24.04 не имел его out-of-box); cron.service active + enabled
|
||||
- [x] Smoke run as root: 71,867 files / 11.74G на kreknin:/volume1/NetBackup/vds-kzntsv/2026-05-20, latest symlink set, ntfy publish 200, email Yandex 250 OK
|
||||
- [x] Permission errors из vitya-run выявлены и устранены переходом на root cron
|
||||
|
||||
## Notes
|
||||
|
||||
- Триггер: после `[[vds-kzntsv-bootstrap]]` Phase 3 + verdaccio/registry стабильны (done).
|
||||
- Integration с [[vds-ntfy-push]] — backup publish на topic `vds-backup` (admin auth).
|
||||
- Retention vs disk: kreknin 5.7T free, daily 7 × ~12G full ≈ 85G headroom (hardlinks делают incremental ~1G/day после day 1).
|
||||
- **acme.json** проходит через rsync (внутри /opt/stacks/traefik) — содержит LE private keys, но VDS↔kreknin link trusted, kreknin ACL restricted to vitya. Acceptable trade-off vs шифрование через restic.
|
||||
- **Atomic revert:** `ssh vitya@89.253.255.94 'sudo systemctl stop cron; sudo rm /etc/cron.d/vds-backup; sudo apt-get remove -y cron msmtp msmtp-mta; sudo rm -rf /etc/msmtprc /opt/stacks/backup /var/log/vds-backup'`
|
||||
53
.tasks/vds-gc-cron.md
Normal file
53
.tasks/vds-gc-cron.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# vds-gc-cron
|
||||
|
||||
## Goal
|
||||
|
||||
Weekly GC для двух container-сервисов на VDS, чтобы storage не разрастался:
|
||||
1. **Registry** (`/opt/stacks/registry/`) — `registry garbage-collect -m` против nested mount `./docker:/var/lib/registry` (host data в `/opt/stacks/registry/docker/docker/registry/v2/...`).
|
||||
2. **Verdaccio** (`/opt/stacks/verdaccio/`) — keep N=10 most-recent versions + dist-tagged per-package, drop ТОЛЬКО proxied tarballs (`_distfiles[tgzName]` defined). Локально-published versions (`@snollajs/*` + similar) защищены.
|
||||
|
||||
## Key files
|
||||
|
||||
- `/opt/stacks/gc/scripts/notify.sh` — ntfy helper, sources `/opt/stacks/ntfy/.env`, posts на `vds-ops` topic.
|
||||
- `/opt/stacks/gc/scripts/registry-gc.sh` — stop registry → offline `garbage-collect -m` → start; empty-storage guard в начале.
|
||||
- `/opt/stacks/gc/scripts/verdaccio-prune.sh` — wrapper, stop verdaccio → docker run `node:20-alpine` mounts storage + script → start.
|
||||
- `/opt/stacks/gc/scripts/verdaccio-prune.js` — ядро prune: walk storage, sort versions by `time[v]` desc, keep top-N + dist-tagged, delete tgz + `_attachments` entries for proxied versions, atomic JSON rewrite + `_rev` bump.
|
||||
- `/etc/cron.d/vds-gc` — Sunday 03:00 / 03:30 MSK.
|
||||
- `/var/log/vds-gc/{registry,verdaccio}-YYYY-MM-DD.log` — append-only logs.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **2026-05-21:** keep N=10 per package для verdaccio (включая `@snollajs/*` — uniform, no special-case scope). Reasoning: N=10 покрывает любые reasonable rollback scenarios, локально-published уже защищены логикой `_distfiles[tgzName]` absent.
|
||||
- **2026-05-21:** weekly Sunday 03:00 / 03:30 MSK (2 часа до 05:00 backup cron — нет конфликта). Reasoning: weekly достаточно для personal scale; verdaccio cache растёт ~MB/день не GB/день.
|
||||
- **2026-05-21:** registry GC = stop→`-m`→start (~30s downtime) instead of readonly+restart-twice. Reasoning: проще, более atomic; на personal registry 30s downtime в воскресенье ночью acceptable.
|
||||
- **2026-05-21:** verdaccio prune = stop+prune+start (~1min downtime). Reasoning: избегаем torn-read race window между tgz delete и package.json rewrite.
|
||||
- **2026-05-21:** verdaccio detection of locally-published — через `_distfiles[<tgzName>]` (НЕ `_distfiles[<version>]`). Initial code был bug — verdaccio keys `_distfiles` by tarball filename like `lodash-0.1.0.tgz`, не version string. Dry-run показал 4597 "locally-published" из 7021 — обнаружен через inspection lodash/react/`@snollajs` storage. Real run после fix: 73 версии legitimately protected.
|
||||
- **2026-05-21:** notify policy — success-low (`broom` tag, prio low) если freed < 10 MiB AND duration < 5 min; success-default иначе; fail-high (`x,boom` tag, prio high) на любой rc≠0; skip-min на empty registry.
|
||||
- **2026-05-21:** atomic package.json rewrite через `write tmp + rename` (POSIX atomic on same fs). Bump `_rev` (`<num+1>-<random_hex>`) чтобы npm clients не закешировали stale packument.
|
||||
- **2026-05-21:** verdaccio prune skip `versions{}` and `time{}` entries — оставляем metadata in-place даже для удалённых tarballs. Reasoning: npm может re-fetch tarball from upstream при следующем install request (proxy behaviour); metadata weighs ~KB не GB.
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] (post-первого live cron run) — phone-verify notification приходит на `vds-ops`.
|
||||
- [ ] Logrotate для `/var/log/vds-gc/` — пока nope (weekly cadence + лог ~10KB/run = 500KB/year, терпимо).
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] recon ntfy creds + verdaccio storage layout (package.json structure: `versions{}`, `_distfiles{}`, `_attachments{}`, `_rev`)
|
||||
- [x] write `notify.sh`, `registry-gc.sh`, `verdaccio-prune.sh`+`verdaccio-prune.js`
|
||||
- [x] dry-run verdaccio prune (initial bug discovery — `_distfiles[v]` vs `_distfiles[tgzName]`)
|
||||
- [x] fix detection → re-dry-run: 4524 tgz / 1.7 GiB candidate
|
||||
- [x] real verdaccio prune: 8.5G→6.8G, 1551 pkgs modified, 0 errors
|
||||
- [x] verify verdaccio responsive post-prune (lodash metadata 117 versions)
|
||||
- [x] push alpine v1+v2 to registry → test GC scenario
|
||||
- [x] delete manifest via DELETE API → re-run GC → freed 61M, registry restarted
|
||||
- [x] patch registry-gc.sh — empty-storage guard
|
||||
- [x] install `/etc/cron.d/vds-gc`, restart cron service
|
||||
|
||||
## Notes
|
||||
|
||||
- Verdaccio `_distfiles` keyed by tarball filename (`lodash-0.1.0.tgz`), не version. Gotcha — easy mistake.
|
||||
- Multi-arch image push через современный buildkit оставляет manifest как OCI index. Для DELETE через API нужно `Accept: application/vnd.oci.image.manifest.v1+json` (или index variants). Иначе registry возвращает 404.
|
||||
- `_attachments` field optional — на proxied packages обычно ~5-10 entries (только cached tarballs), на locally-published — все версии package.
|
||||
- `du -sh` (default block-size) vs `du -sb` (bytes) могут rounding-discrepancy на 5-10%. Used `du -sb` в скриптах для precise byte math.
|
||||
- registry compose mount `./docker:/var/lib/registry` → registry создаёт data в `/var/lib/registry/docker/registry/v2/...` → host path nested `/opt/stacks/registry/docker/docker/registry/v2/...`. Странно но работает.
|
||||
224
.tasks/vds-kzntsv-bootstrap.md
Normal file
224
.tasks/vds-kzntsv-bootstrap.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# vds-kzntsv-bootstrap
|
||||
|
||||
## Goal
|
||||
|
||||
Поднять облачный VDS `vds.kzntsv.site` (Rusonyx) и вынести туда ключевые инфраструктурные сервисы пользователя, чтобы не зависеть от собственного железа (мёртвая NAS показала single-point-of-failure). Цель — устранить риск повторения 2026-05-18 для **инфраструктурного** слоя (gitea/verdaccio/seafile/registry/hermes); production CMS (MoreThenCms) остаётся на [[windows-recovery-host]] и обсуждается отдельно в [[future-resilient-architecture-goals]].
|
||||
|
||||
После миграции [[kreknin-synology]] остаётся как backup target, не как live host инфры.
|
||||
|
||||
## Сервисы под миграцию (snapshot 2026-05-19)
|
||||
|
||||
Источник размеров — `du -sh /volume1/docker/*/` на kreknin (см. Decisions log 2026-05-19).
|
||||
|
||||
| Сервис | Сейчас на kreknin | После cleanup / на VDS | План |
|
||||
|---|---|---|---|
|
||||
| gitea | `/volume1/docker/gitea/` 2.6G | ~2.6G | lift-and-shift (compose + data tar) |
|
||||
| verdaccio | `/volume1/docker/personal/verdaccio/` 8.5G | 2–3G | prune old tarballs до миграции |
|
||||
| owncloud | `/volume1/docker/owncloud/` 187M + дубль 195M | — | **отказ**, заменяется seafile |
|
||||
| seafile | (новый) | 30–40G | green install на VDS |
|
||||
| registry | `/volume1/docker/infrastucture/registry/` 99G | 5–15G | **`registry garbage-collect` ДО миграции** (write op, требует read-only/stopped registry) |
|
||||
| hermes | `/volume1/docker/hermes/` 0 (пусто на kreknin) | ~10G (user estimate) | TBD — что это, где state |
|
||||
|
||||
Раскладка после cleanup: ~55–75G data + ~5G OS/docker → ~80–100G с headroom.
|
||||
|
||||
## Тариф VDS (Rusonyx, https://www.rusonyx.ru/hosting/vps/#ssd)
|
||||
|
||||
**Заказан (финал, user 2026-05-19): `160 NVMe`** (Rusonyx переименовал прежний `160 SSD` тариф в NVMe — апгрейд storage по той же цене, IOPS выше).
|
||||
|
||||
Конфигурация заказа:
|
||||
- 6 vCPU 2.6GHz
|
||||
- 8192 MiB RAM (8 GiB)
|
||||
- 163840 MiB disk (160 GiB) NVMe
|
||||
- Ubuntu Server 24.04
|
||||
- 1 IPv4 (free)
|
||||
- Лицензия / CMS / ispmanager — нет (docker-only, control-panel мусор не нужен)
|
||||
- VPS Backup от Rusonyx — 0 шт (свой backup через rsync/borg → kreknin или off-site cloud)
|
||||
- SSH root access — Вкл на старте (после bootstrap — отключить, sudo-user)
|
||||
|
||||
Reasoning:
|
||||
- 160GB headroom ~45% на старте после миграции (~60-70G data + 5G OS) — годен на 2-3 года без add-on operations.
|
||||
- 8GB RAM overkill для baseline (~2.1G) но даёт реальный запас под burst и любые будущие сервисы.
|
||||
- User выбрал «купи-и-забудь» вариант over 80 SSD+add-on; +1000 ₽/мес vs 80 SSD = +12k₽/год за operational simplicity.
|
||||
|
||||
**Не выбраны:**
|
||||
- 40 SSD — 4GB RAM впритык, OOM risk под seafile+registry burst.
|
||||
- 80 SSD — RAM ок, но 80GB диск требует add-on (цены непрозрачны без звонка в managers).
|
||||
- 220+ SSD — overkill без local LLM-inference.
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] **Hermes** — Nous Research agent runtime (https://hermes-agent.nousresearch.com/). Открытые подвопросы:
|
||||
- self-host их open-source / docker image, или client-wrapper к managed API?
|
||||
- если self-host — какой репо/образ, какие порты, какие external deps (vector DB, LLM endpoint)?
|
||||
- state где живёт (postgres / sqlite / files / vector store)?
|
||||
- 10G — это weights/embeddings/vector store/document cache?
|
||||
- влияет на RAM-выбор тарифа (если local LLM inference — 8GB мало, нужно 16+)
|
||||
- [x] ~~Owncloud дубль~~ — **live = `/volume1/docker/owncloud/`** (vitya:users, mysql активен 2026-05-20), stale = `/volume1/docker/personal/owncloud/` (alexey, mysql last May 5). Owncloud в scope этих 3 фаз не входит — user явно убрал в Phase 3.
|
||||
- [x] ~~DNS-cut стратегия~~ — DNS A-records проставлены user'ом 2026-05-20 сразу на VDS IP до bootstrap'а (`vds`, `*.vds`, `git`, `registry`, `verdaccio`). Сервисы на kreknin продолжают отвечать пока traefik там жив; cut фактический произойдёт когда remote DNS resolver кэш протухнет (≤ TTL).
|
||||
- [ ] Backup VDS → kreknin: какой механизм? rsnapshot / restic / borg / Hyper Backup pull через SFTP? (deferred — после Phase 3)
|
||||
- [x] ~~OS на VDS~~ → **Ubuntu 24.04 LTS** (decision 2026-05-19): LTS до апреля 2029, docker official APT repo flow, mainstream community для docker-стека, гарантированно есть в Rusonyx templates.
|
||||
|
||||
## Key files
|
||||
|
||||
(пока нет; появятся по ходу — compose-файлы, ansible-плейбук если будет, sync scripts)
|
||||
|
||||
## 🚨 Pre-Phase-1 — Rusonyx warning 2026-05-20
|
||||
|
||||
**`apt upgrade` Ubuntu 24.04 на Rusonyx виртуализации перезапускает SSH service. Делать ТОЛЬКО через VNC console Rusonyx-панели**, иначе SSH рвётся посередине, апгрейд бьётся.
|
||||
|
||||
Sequence (paste-ready в VNC console, root login):
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt upgrade -y
|
||||
apt --fix-broken install -y
|
||||
apt upgrade -y
|
||||
```
|
||||
|
||||
После reboot SSH повторно достижим (89.253.255.94, root, пароль из `vds-kzntsv.env`). Отсюда мой Phase 1 начинается.
|
||||
|
||||
## Connectivity (2026-05-20)
|
||||
|
||||
- IP: `89.253.255.94`
|
||||
- Vendor hostname: `vps-21075162-534388.host4g.ru`
|
||||
- DNS (REGRU, user 2026-05-20):
|
||||
- A `vds.kzntsv.site` → 89.253.255.94
|
||||
- A `*.vds.kzntsv.site` → 89.253.255.94
|
||||
- A `git.kzntsv.site` → 89.253.255.94
|
||||
- A `registry.kzntsv.site` → 89.253.255.94
|
||||
- A `verdaccio.kzntsv.site` → 89.253.255.94
|
||||
- Креды (root initial + sudo user vitya + portainer admin + LE email): `C:\Users\vitya\projects\.common\secrets\vds-kzntsv.env`. Root pass одноразовый, rotate'нется в Phase 1 (PasswordAuthentication off, SSH-key-only).
|
||||
|
||||
## Kreknin pre-migration findings (probe 2026-05-20)
|
||||
|
||||
SSH `vitya@195.19.90.188` через `id_ed25519_kreknin` ✅. Sudo требует пароль (пока не у меня).
|
||||
|
||||
| Сервис | Путь | Размер | DB | Compose live? |
|
||||
|---|---|---|---|---|
|
||||
| gitea | `/volume1/docker/gitea/` | data 2.6G + own postgres 9.6 dir | внутренний Postgres 9.6 (`gitea:gitea`) | ✅ `gitea` + `gitea-db` на сети `proxy` |
|
||||
| verdaccio | `/volume1/docker/personal/verdaccio/` | storage 8.5G + config 8K | нет | ✅ |
|
||||
| registry | `/volume1/docker/infrastucture/registry/` | **99G** | нет (auth + docker filesystem) | ✅ (compose-файл есть) |
|
||||
| owncloud-live | `/volume1/docker/owncloud/` | mysql активен 2026-05-20 | mysql + redis в одном compose | ✅ live (vitya:users owner) |
|
||||
| owncloud-stale | `/volume1/docker/personal/owncloud/` | mysql последний May 5 | mysql + redis | устарел |
|
||||
| hermes | `/volume1/docker/hermes/` | только `/data/` (uid 10000), нет compose | ? | ❓ конфиг где-то ещё (DSM Container Manager?) — open question |
|
||||
|
||||
## Phase 1 — Bootstrap (план, после VNC-upgrade)
|
||||
|
||||
🖥️ paste-ready, root@89.253.255.94 через SSH (после VNC-upgrade reboot)
|
||||
|
||||
1. `adduser --gecos "" --disabled-password vitya` + `echo 'vitya:Pryakhin10~' | chpasswd` + `usermod -aG sudo vitya`.
|
||||
2. `mkdir -p /home/vitya/.ssh && echo '<pubkey>' > /home/vitya/.ssh/authorized_keys && chmod 700 /home/vitya/.ssh && chmod 600 /home/vitya/.ssh/authorized_keys && chown -R vitya:vitya /home/vitya/.ssh`. Pubkey = `~/.ssh/id_ed25519.pub` с Windows-PC.
|
||||
3. Тест login `ssh vitya@89.253.255.94` отдельным окном **до** harden sshd (lockout-safety).
|
||||
4. Harden sshd: `sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/; s/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && systemctl reload sshd`.
|
||||
5. `apt install -y ufw fail2ban` + `ufw default deny incoming && ufw default allow outgoing && ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw enable`. fail2ban sshd jail дефолт-on.
|
||||
6. Docker official APT:
|
||||
```bash
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list
|
||||
apt update
|
||||
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
usermod -aG docker vitya
|
||||
```
|
||||
7. `mkdir -p /opt/stacks/{traefik,portainer,databases}`.
|
||||
8. Traefik compose (`traefik:v3.0`), HTTP-01 challenge (DNS уже на VDS), per-subdomain certs. Dashboard на `traefik.vds.kzntsv.site` с basicAuth `vitya:Pryakhin9` (htpasswd-hashed).
|
||||
9. Portainer compose (`portainer/portainer-ce:latest`), labels → `portainer.vds.kzntsv.site`. First-visit setup wizard: admin `vitya` / `vitya.kuznetsov@gmail.com` / `Pryakhin9`. Generate API key → дописать в `vds-kzntsv.env` `PORTAINER_API_KEY=...`. С этого момента я могу деплоить stacks через Portainer REST API.
|
||||
|
||||
**Traefik v2.11 vs v3.0 decision:** v3 mainline, breaking changes в middleware label syntax. v2.11 LTS до Q1 2027, совместим с существующими compose-стеками на windows-recovery-host. Recommend **v3.0** — VDS green install, нет legacy compose-файлов к pin'ить; за год v3 уже зрелый.
|
||||
|
||||
## Phase 2 — Shared DBs (план)
|
||||
|
||||
DB-park (Postgres 16, MariaDB 11, Redis 7, MongoDB? — см. Open questions). Каждый — отдельный compose в `/opt/stacks/databases/<engine>/`, на общей docker network `shared-dbs` (external). Admin GUI через Portainer (или отдельные образы adminer/pgadmin/redis-commander если нужно).
|
||||
|
||||
Exposure model — см. Open questions ниже.
|
||||
|
||||
## Phase 3 — Migration с kreknin
|
||||
|
||||
### 🚨 Источник данных — restored backup, НЕ live containers
|
||||
|
||||
Данные `/volume1/docker/{gitea,personal/verdaccio,infrastucture/registry}/` на [[kreknin-synology]] это **restored Hyper Backup** мёртвой [[dead-synology-diskstation]] (restore сессии 2026-05-18 вытащил `/docker` шару из `.hbk` репо). На kreknin сами эти сервисы **не запущены** — files сидят на disk. Поэтому миграция = чистый pull данных + standup на VDS, **без docker stop / pg_dump на kreknin**. Container'ы умерли вместе с DiskStation 2026-05-18.
|
||||
|
||||
Read first: `.wiki/concepts/hyper-backup-structure-and-recovery.md`, `.wiki/sources/nas-recovery-session-2026-05-18.md`, `.wiki/entities/kreknin-synology.md`, `.wiki/entities/dead-synology-diskstation.md`.
|
||||
|
||||
### 3.1 Gitea
|
||||
1. На kreknin (через sudo, перм issue на postgres dir): `tar c -C /volume1/docker/gitea . | ssh vds 'tar x -C /opt/migrate/gitea/'` — pulls `data/` + `postgres/` + `docker-compose.yml`. Hyper Backup restored файлы могут иметь Synology ACL + funky POSIX perms (см. [[hyper-backup-structure-and-recovery]] раздел ACL); rsync через root tar выровняет.
|
||||
2. На VDS: `chown -R 999:999 /opt/migrate/gitea/postgres && chmod 700 /opt/migrate/gitea/postgres` (postgres uid 999, требует strict 700 на datadir).
|
||||
3. На VDS: запустить **temporary** Postgres 9.6 на restored datadir → `docker run --rm -d --name pg96-temp -v /opt/migrate/gitea/postgres:/var/lib/postgresql/data postgres:9.6` (env `POSTGRES_USER=gitea POSTGRES_PASSWORD=gitea POSTGRES_DB=gitea`). Подождать `pg_isready`.
|
||||
4. На VDS: `docker exec pg96-temp pg_dump -U gitea gitea > /opt/migrate/gitea.sql` → ~80-200 MB SQL.
|
||||
5. На VDS shared postgres-16: `psql -U postgres -c 'CREATE DATABASE gitea OWNER gitea;'` (после создания юзера gitea в shared cluster) → `psql -U gitea -d gitea < /opt/migrate/gitea.sql`. Гитеа автоматически мигрирует schema под 16 (Gitea умеет).
|
||||
6. Stop temp pg96, удалить `/opt/migrate/gitea/postgres/` (больше не нужно — данные в shared postgres).
|
||||
7. На VDS Portainer stack:
|
||||
```yaml
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:1.25.5
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- DB_TYPE=postgres
|
||||
- DB_HOST=postgres:5432 # shared
|
||||
- DB_NAME=gitea
|
||||
- DB_USER=gitea
|
||||
- DB_PASSWD=gitea
|
||||
volumes:
|
||||
- /opt/stacks/gitea/data:/data
|
||||
networks: [proxy, shared-dbs]
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.gitea.rule=Host(`git.kzntsv.site`)
|
||||
- traefik.http.routers.gitea.entrypoints=websecure
|
||||
- traefik.http.routers.gitea.tls.certresolver=letsEncrypt
|
||||
- traefik.http.services.gitea.loadbalancer.server.port=3000
|
||||
```
|
||||
`data/` смонтировать с `/opt/migrate/gitea/data/` (или mv туда).
|
||||
8. Smoke: `git.kzntsv.site` → existing user login (data restored) → repo browse → clone test.
|
||||
9. Decommission на kreknin: ничего не делаем, файлы restored backup и так сидят паркингом на kreknin volume. Не трогать.
|
||||
|
||||
### 3.2 Verdaccio
|
||||
1. На kreknin → VDS: rsync (через `scp -O` или `tar | ssh`) `/volume1/docker/personal/verdaccio/{storage,config}/` → vds:/opt/stacks/verdaccio/.
|
||||
2. На VDS: chown под uid контейнера verdaccio (10001, см. verdaccio Dockerfile).
|
||||
3. **Pre-clean ПОСЛЕ rsync, на VDS** (не на kreknin — кreknin это backup-target read-only-bydefault): удалить старые tarball'ы > N версий из `storage/<scope>/<package>/` либо просто оставить как есть (8.5G нормально для 160 GB VDS).
|
||||
4. На VDS Portainer stack: image `verdaccio/verdaccio:6`, volumes `/opt/stacks/verdaccio/storage:/verdaccio/storage` и `config:/verdaccio/conf`, labels на `verdaccio.kzntsv.site`.
|
||||
5. Smoke: `npm publish` тест с windows-recovery-host против `verdaccio.kzntsv.site`.
|
||||
|
||||
### 3.3 Registry
|
||||
1. На kreknin: pre-clean GC доступен **без** running registry container — запустить temp registry:2 локально на kreknin против restored datadir: `sudo docker run --rm -v /volume1/docker/infrastucture/registry/docker:/var/lib/registry -v /volume1/docker/infrastucture/registry/config.yml:/etc/docker/registry/config.yml registry:2 garbage-collect /etc/docker/registry/config.yml`. **Write op on kreknin** — требует согласия user'а (правка backup'а на kreknin диске). Альтернатива: skip GC на kreknin → rsync 99G сетью → GC на VDS пост-фактум (медленнее по сети, безопаснее для kreknin).
|
||||
2. На kreknin → VDS: rsync `/volume1/docker/infrastucture/registry/{auth,docker,docker-compose.yml,config.yml}` → vds:/opt/stacks/registry/.
|
||||
3. На VDS Portainer stack: image `registry:2`, volumes для `auth` + `docker` (data) + `config.yml`, labels на `registry.kzntsv.site`. basicAuth middleware если в config задан htpasswd.
|
||||
4. Smoke: `docker login registry.kzntsv.site` + `docker pull` известный image.
|
||||
|
||||
### Open question по 3.3 GC
|
||||
- GC на kreknin (write-op на backup-target, ~30 мин обработка, экономит ~85G сетевого трафика и ~85G диска на VDS)
|
||||
- GC на VDS пост-rsync (читает 99G по сети — медленно при типичном Rusonyx ~50-100 Mbps, ~3-5 ч; не трогает kreknin)
|
||||
|
||||
Recommend **GC на kreknin** — backup-target, но `garbage-collect` registry:2 пересоберёт data IN-PLACE, не deletes; risk минимальный.
|
||||
|
||||
Связанные wiki-страницы (для контекста):
|
||||
- `.wiki/entities/kreknin-synology.md` — текущий host kreknin
|
||||
- `.wiki/concepts/future-resilient-architecture-goals.md` — глобальные цели resilience
|
||||
- `.wiki/concepts/recovery-architecture-snapshot.md` — текущая prod-инфра
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **2026-05-19:** OS на VDS = **Ubuntu 24.04 LTS** (Noble). Reasoning: LTS support до апреля 2029, docker official APT flow, mainstream community для docker-workload. Debian 12 отвергнут — не даёт material выгоды при 8GB RAM (idle разница ~100M), а docker-docs primary-target — Ubuntu LTS.
|
||||
- **2026-05-19 (заказан):** Тариф `160 NVMe` (Rusonyx переименовал 160 SSD → 160 NVMe, та же цена, апгрейд по IOPS). VPS Backup от Rusonyx — 0 шт (свой backup pipeline). SSH root — Вкл на bootstrap, потом отключить.
|
||||
- **2026-05-19 (final tier):** User выбрал **160 SSD (2500 ₽/мес)**. Reasoning user'а — operational simplicity, не звонить в support за add-on ценой, есть запас на 2-3 года. +1000 ₽/мес vs 80 SSD приемлемо.
|
||||
- **2026-05-19 (промежуточный заход, откатан):** Рекомендовалось 80 SSD + disk add-on исходя из того что 8GB RAM избыточно после уточнения hermes-профиля. User решил что 500 ₽/мес экономии не стоят звонков в support и риска что add-on окажется дорогой.
|
||||
- **2026-05-19 (первый заход):** Изначально 160 SSD по budget-расчёту headroom; затем переоценено вниз после уточнения hermes; затем user вернул обратно к 160 SSD по user-preference.
|
||||
- **2026-05-19:** Owncloud → seafile. Reasoning: пользователь решил заменить (rationale пока не записан).
|
||||
- **2026-05-19:** Registry **garbage-collect ДО миграции** (на kreknin), чтобы не тащить 99G чтобы потом всё равно чистить.
|
||||
- **2026-05-19:** Verdaccio тоже prune ДО миграции, по аналогичной логике.
|
||||
|
||||
## Next actions (по порядку)
|
||||
|
||||
1. **Уточнить hermes** — что это, где его state.
|
||||
2. **Решить owncloud дубль** — нужны ли данные для миграции в seafile или green install.
|
||||
3. **Registry GC на kreknin** — stop писателей, `docker run --rm -v ...:/var/lib/registry registry:2 garbage-collect /etc/docker/registry/config.yml`, measure delta. ⚠️ prod-changing, согласие от user.
|
||||
4. **Verdaccio prune** — `npm cache clean` + удалить old tarballs из storage. (low risk, restorable из upstream npm)
|
||||
5. ~~**Закупить тариф у Rusonyx**~~ → **заказан 160 NVMe, Ubuntu 24.04, 8GB/6vCPU/160GB, 1 IPv4** (2026-05-19, ожидание выделения IP).
|
||||
6. **DNS** — A `vds.kzntsv.site → <IP>` в REGRU.
|
||||
7. **Bootstrap VDS** (zero-day чек-лист): apt update/upgrade → создать sudo-user `vitya` + копировать SSH key → `PermitRootLogin no` + `PasswordAuthentication no` → ufw (22/80/443 only) → fail2ban → docker official APT repo (docker-ce + buildx + compose-plugin). Полная paste-ready команда в чате сессии.
|
||||
8. **Per-service migration** (gitea → tarball+restore, verdaccio → tar storage, seafile → green install, registry → rsync, hermes → repo+state).
|
||||
9. **Backup pipeline** VDS → kreknin (или cloud — Backblaze B2 для off-site, см. [[future-resilient-architecture-goals]]).
|
||||
10. **DNS cut** к production, удаление сервисов с kreknin (с paranoid keep-on-kreknin-7days).
|
||||
99
.tasks/vds-ntfy-push.md
Normal file
99
.tasks/vds-ntfy-push.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# vds-ntfy-push
|
||||
|
||||
## Goal
|
||||
|
||||
Self-host [ntfy.sh](https://ntfy.sh) на VDS для push-нотификаций на телефон. Docker-image `binwiederhier/ntfy`. Использовать для:
|
||||
- [[vds-backup-rsync-kreknin]] backup status (вместо/параллельно email на vitya.kuznetsov@gmail.com)
|
||||
- Monitoring alerts (CMS down, traefik certs истекают, disk fill, registry GC fail, etc.)
|
||||
- Любые ad-hoc уведомления от ops-скриптов
|
||||
|
||||
Domain: `ntfy.vds.kzntsv.site` (под уже существующим wildcard `*.vds.kzntsv.site`).
|
||||
|
||||
## Open questions
|
||||
|
||||
- [x] ~~**Auth model:**~~ per-topic ACL + admin user `vitya` (resolved 2026-05-20). Реализация: `NTFY_AUTH_DEFAULT_ACCESS=deny-all` + `ntfy user add --role=admin vitya`. Admin role → rw to all topics, per-topic ACL не нужен. Anon (no creds) → 403 на publish и subscribe ✅.
|
||||
- [x] ~~**Топики:**~~ начали с двух: `vds-backup` (для vds-backup-rsync-kreknin) + `vds-ops` (general alerts: cert expiry, disk fill, container crashes). Можно добавлять без миграции — admin role даёт доступ ко всему автоматом.
|
||||
- [x] ~~**Phone app:**~~ Android (resolved 2026-05-20) → ntfy Android app, pure self-host без APNs middleware.
|
||||
- [x] ~~**Persistence:**~~ sqlite в `/opt/stacks/ntfy/data/` (cache.db + auth.db). Volume mount `./data:/var/cache/ntfy`.
|
||||
- [x] ~~**Phone-side smoke:**~~ confirmed 2026-05-20 22:54 MSK — оба push'а («Topic renamed», «Password updated») пришли в Android ntfy app, screenshot в conversation.
|
||||
|
||||
## Implementation sketch
|
||||
|
||||
`/opt/stacks/ntfy/docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ntfy:
|
||||
image: binwiederhier/ntfy
|
||||
container_name: ntfy
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- serve
|
||||
networks:
|
||||
- proxy
|
||||
volumes:
|
||||
- ./data:/var/cache/ntfy
|
||||
- ./etc:/etc/ntfy
|
||||
environment:
|
||||
NTFY_BASE_URL: https://ntfy.vds.kzntsv.site
|
||||
NTFY_CACHE_FILE: /var/cache/ntfy/cache.db
|
||||
NTFY_AUTH_FILE: /var/cache/ntfy/auth.db
|
||||
NTFY_AUTH_DEFAULT_ACCESS: deny-all
|
||||
NTFY_BEHIND_PROXY: true
|
||||
NTFY_ATTACHMENT_CACHE_DIR: /var/cache/ntfy/attachments
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.ntfy.rule=Host(`ntfy.vds.kzntsv.site`)
|
||||
- traefik.http.routers.ntfy.entrypoints=websecure
|
||||
- traefik.http.routers.ntfy.tls.certresolver=letsEncrypt
|
||||
- traefik.http.services.ntfy.loadbalancer.server.port=80
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
```
|
||||
|
||||
После up — `docker exec ntfy ntfy user add --role=admin vitya` → задать пароль → грант access на топики.
|
||||
|
||||
Publish from any script:
|
||||
```bash
|
||||
curl -u "vitya:$NTFY_PASS" \
|
||||
-H "Title: VDS backup completed" \
|
||||
-H "Tags: white_check_mark" \
|
||||
-H "Priority: default" \
|
||||
-d "Size 12.5G, 18m12s, kreknin OK" \
|
||||
https://ntfy.vds.kzntsv.site/vds-backup
|
||||
```
|
||||
|
||||
Phone subscribes to `https://ntfy.vds.kzntsv.site/vds-backup` (с basic auth) — получает уведомление.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **2026-05-20** — Image pinned `binwiederhier/ntfy:v2.11.0` (conservative, matches pin policy остальных стэков: traefik v2.11, portainer 2.21.5).
|
||||
- **2026-05-20** — Auth: `NTFY_AUTH_DEFAULT_ACCESS=deny-all` + admin user `vitya` (random hex16 password). Admin role гранит rw ко всем топикам автоматом — per-topic `ntfy access` calls returned `user vitya is an admin user, access control entries have no effect`. Не нужны явные grants; разлогиниться от admin позже если будет потребность в ограниченных read-only users.
|
||||
- **2026-05-20** — Topics: `vds-backup` + `vds-ops` (минимум для текущих нужд). Расширение бесплатное (admin → rw to all).
|
||||
- **2026-05-20** — TLS через traefik LE http-01 (issuer R12, valid Aug 18 2026); ntfy listens HTTP `:80` внутри `proxy` network, `NTFY_BEHIND_PROXY=true` чтобы trust X-Forwarded-* от traefik.
|
||||
- **2026-05-20** — Smoke green: anon publish/subscribe → 403, authed publish vds-backup+vds-ops → 200 с JSON id/ts.
|
||||
- **2026-05-20** — User feedback: random hex password неудобен для ввода в phone app. Меняю на `Pryakhin9` (consistency с TRAEFIK_DASHBOARD_PASS). Trade-off: меньше entropy, но scope ограничен (admin role, single user, push-сервис без чувствительных данных, can rotate если threat model изменится). Lesson — для human-input creds (phone/web UI) использовать память user'а, не random hex; random hex оставить для script-only (DB, API keys).
|
||||
- **2026-05-20** — Topics renamed `backup` → `vds-backup`, `ops` → `vds-ops` (consistency с VDS scope, упрощает filtering если позже добавятся nas-* или kreknin-* топики).
|
||||
- **2026-05-20** — Phone-verified: Android ntfy app subscription → оба push'а пришли в течение секунд. End-to-end complete.
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] `/opt/stacks/ntfy/{data}` создано (owner vitya:vitya)
|
||||
- [x] `docker-compose.yml` написан с traefik labels (Host, websecure, letsEncrypt, port 80)
|
||||
- [x] `docker compose up -d` → container healthy, listening :80
|
||||
- [x] Admin user `vitya` добавлен через `printf "%s\n%s\n" "$PASS" "$PASS" | docker exec -i ntfy ntfy user add --role=admin vitya`
|
||||
- [x] LE cert issued via http-01, R12, valid 2026-05-20 → 2026-08-18
|
||||
- [x] Smoke test (publish/subscribe/anon-deny) green
|
||||
- [x] Creds saved: `/opt/stacks/ntfy/.env` (chmod 600) + Windows `~/projects/.common/secrets/vds-kzntsv.env`
|
||||
- [x] Password rotated random-hex → `Pryakhin9` per user request (ntfy user change-pass)
|
||||
- [x] Topics renamed `backup`/`ops` → `vds-backup`/`vds-ops`
|
||||
- [x] Phone-verified (Android ntfy app, screenshot 22:54 MSK)
|
||||
|
||||
## Notes
|
||||
|
||||
- Триггер: после `[[vds-kzntsv-bootstrap]]` Phase 3 и до или одновременно с `[[vds-backup-rsync-kreknin]]` (backup pipeline хочет ntfy для статус-уведомлений).
|
||||
- Интегрируется с `[[vds-backup-rsync-kreknin]]` — backup script публикует через ntfy + дублируется email'ом для надёжности.
|
||||
- **Android setup hint:** ntfy app → Add subscription → Topic `vds-backup` (или `vds-ops`) → Server `https://ntfy.vds.kzntsv.site` → enable «Use auth» → user `vitya` / password из vds-kzntsv.env.
|
||||
- Cert renewal — automatic via traefik (letsEncrypt resolver), нет дополнительных шагов.
|
||||
116
.wiki/concepts/compose-bcrypt-escape-trap.md
Normal file
116
.wiki/concepts/compose-bcrypt-escape-trap.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Docker Compose bcrypt `$` escape trap
|
||||
type: concept
|
||||
tags: [docker-compose, bcrypt, password, escape, gotcha]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
updated: 2026-05-20
|
||||
---
|
||||
|
||||
# Docker Compose ест `$` в bcrypt hashes
|
||||
|
||||
## Симптом
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
command: --admin-password '$2y$05$abc123...'
|
||||
```
|
||||
|
||||
При `docker compose up` Compose выдаёт warning:
|
||||
```
|
||||
warning msg="The \"r9BLFM98iCLTskjgj7Y3teOACut3Nxk\" variable is not set. Defaulting to a blank string."
|
||||
```
|
||||
|
||||
И в контейнер передаётся пустая строка вместо bcrypt hash.
|
||||
|
||||
## Root cause
|
||||
|
||||
Compose interpolates `${VAR}` и `$VAR` syntax из env variables **в YAML values**. Bcrypt hash начинается с `$2y$` или `$2a$` — выглядит как `$NAME` шаблоны для compose'а:
|
||||
|
||||
- `$2y` → попытка expand env var `2y` → не определена → empty string
|
||||
- `$05` → expand env var `05` → empty string
|
||||
- Остаток до точки/конца строки → имя var → empty
|
||||
- Точки/слэши прерывают имя var
|
||||
|
||||
Результат — обрезанный/пустой hash.
|
||||
|
||||
## Решение 1 — double-escape `$` → `$$`
|
||||
|
||||
В YAML literal compose treats `$$` как escape для `$`. Нужно sed-replace перед записью:
|
||||
|
||||
```bash
|
||||
BCRYPT=$(htpasswd -nbB user pass | cut -d':' -f2)
|
||||
BCRYPT_ESCAPED=$(echo "$BCRYPT" | sed 's/\$/\$\$/g')
|
||||
cat > docker-compose.yml <<COMPOSE
|
||||
services:
|
||||
app:
|
||||
command: --admin-password '$BCRYPT_ESCAPED'
|
||||
COMPOSE
|
||||
```
|
||||
|
||||
`docker compose config` покажет `$$2y$$05$$...` — это нормально, в runtime expand'ится в `$2y$05$...`.
|
||||
|
||||
**Но осторожно с кавычками**: YAML string form `command: "... '<hash>'"` — после tokenization shell видит **literal** одинарные кавычки внутри значения. Argument приходит как `'$2y$05$...'` (с кавычками вокруг hash) → bcrypt verify fails из-за паразитных `'` char'ов.
|
||||
|
||||
## Решение 2 — YAML list form
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- "--admin-password"
|
||||
- "$$2y$$05$$abc123..."
|
||||
```
|
||||
|
||||
Каждый list-element — отдельный argv element, без shell tokenization. Кавычки не утекают.
|
||||
|
||||
**Но опять же** — некоторые версии Compose (2024+) могут иметь регрессии где YAML list form **не unescape'ит** `$$` обратно в `$`. Проверять через `docker compose config | grep command` после написания.
|
||||
|
||||
## Решение 3 — `docker run` напрямую, без compose
|
||||
|
||||
Bypass проблему. Shell escape'ы (одинарные кавычки) предсказуемы:
|
||||
|
||||
```bash
|
||||
BCRYPT=$(htpasswd -nbB user pass | cut -d':' -f2)
|
||||
docker run -d --name app \
|
||||
-v ... \
|
||||
app/app:tag \
|
||||
--admin-password "$BCRYPT" # bash interpolation одного слоя
|
||||
```
|
||||
|
||||
Bash `"$BCRYPT"` expand'ится один раз. Внутри expansion'а `$` уже не парсится. Container получает чистый hash.
|
||||
|
||||
Trade-off: лишаемся compose файла → нет declarative restart, нужно `docker run --restart unless-stopped`. Управляемо.
|
||||
|
||||
## Решение 4 — env file with single var
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
env_file: ./bcrypt.env
|
||||
command: --admin-password ${BCRYPT}
|
||||
```
|
||||
|
||||
`bcrypt.env`:
|
||||
```
|
||||
BCRYPT=$2y$05$abc123...
|
||||
```
|
||||
|
||||
Env file читается raw, без compose interpolation. Тогда `${BCRYPT}` в command expand'ится из этого env. Работает.
|
||||
|
||||
Но менее transparent — debugger должен смотреть в env file.
|
||||
|
||||
## Похожие traps
|
||||
|
||||
Любые значения с `$`:
|
||||
- Postgres password `secret$pass$word`
|
||||
- API keys с `$` (rare)
|
||||
- Cron syntax в command (`$1` etc.)
|
||||
- Bash variable substitutions внутри command
|
||||
|
||||
## Где применено
|
||||
|
||||
`portainer/portainer-ce:2.21.5` на [`vds-kzntsv`](../entities/vds-kzntsv.md) — финальный путь = решение 3 (`docker run` direct). Хотя в этой же сессии оказалось что `--admin-password` flag broken в Portainer 2.21+ regardless escape — см. [`portainer-2.21-admin-password-regression`](portainer-2.21-admin-password-regression.md).
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Compose docs: [Variable interpolation](https://docs.docker.com/compose/compose-file/12-interpolation/)
|
||||
- Сессия: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 1 cont.
|
||||
147
.wiki/concepts/db-tls-self-signed-via-traefik-raw-tcp.md
Normal file
147
.wiki/concepts/db-tls-self-signed-via-traefik-raw-tcp.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: DB TLS через traefik raw TCP с self-signed certs
|
||||
type: concept
|
||||
tags: [traefik, tls, postgres, mariadb, mongo, redis, self-signed, pattern]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
updated: 2026-05-20
|
||||
---
|
||||
|
||||
# Self-signed DB TLS через Traefik raw TCP
|
||||
|
||||
Pattern для выставления нескольких DB-контейнеров наружу (public internet) через traefik с минимумом сложности cert management. Self-signed на старте, LE-extraction позже.
|
||||
|
||||
Сочетается с [`traefik-tcp-passthrough-vs-starttls`](traefik-tcp-passthrough-vs-starttls.md) — там объяснено почему `HostSNI(*)` + raw TCP, не SNI passthrough.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
Internet client
|
||||
↓ TLS handshake (порт зависит от DB)
|
||||
↓ <db>.vds.kzntsv.site:5432/3306/27017/6379
|
||||
Traefik (raw TCP forward, без TLS inspection)
|
||||
↓ docker network `proxy`
|
||||
DB container (терминирует TLS своим self-signed cert)
|
||||
↓ docker network `shared-dbs`
|
||||
Other VDS containers (могут ходить без TLS по dns name `postgres`/`mariadb`/...)
|
||||
```
|
||||
|
||||
Каждая DB:
|
||||
- Подключена к двум networks: `proxy` (для traefik) и `shared-dbs` (для других контейнеров VDS).
|
||||
- Имеет свой self-signed cert (CN = `<db>.vds.kzntsv.site`) в `./certs/`.
|
||||
- Конфигурируется для TLS-required.
|
||||
- Объявляет traefik label с `HostSNI(\`*\`)` на dedicated entrypoint port.
|
||||
|
||||
Traefik static config:
|
||||
```yaml
|
||||
entryPoints:
|
||||
postgres: { address: ":5432" }
|
||||
mariadb: { address: ":3306" }
|
||||
mongo: { address: ":27017" }
|
||||
redis: { address: ":6379" }
|
||||
```
|
||||
|
||||
ufw: allow на эти 4 порта.
|
||||
|
||||
## Cert generation
|
||||
|
||||
```bash
|
||||
for db in postgres mariadb mongo redis; do
|
||||
openssl req -x509 -newkey rsa:2048 \
|
||||
-keyout /opt/stacks/databases/$db/certs/server.key \
|
||||
-out /opt/stacks/databases/$db/certs/server.crt \
|
||||
-days 3650 -nodes \
|
||||
-subj "/CN=$db.vds.kzntsv.site/O=kzntsv.site/C=RU" \
|
||||
-addext "subjectAltName=DNS:$db.vds.kzntsv.site"
|
||||
chmod 600 /opt/stacks/databases/$db/certs/server.key
|
||||
done
|
||||
```
|
||||
|
||||
## DB-specific quirks
|
||||
|
||||
### Postgres 16 (non-alpine, uid 999)
|
||||
|
||||
Postgres key file перм-checks: must be 600 + owned by postgres user OR root. Postgres alpine использует **uid 70**, не 999. Не-alpine Debian — **uid 999**. Если используем alpine — `chown -R 70:70 certs/server.key`; если debian — `chown 999:999`. Лучше debian (`postgres:16`) для совместимости с прочими стеками. Команда:
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- ssl=on
|
||||
- -c
|
||||
- ssl_cert_file=/etc/postgres-certs/server.crt
|
||||
- -c
|
||||
- ssl_key_file=/etc/postgres-certs/server.key
|
||||
```
|
||||
|
||||
### MariaDB 11.4
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- --ssl-cert=/etc/mariadb-certs/server.crt
|
||||
- --ssl-key=/etc/mariadb-certs/server.key
|
||||
- --require-secure-transport=ON # форсит TLS для всех клиентов
|
||||
```
|
||||
|
||||
### MongoDB 7
|
||||
|
||||
Cert + key должны быть **в одном PEM-файле** (`cat server.crt server.key > server.pem`). Плюс Mongo 7 enforce'ит "chain of trust" — нужен `--tlsCAFile` (для self-signed — указываем server.crt сам как CA). `--tlsAllowConnectionsWithoutCertificates` нужен иначе сервер требует client cert.
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- --tlsMode=requireTLS
|
||||
- --tlsCertificateKeyFile=/etc/mongo-certs/server.pem
|
||||
- --tlsCAFile=/etc/mongo-certs/server.crt
|
||||
- --tlsAllowConnectionsWithoutCertificates
|
||||
- --bind_ip_all
|
||||
```
|
||||
|
||||
### Redis 7 alpine
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- redis-server
|
||||
- --port
|
||||
- "0" # disable non-TLS port
|
||||
- --tls-port
|
||||
- "6379"
|
||||
- --tls-cert-file
|
||||
- /etc/redis-certs/server.crt
|
||||
- --tls-key-file
|
||||
- /etc/redis-certs/server.key
|
||||
- --tls-auth-clients
|
||||
- "no" # без mTLS
|
||||
- --requirepass
|
||||
- <password>
|
||||
```
|
||||
|
||||
## Client connection examples
|
||||
|
||||
```bash
|
||||
# Postgres (sslmode=require, не verify-full — self-signed)
|
||||
psql "postgresql://postgres:$PG_PASS@postgres.vds.kzntsv.site:5432/postgres?sslmode=require"
|
||||
|
||||
# MariaDB (--ssl + --ssl-verify-server-cert=0)
|
||||
mariadb -h mariadb.vds.kzntsv.site -P 3306 -u root -p$MARIA_PASS --ssl --ssl-verify-server-cert=0
|
||||
|
||||
# Mongo (tls=true + tlsAllowInvalidCertificates)
|
||||
mongosh "mongodb://root:$MONGO_PASS@mongo.vds.kzntsv.site:27017/admin?tls=true&tlsAllowInvalidCertificates=true"
|
||||
|
||||
# Redis (--tls --insecure)
|
||||
redis-cli --tls --insecure -h redis.vds.kzntsv.site -p 6379 -a $REDIS_PASS
|
||||
```
|
||||
|
||||
## Trade-off
|
||||
|
||||
- ✔ Уровень входа: 5 минут на cert + 5 минут на compose. Никаких lego/cert-manager headache на старте.
|
||||
- ✔ Каждый клиент явно отключает verify — predictable failure mode (если cert меняется случайно — клиент сразу пишет в логи).
|
||||
- ✗ Клиенты должны помнить `verify=disable`. Production-tier клиенты обычно фейлят на self-signed по defaultу.
|
||||
- ✗ Cert не ротируется автоматически. 10-year validity = временное обходное.
|
||||
- ✗ Для каждой DB — отдельный cert (не wildcard). Расширяемо, но если будет mongo + mongo-readonly — у каждого свой.
|
||||
|
||||
## Roadmap к LE certs
|
||||
|
||||
Sidecar контейнер с lego (готовый container `goacme/lego` или собственный) который watch'ит traefik `acme.json` и каждый раз когда меняется (i.e. cert ротировался) — извлекает PEM-bundle на disk + триггерит `docker exec <db> kill -HUP 1` для reload. Каждый DB получает auto-renewed LE cert. Это deferred — см. follow-up task в [`vds-kzntsv`](../entities/vds-kzntsv.md).
|
||||
|
||||
## Где применено
|
||||
|
||||
4 shared DBs на [`vds-kzntsv`](../entities/vds-kzntsv.md): postgres / mariadb / mongo / redis. Self-signed 10-year certs в `/opt/stacks/databases/<engine>/certs/`. Strong random hex32 пароли в `~/projects/.common/secrets/vds-kzntsv.env`.
|
||||
128
.wiki/concepts/docker-host-loopback-detect.md
Normal file
128
.wiki/concepts/docker-host-loopback-detect.md
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: Docker host-loopback detection — как доказать что host.docker.internal:N не петля в traefik
|
||||
type: concept
|
||||
tags: [docker, traefik, debugging, recipe, gotcha, networking]
|
||||
sources: [../sources/iis-host-migration-2026-05-19.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Docker host-loopback detection
|
||||
|
||||
Конкретная техника **доказать что `host.docker.internal:<port>` внутри docker-контейнера действительно резолвится в host-уровневый процесс**, а не возвращается обратно в тот же docker-контейнер (Docker Desktop NAT loopback). Эта проверка обязательна **прежде traefik patch'a** на host-backend, иначе можно повторить incident attempt 1 из [[iis-migration-2026-05-19-postmortem]] (traefik backend `host.docker.internal:80` → Docker NAT loop в собственный HTTP entrypoint → 301 от http-catchall middleware → TOO_MANY_REDIRECTS).
|
||||
|
||||
## Когда применять
|
||||
|
||||
- Любой раз когда **traefik** (или другой reverse-proxy в docker container) должен идти **на host** (нативный IIS, nginx, Postgres, etc.).
|
||||
- Особенно когда backend port **совпадает** с одним из traefik publish-ports или потенциально может маршрутизироваться обратно в traefik через Docker Desktop port-mapping.
|
||||
|
||||
## Recipe
|
||||
|
||||
### Шаг 1 — выбрать backend port вне traefik publish-set
|
||||
|
||||
Запомни traefik publish-ports — это **запретный список** для backend. Например при наших traefik publish'ах `:8000, :4443, :8080` — backend `:80` тоже опасен (Docker Desktop NAT loopback creates implicit mapping в нижележащих случаях).
|
||||
|
||||
Безопасные porter — те что **никем другим в docker не публикуются** и **не совпадают** с traefik publish-set. Прежде выбора — `Test-NetConnection -Port N` на host, чтобы убедиться нет другого listener'а.
|
||||
|
||||
### Шаг 2 — IIS binding и Stop/Start
|
||||
|
||||
```powershell
|
||||
# 🖥️ ELEVATED PowerShell on Windows host
|
||||
Import-Module WebAdministration
|
||||
$site = 'snolla'; $port = 8089
|
||||
New-WebBinding -Name $site -Protocol http -Port $port -IPAddress '*'
|
||||
Stop-Website -Name $site
|
||||
Start-Website -Name $site # ← КРИТИЧНО, без restart binding не активируется
|
||||
Get-NetTCPConnection -LocalPort $port -State Listen # verify
|
||||
```
|
||||
|
||||
### Шаг 3 — probe изнутри traefik container (НЕ через локальный curl)
|
||||
|
||||
**КЛЮЧЕВОЕ:** probe должен идти **изнутри docker-контейнера**, чтобы воспроизвести точно тот же путь что traefik будет использовать.
|
||||
|
||||
```powershell
|
||||
# 💻 Windows host (non-elevated)
|
||||
docker exec traefik wget --spider -S --header="Host: <domain>" "http://host.docker.internal:8089/" 2>&1 | Select-Object -First 10
|
||||
```
|
||||
|
||||
`--spider` = HEAD-only, не follow redirects. `-S` = show response headers. Без этих флагов wget может **уйти follow public DNS → router → traefik → backend** → искусственная петля через интернет (не имеет отношения к Docker NAT).
|
||||
|
||||
### Шаг 4 — интерпретировать
|
||||
|
||||
**Хороший знак (traefik найдёт host IIS):**
|
||||
|
||||
```
|
||||
Connecting to host.docker.internal:8089 (192.168.65.254:8089)
|
||||
HTTP/1.1 200 OK
|
||||
Server: Microsoft-IIS/10.0 ← НАШ IIS отвечает
|
||||
X-Powered-By: ASP.NET
|
||||
Content-Length: 28413
|
||||
```
|
||||
|
||||
Признаки:
|
||||
- IP-резолв `host.docker.internal` = **192.168.65.254** (Docker Desktop host gateway, может быть другой адрес в зависимости от версии).
|
||||
- `Server: Microsoft-IIS/10.0` ⇒ это IIS, не traefik.
|
||||
- `X-Powered-By: ASP.NET` ⇒ ASP.NET runtime обработал.
|
||||
- Content-Length ≠ 17 (traefik «Moved Permanently» body длиной 17 = подозрительный знак, см. ниже).
|
||||
|
||||
**Плохой знак (Docker NAT loopback):**
|
||||
|
||||
```
|
||||
HTTP/1.1 301 Moved Permanently
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Length: 17 ← traefik signature (= "Moved Permanently\n")
|
||||
Location: https://<host>/ ← redirect-to-https middleware
|
||||
```
|
||||
|
||||
Признаки:
|
||||
- **Нет** `Server: Microsoft-IIS/10.0` (или `Server: traefik`).
|
||||
- `Content-Length: 17` — общая длина text/plain "Moved Permanently".
|
||||
- 301 на `https://<входной-host>/` — это traefik http-catchall middleware (см. `https.yml` `redirect-to-https`).
|
||||
|
||||
Если видишь второй паттерн — **STOP**. Backend port пересекается с traefik. Не делай traefik patch, выбери другой port.
|
||||
|
||||
### Шаг 5 — public-path verification
|
||||
|
||||
После step 4 — проверь по реальному пути client → traefik → backend:
|
||||
|
||||
```powershell
|
||||
# 💻 Windows host
|
||||
curl.exe -k -sS -I --max-redirs 0 -m 10 -H "Host: <domain>" "https://localhost:4443/"
|
||||
```
|
||||
|
||||
Должно быть first response = `Server: Microsoft-IIS/10.0` (либо CMS canonical 301 от IIS — main thing — Server header указывает на IIS, не на traefik).
|
||||
|
||||
## Pitfall — WinHTTP proxy на хосте strip's headers
|
||||
|
||||
Локальный `curl.exe http://localhost:<host-port>/` на Windows может ходить **через WinHTTP system proxy** который **strips `Server` / `X-Powered-By` headers** на response (плюс часто добавляет `Proxy-Connection: keep-alive`). Это даёт ложное впечатление «не IIS отвечает», хотя на самом деле IIS работает корректно.
|
||||
|
||||
**Признаки** что probe идёт через WinHTTP proxy:
|
||||
- Нет `Server:` в response, но контент корректный (например HTML страница сайта).
|
||||
- `Proxy-Connection: keep-alive` в response.
|
||||
- Headers выглядят «обрезанными».
|
||||
|
||||
**Решение:** для loop-detect / IIS-confirmation тестов используй **`docker exec traefik wget`** (изнутри docker, минует Windows-уровневые proxy) или **`Invoke-WebRequest` через PS** с `-Proxy ''`. НЕ используй `curl.exe` как единственный источник truth для headers.
|
||||
|
||||
## Подтверждённый рабочий пример (2026-05-19 attempt 2)
|
||||
|
||||
```
|
||||
docker exec traefik wget --spider -S --header="Host: emspb.ru" "http://host.docker.internal:8089/"
|
||||
→ Connecting to host.docker.internal:8089 (192.168.65.254:8089)
|
||||
→ HTTP/1.1 301 Moved Permanently
|
||||
Server: Microsoft-IIS/10.0
|
||||
X-Powered-By: ASP.NET
|
||||
Location: http://www.emspb.ru/
|
||||
|
||||
docker exec traefik wget --spider -S --header="Host: localhost:8089" "http://host.docker.internal:8089/"
|
||||
→ HTTP/1.1 200 OK
|
||||
Server: Microsoft-IIS/10.0
|
||||
X-Powered-By: ASP.NET
|
||||
Content-Length: 28413
|
||||
```
|
||||
|
||||
Заключение: `host.docker.internal:8089` → `192.168.65.254:8089` (Docker Desktop host gateway) → host IIS. **NO Docker NAT loop.** 301 — это CMS canonical (CMS-side www-redirect), не traefik http-catchall (тот бы дал `Content-Length: 17` text/plain без `Server: Microsoft-IIS/10.0`).
|
||||
|
||||
## Связано
|
||||
|
||||
- [[iis-migration-2026-05-19-postmortem]] — почему attempt 1 сломался (этот же loop, но с `:80` который пересекался с docker-NAT mapping).
|
||||
- [[traefik-on-windows-docker-desktop]] — общие traefik pitfalls на Docker Desktop.
|
||||
- [[iis-host-migration-2026-05-19]] Phase 10 — где этот recipe применён первый раз и подтверждён.
|
||||
111
.wiki/concepts/future-resilient-architecture-goals.md
Normal file
111
.wiki/concepts/future-resilient-architecture-goals.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Future Resilient Architecture — цели на потом
|
||||
type: concept
|
||||
tags: [planning, architecture, resilience, roadmap, todo]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Future Resilient Architecture
|
||||
|
||||
Placeholder для глобальной задачи "выстроить отказоустойчивую архитектуру" — обсуждается **после** того как recovery полностью устаканится.
|
||||
|
||||
Эта страница — **черновик целей**, не план. По ходу обсуждения распилится на специфичные концепты + ADR (architectural decision records).
|
||||
|
||||
## Что сейчас не так
|
||||
|
||||
См. [[recovery-architecture-snapshot]] → раздел "Single Points of Failure". Кратко:
|
||||
|
||||
- ⚠️ Windows-PC = SPOF
|
||||
- ⚠️ Один public IP
|
||||
- ⚠️ Один MSSQL primary без replica
|
||||
- ⚠️ Один MinIO single-node
|
||||
- ⚠️ Backup только Hyper Backup на одну синку (Kreknin), сейчас она cold
|
||||
- ⚠️ Backup нового рабочего состояния (CMS data на recovery-host) **отсутствует**
|
||||
- ⚠️ Нет off-site backup (cloud)
|
||||
|
||||
## Цели верхнего уровня
|
||||
|
||||
1. **RTO** (Recovery Time Objective) — сколько максимум **downtime** клиенты должны видеть при отказе.
|
||||
- Текущий по факту: ~15 часов (что и было).
|
||||
- Целевой: **< 1 час** для одиночного отказа, **< 4 часов** для каскадного.
|
||||
2. **RPO** (Recovery Point Objective) — сколько максимум **данных потерять** при отказе.
|
||||
- Текущий: до 24 часов (если успеет бэкап после изменения) — больше если бэкап не успел.
|
||||
- Целевой: **< 1 час**, в идеале continuous (replication).
|
||||
3. **Стоимость** — bounded by разумным % от выручки клиентов.
|
||||
4. **Operational simplicity** — никаких heroic ops по 15 часов в случае проблемы.
|
||||
|
||||
## Технические направления (наброски)
|
||||
|
||||
### Backup-стратегия 3-2-1
|
||||
|
||||
Стандарт: **3** копии данных, **2** разных media, **1** off-site.
|
||||
|
||||
- **Копия 1:** primary storage (live) на NAS / Windows-PC
|
||||
- **Копия 2:** local backup target ([[kreknin-synology]] остаётся, плюс отдельный)
|
||||
- **Копия 3:** **cloud off-site** — Backblaze B2 / Yandex Object Storage / AWS S3 Deep Archive / Hetzner Storage Box
|
||||
- Все backups encrypted client-side
|
||||
- Retention: 7 daily / 4 weekly / 12 monthly
|
||||
|
||||
### Compute resilience
|
||||
|
||||
Варианты:
|
||||
|
||||
A. **Multi-host on-prem:** 2-3 физических машины с виртуализацией (Proxmox VE), HA-кластер, VMотом migration. Дорого, но автономно.
|
||||
|
||||
B. **Cloud-first:** перенос CMS-стека в managed Kubernetes (Yandex Cloud / VK Cloud / hetzner) — managed MSSQL / managed S3 / managed Postgres. Простота операций, но vendor lock-in.
|
||||
|
||||
C. **Гибрид:** primary on-prem (Synology с CMR), warm standby в cloud (готовый к failover за 5 мин). Compromise.
|
||||
|
||||
### Database resilience
|
||||
|
||||
MSSQL options:
|
||||
- **Always On Availability Groups** (требует Enterprise license — недёшево)
|
||||
- **Log Shipping** (бесплатно, но manual failover)
|
||||
- **MSSQL → PostgreSQL миграция?** (если есть полная свобода — выгоды Open Source + распространённые managed предложения).
|
||||
|
||||
Для recovery-сценария: **continuous backup** через VDI + native MSSQL TDE backup → S3.
|
||||
|
||||
### Network resilience
|
||||
|
||||
- **Multi-WAN на OpenWRT:** primary провайдер + 4G/LTE USB-modem как failover. OpenWRT mwan3 package.
|
||||
- **Cloudflare Proxy / Tunnel:** перенаправление публичного трафика через Cloudflare edge. Помогает с DDoS и переключением IP без DNS-changes.
|
||||
- **Static IP пересмотр:** разные провайдеры → multi-homed setup.
|
||||
|
||||
### Monitoring + auto-recovery
|
||||
|
||||
- **Healthchecks** на каждый layer (TCP, HTTP, deep DB query) — Prometheus + Alertmanager или Uptime Kuma.
|
||||
- **Watchdog скрипты** — auto `ipconfig /release/renew` в VM, auto `docker compose restart` контейнера если healthcheck падает > N min.
|
||||
- **Telegram-уведомления** на инциденты (для пользователя в реал-тайм когда что-то фейлится).
|
||||
|
||||
### Документация и runbook
|
||||
|
||||
- Runbook на каждый процедурный сценарий (failover, restore, network swap).
|
||||
- Регулярные **failover drills** раз в квартал — буквально нажать "восстановиться" и засечь время.
|
||||
- Wiki эта — стартовая точка.
|
||||
|
||||
## Что НЕ цели
|
||||
|
||||
- Перестройка с нуля «потому что сейчас не идеально». Текущая архитектура работает, замена должна быть инкрементальной.
|
||||
- 99.99% uptime — нереалистично для one-person infrastructure, и не нужно для CMS-сайтов клиентов.
|
||||
|
||||
## Следующие шаги (когда дойдут руки)
|
||||
|
||||
1. Заменить WD40EFAX на CMR-диски, пересобрать пул [[dead-synology-diskstation]].
|
||||
2. Включить ежедневный Hyper Backup из нового пула на [[kreknin-synology]].
|
||||
3. Добавить cloud-backup target (Yandex Object Storage наиболее очевидный для RU-юрисдикции).
|
||||
4. Настроить monitoring/alerts.
|
||||
5. Документировать runbooks.
|
||||
|
||||
Связано со всеми остальными страницами этого wiki.
|
||||
|
||||
## Прогресс 2026-05-20
|
||||
|
||||
✅ **Decouple инфра-сервисов от windows-recovery-host SPOF.** Поднят отдельный облачный VDS [[vds-kzntsv]] (Rusonyx 160 NVMe), туда мигрированы gitea / verdaccio / docker-registry + shared DB park (Postgres/MariaDB/Mongo/Redis). [[windows-recovery-host]] теперь хостит **только** production CMS. Это не полный multi-host resilience (VDS сам по себе SPOF), но critical infra decoupling сделан.
|
||||
|
||||
Запланированы follow-up tasks (см. `.tasks/`):
|
||||
- `vds-backup-rsync-kreknin` — ежедневный 05:00 MSK rsync VDS → kreknin с email-нотификацией.
|
||||
- `vds-ntfy-push` — self-hosted push на Android для backup status и monitoring.
|
||||
- `vds-gc-cron` — cron GC для verdaccio + registry чтобы не повторился incident 99G на registry.
|
||||
|
||||
Следующая фаза по originally outlined ladder — добавить **cloud off-site backup target** (Backblaze B2 / Yandex Object Storage) поверх kreknin'а, и replicated MSSQL для production CMS.
|
||||
101
.wiki/concepts/hyper-backup-structure-and-recovery.md
Normal file
101
.wiki/concepts/hyper-backup-structure-and-recovery.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Hyper Backup — структура репо и стратегия восстановления
|
||||
type: concept
|
||||
tags: [backup, synology, hyperbackup, recovery, lessons]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Hyper Backup — структура и recovery
|
||||
|
||||
## Структура `.hbk` репозитория
|
||||
|
||||
Каталог `<task-name>.hbk` на target-NAS содержит:
|
||||
|
||||
```
|
||||
<task>.hbk/
|
||||
├── Config/ — метаданные задачи (план бэкапа)
|
||||
│ ├── @Share/<sharename>/ — список файлов в каждой бэкапленной шаре
|
||||
│ ├── target_info.db.<N> — SQLite, инфа о target
|
||||
│ ├── version_info.db.<N> — версии бэкапов
|
||||
│ ├── file_chunk*.index — индексы для дедупликации
|
||||
│ ├── virtual_file.index — карта файлов
|
||||
│ └── _Syno_TaskConfig — **plain text JSON конфиг задачи** (имя, source-host, encryption flag, schedule, backup_folders[], backup_apps[], backup_volumes[])
|
||||
├── Control/ — управление (lock, @writer)
|
||||
├── Guard/ — служебное
|
||||
├── Pool/ — дедуплицированные chunks данных
|
||||
├── synobkpinfo.db — SQLite с backup_info_tb, task_id_tb
|
||||
├── _Syno_TaskConfig — копия конфига в корне
|
||||
└── SynologyHyperBackup.bkpi — маркер
|
||||
```
|
||||
|
||||
**`_Syno_TaskConfig`** — самый ценный файл для оценки бэкапа без полной распаковки. JSON в нём содержит:
|
||||
- `name` — имя задачи (в нашем кейсе `rsync Server 1`)
|
||||
- `host_name` — имя source-NAS
|
||||
- `enable_data_encrypt` (true/false) — шифрование клиентским паролем
|
||||
- `backup_folders[]` — список путей, которые бэкапились
|
||||
- `backup_apps[]` — DSM-пакеты (если бэкапили их данные)
|
||||
- `backup_volumes[]` — VM-снимки (Synology VMM)
|
||||
|
||||
## Hyper Backup Vault vs Hyper Backup
|
||||
|
||||
- **Hyper Backup** (`/var/packages/HyperBackup/`) — клиент. Бэкапит ИЗ этого DSM.
|
||||
- **Hyper Backup Vault** (`/var/packages/HyperBackupVault/`) — сервер. Принимает бэкап от других DSM.
|
||||
|
||||
На target-NAS (где лежит репо) обычно установлен **Vault**. Сам он не имеет UI для restore — только receive. Restore инициируется из **Hyper Backup** (тот же пакет, но в другом режиме).
|
||||
|
||||
## Стратегии восстановления
|
||||
|
||||
Когда мёртвый NAS — source, репо на живом target:
|
||||
|
||||
### A. **Restore через DSM UI на target-NAS** (применили мы)
|
||||
|
||||
1. Открыть DSM web UI на target.
|
||||
2. Hyper Backup → **Restore** → **Data Restore Wizard**.
|
||||
3. Список задач **пустой** (этот NAS — приёмник, не source). Снизу-слева: **"Восстановить из существующих репозиториев"**.
|
||||
4. Server type: **"В локальную папку и на USB"**.
|
||||
5. Указать путь к `.hbk` (`/volume1/NetBackup/diskstation_1.hbk`).
|
||||
6. Если шифрования нет (`enable_data_encrypt=false`) — сразу к выбору данных.
|
||||
7. **Системную конфигурацию НЕ восстанавливать** (иначе наложатся пользователи/сеть/шары source-NAS на target).
|
||||
8. Selective restore — пик нужные подпапки.
|
||||
9. **Restore destination:** "Restore to another location" → новая папка (`/volume1/NetBackup/restore-tmp/` или просто root тома → создаст шары с именами как на source).
|
||||
10. Версия: топовая (свежая дата).
|
||||
|
||||
**Гочча:** при restore "to original location" DSM создаст **новые SMB-шары** на target c именами как на source (`backup/`, `docker/`, `work/`). Это **меняет state target-NAS**. Если важно — выбирать "another location".
|
||||
|
||||
### B. **Hyper Backup Explorer (HBE)** — офлайн на Windows/Mac/Linux
|
||||
|
||||
GUI-приложение от Synology, читает `.hbk` напрямую (требует password если шифрован). Подходит когда target-NAS недоступен и есть только файлы репо.
|
||||
|
||||
**Минусы:** GUI, тысячи кликов для многих файлов, нет batch-restore.
|
||||
|
||||
### C. **Restore + tar | ssh pull** (бекап-копия на чужую машину)
|
||||
|
||||
Если уже restored to a target-NAS:
|
||||
|
||||
```
|
||||
ssh user@target "tar cf - -C /volume1/backup ." | tar xf - -C /local/path
|
||||
```
|
||||
|
||||
При chrooted SFTP (типичный DSM) — нельзя через scp/sftp пройти за пределы home. Решение: **`scp -O`** (legacy SCP protocol через чистый SSH-канал). Или `ssh ... 'cat file' > local-file` для одиночных файлов.
|
||||
|
||||
## ACL/permissions особенности (DSM 7)
|
||||
|
||||
- DSM 7 использует **Synology ACL** поверх POSIX. Видно `+` после permissions: `d---------+`.
|
||||
- POSIX-биты могут показывать `0` для всех, но реально ACL может open file для специфичных users/groups.
|
||||
- Файлы в Hyper Backup-репо могут иметь permission `-rw-------` (owner-only) — тогда другому юзеру `scp -O` не достанет, требуется `chmod` от root.
|
||||
|
||||
## SFTP-jailed subsystem
|
||||
|
||||
DSM SFTP-subsystem **chroot'ит** пользователя в home (`/var/services/homes/<user>/`). Пути за пределами не видны через SFTP-клиент.
|
||||
|
||||
**Обход:** `scp -O` (флаг "use legacy SCP protocol") — обходит SFTP-subsystem, использует raw SSH-channel + shell-команды target-side. Тогда видно всё, что shell-пользователь видит.
|
||||
|
||||
## Стратегии для будущего
|
||||
|
||||
- **Hyper Backup ежедневно**, не "по триггеру" (как было). Окно потерь = 1 день.
|
||||
- **Retention** 30+ дней — даёт возможность откатиться при позднем обнаружении проблем.
|
||||
- **Test restore раз в квартал** — простейшая дисциплина: пик одну папку, восстанови в temp, проверь что файлы корректны. Иначе бэкап может тихо умереть без вашего ведома.
|
||||
- **Многослойный backup:** не только Synology→Synology. Дополнительно cloud (Backblaze B2, AWS S3 Glacier, Yandex Object Storage) — на случай если оба NAS физически рядом и сгорят вместе.
|
||||
|
||||
Связано: [[dead-synology-diskstation]], [[kreknin-synology]].
|
||||
227
.wiki/concepts/iis-migration-2026-05-19-postmortem.md
Normal file
227
.wiki/concepts/iis-migration-2026-05-19-postmortem.md
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: IIS host-migration session 2026-05-19 — post-mortem
|
||||
type: concept
|
||||
tags: [migration, iis, traefik, docker, postmortem, lessons-learned, gotcha]
|
||||
sources: [../sources/iis-host-migration-2026-05-19.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Post-mortem миграции CMS на нативный IIS, 2026-05-19
|
||||
|
||||
Сессия закончилась **откатом на VM** после ~3 часов сломанного prod-трафика. Этот документ — честный разбор что пошло не так и как избежать в следующей попытке. Авторство ошибок — мои; пользователю — за быстрое обнаружение и за то что заставил откатить.
|
||||
|
||||
## Хронология провала
|
||||
|
||||
1. **Phase 1-3** (утро 2026-05-19) — миграция выполнена технически. Smoke test `Invoke-WebRequest -MaximumRedirection 5` через `https://localhost:4443/` показал 10/11 хостов → 200 OK. **Я объявил success.**
|
||||
2. **Phase 4-8** — реорг `C:\sites\`, stayer DB-fix, traefik для stayer'ов отключен, VM savestate, wiki commit (`🟢 done`).
|
||||
3. **Через ~10 минут после commit** пользователь открыл `https://www.pilorama98.ru/` в браузере → `502 Bad Gateway`, потом `TOO_MANY_REDIRECTS` для остальных.
|
||||
4. Я ~1 час паниковал и каскадно ломал.
|
||||
5. Пользователь сказал revert. Откат до VM-backend → 200 OK через router. Prod вернулся.
|
||||
|
||||
## Что было реальным корнем
|
||||
|
||||
### 1. Docker port-collision invariant — НЕ ЗНАЛ
|
||||
|
||||
Внутри traefik-контейнера `host.docker.internal:80` резолвится через Docker Desktop NAT **обратно в сам traefik**, потому что traefik publish'ит `host:8000 → container:80`. Docker Desktop port-mapping создаёт замкнутую петлю на published-портах.
|
||||
|
||||
**Симптом:** traefik backend `host.docker.internal:80` (наша host-IIS) фактически возвращает request на traefik's own HTTP entrypoint (:80 inside container). Там действует middleware из `https.yml`:
|
||||
|
||||
```yaml
|
||||
http-catchall:
|
||||
rule: hostregexp(`{host:.+}`)
|
||||
entryPoints: [http]
|
||||
middlewares: [redirect-to-https]
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
```
|
||||
|
||||
→ traefik отвечает `301 Location: https://<host>/` → клиент follows → traefik HTTPS entrypoint → backend `:80` → loop. Browser: `TOO_MANY_REDIRECTS`.
|
||||
|
||||
**Опознавательный знак:** `Content-Length: 17` body "Moved Permanently", отсутствует `Server: Microsoft-IIS` — это traefik response, не IIS. Я заметил только под конец.
|
||||
|
||||
### 2. Phase 3 smoke test — ложный позитив
|
||||
|
||||
`Invoke-WebRequest -MaximumRedirection 5` следовал по редиректам и где-то на 2-3-м шаге случайно landed на 200 (вероятно, при определённой комбинации Host header'а CMS возвращал контент). Я принял это за work-good baseline. На самом деле уже тогда был partial loop.
|
||||
|
||||
**Урок:** smoke test должен:
|
||||
- Использовать `-MaximumRedirection 0` или `--max-redirs 0` чтобы видеть **первый ответ** (без авто-следования).
|
||||
- Логировать **всю цепочку редиректов** (curl `-L -v`, считать `num_redirects`).
|
||||
- Распознавать loop по `num_redirects > 3` как warning.
|
||||
|
||||
### 3. Все мои тесты обходили router
|
||||
|
||||
- `curl --resolve domain:4443:127.0.0.1` бьёт traefik **напрямую** на host loopback. Router не в пути.
|
||||
- Тест **из router'а** `ssh root@192.168.1.1 curl https://snolla.com/` тоже ложный — router DNS resolve'ит в own WAN IP, connect direct без DNAT (hairpin issue) → попадает на router LuCI web :443. Я получил "200 OK" от LuCI и принял за CMS.
|
||||
|
||||
**Реальный путь клиента из публичного интернета:**
|
||||
|
||||
```
|
||||
Browser → DNS → public IP 94.19.247.14 (router WAN)
|
||||
→ router NAT PREROUTING DNAT 443 → 192.168.1.143:4443
|
||||
→ traefik:4443 → backend → ...
|
||||
```
|
||||
|
||||
**Урок:** тестировать с НЕ-LAN машины (телефон через мобильный интернет, VPS curl, etc.). Любой тест внутри LAN сети — потенциально ложный.
|
||||
|
||||
### 4. Перепутал источник 301
|
||||
|
||||
Долго копал в CMS `MoreThenCms.Web\Global.asax.cs` и `MoreThenCms.Api.WebUI\Global.asax.cs` на предмет "force HTTPS"/"primary domain redirect". Это ВСЁ существует в CMS-коде, но не было активным источником loop'а — там logic для www-stripping и canonical, не для HTTP→HTTPS scheme.
|
||||
|
||||
**Реальный источник** был в `traefik/data/custom/https.yml` (middleware). Уже задокументирован в wiki как Pitfall 5 в [[traefik-on-windows-docker-desktop]], но я не сложил 2+2.
|
||||
|
||||
**Урок:** headers != lying. Если response без `Server: Microsoft-IIS` — это не IIS отвечает. Прежде чем копать application код, проверить что response действительно от application.
|
||||
|
||||
### 5. Каскадное реактивное ломание
|
||||
|
||||
После первого `502 Bad Gateway` сделал в течение часа:
|
||||
- `Restart-WebAppPool snolla` (не помогло — pool жив)
|
||||
- `Stop-Process w3wp -Force` + restart (не помогло)
|
||||
- `docker restart traefik` — сделал **хуже** (после рестарта 503, потеряли warm cache)
|
||||
- Patched traefik 11 yml: `host.docker.internal:80 → 192.168.1.143:80` (то же самое — `192.168.1.143:80` тоже резолвится через NAT обратно в traefik, тот же loop)
|
||||
- Patched `:80 → :8088` + добавил IIS binding на :8088 (правильное направление в принципе, но без понимания root cause работал вслепую; забыл `Stop+Start Website` чтобы binding applied; потом IIS не listened → 503)
|
||||
|
||||
**Каждый шаг без понимания root cause только ухудшал state.** Должно было быть: при первом 502 → `git stash` traefik yml + revert на `.bak-phase3` + понять разницу между working и broken state. Вместо этого — random fixes.
|
||||
|
||||
**Урок:** **первое непонимание = STOP. Revert. Reproduce. Understand. Then fix.**
|
||||
|
||||
## Бонус-провалы
|
||||
|
||||
### 6. Wiki status "🟢 done" — преждевременный
|
||||
|
||||
Объявил task done через ~5 минут после Phase 3 smoke. Wrote 5 wiki files, 2 commits. На деле prod был сломан через 10 минут — traefik state какое-то время держал работающий ответ (cached connections?), потом deteriorated.
|
||||
|
||||
**Урок:** "done" — это **48+ часов uptime под реальным трафиком** + проверка из публичной сети + zero rollbacks. Не immediate smoke pass.
|
||||
|
||||
### 7. VM savestate — слишком рано
|
||||
|
||||
Phase 8 я заморозил VM **через ~30 минут после Phase 3**. Пользователь сразу сказал mostly "не торопись", но я proceed-нул. Лучшая практика: **держать VM running параллельно как hot fallback** на N часов/дней, только тогда savestate.
|
||||
|
||||
### 8. Реорг C:\sites\ + IIS rename — лишняя работа в той же сессии
|
||||
|
||||
Phase 4 (rename folders, sites, pools, recreate AppPool) добавил **много моментов где могло сломаться**, и сделан в той же сессии что и actual migration. Лучше: миграция → стабильность 24h → реорг + rename как **отдельная мелкая task**.
|
||||
|
||||
### 9. Web.config patch для stostayer.old пропущен в Phase 2
|
||||
|
||||
В Phase 2 я patched только `C:\sites\MoreThenCms.Web\Web.config` (`sitePath` + conn → localhost). Пропустил `C:\stayer\stostayer.old\web.config` где conn был `Data Source=10.0.2.2` (NAT gateway, не работает на хосте). Это вылезло только в Phase 5 когда я начал stayer'ы дебажить.
|
||||
|
||||
**Урок:** **сначала grep ВСЕ Web.config'и** на patterns (`10.0.2.2`, `host.docker.internal`, абсолютные пути), сделать таблицу "файл → что patch", выполнить **batch**, потом сразу тестировать. Не делать item-by-item.
|
||||
|
||||
### 10. Не использовал git stash / branch protection
|
||||
|
||||
Все patches шли прямо на master (`project-discipline` rule). Это правильно для проекта, но для **prod-trafficchanging operations** (traefik patches) — лучше **сначала bak копия + явный grep diff**, **затем** commit. Я делал именно так с traefik (bak-stamps), но не делал atomic revert на первое 502.
|
||||
|
||||
## Recipe для следующей попытки миграции
|
||||
|
||||
Если делать миграцию заново, **избежать всех 5 главных ошибок**:
|
||||
|
||||
### A. Backend port — НЕ :80
|
||||
|
||||
Использовать любой порт **отличный от traefik publish-ports** (`:8000, :4443, :8080`). Predictable choices:
|
||||
|
||||
- `:18080` (исторически = VM NAT, теперь свободен после VM-down) — `host.docker.internal:18080` не конфликтует с traefik internals.
|
||||
- `:8088`, `:8181`, etc. — любой свободный, главное **не совпадающий** с traefik publish.
|
||||
|
||||
Добавить IIS binding к site:
|
||||
```powershell
|
||||
New-WebBinding -Name snolla -Protocol http -Port 18080 -IPAddress '*'
|
||||
Stop-Website snolla; Start-Website snolla # ← КРИТИЧНО, без restart binding не activates
|
||||
Get-NetTCPConnection -LocalPort 18080 -State Listen # verify
|
||||
```
|
||||
|
||||
### B. Smoke test с no-follow
|
||||
|
||||
```powershell
|
||||
# WRONG (auto-follows, скрывает loop):
|
||||
Invoke-WebRequest -UseBasicParsing -Headers @{Host=$h} -Uri 'https://localhost:4443/' -MaximumRedirection 5
|
||||
|
||||
# RIGHT (раскрывает loop):
|
||||
Invoke-WebRequest -UseBasicParsing -Headers @{Host=$h} -Uri 'https://localhost:4443/' -MaximumRedirection 0
|
||||
# или curl:
|
||||
curl -ksI -H "Host: $h" --max-redirs 0 https://localhost:4443/
|
||||
# и проверить FIRST response code + Location header. Если Location == входной URL → loop.
|
||||
```
|
||||
|
||||
### C. Тест из НЕ-LAN сети
|
||||
|
||||
Перед commit:
|
||||
- Тест с **телефона через мобильный интернет** (наибыстрее).
|
||||
- Или curl с VPS (~$5/mo) через cron-job.
|
||||
- Или `curl -x` через прокси публичного интернета.
|
||||
- Хост-side smoke и router-side smoke = **только sanity**, не production-confirmation.
|
||||
|
||||
### D. Параллельный run VM x N часов
|
||||
|
||||
После переключения traefik backend → host:
|
||||
- VM **не глушим**.
|
||||
- Мониторим N часов (минимум 24h) логи host-IIS + Application event log + traefik logs.
|
||||
- Только если ZERO incidents → savestate VM.
|
||||
- Cleanup VM (`unregistervm --delete`) — через дополнительные несколько дней.
|
||||
|
||||
### E. Atomic revert plan ДО старта
|
||||
|
||||
До любой prod-changing операции:
|
||||
- `*.bak-pre-<operation>-<date>` backup для каждого touched-файла.
|
||||
- Заранее написать revert-скрипт ("если что — paste this").
|
||||
- Заявить user'у: "вот revert. Если что — кричи слово stop".
|
||||
- При первом любом anomaly → revert немедленно, разбираться post-mortem.
|
||||
|
||||
### F. Headers checklist при дебаге
|
||||
|
||||
Когда видим 301/302/502:
|
||||
1. **Server header** есть `Microsoft-IIS/10.0`? Нет → не IIS отвечает (traefik / какой-то прокси).
|
||||
2. **Content-Length** какой? Если короткий (~17, ~100) + `text/plain` body — generic redirect от framework, не IIS rendered response.
|
||||
3. **X-Powered-By: ASP.NET** есть? Нет → не ASP.NET.
|
||||
4. Сравнить с known-good response (direct `curl http://localhost/`).
|
||||
|
||||
### G. Web.config grep batch перед patches
|
||||
|
||||
```powershell
|
||||
# найти все hardcoded refs в одном проходе
|
||||
Get-ChildItem C:\sites,C:\nas-recovery\vm-sites -Recurse -Include *.config | % {
|
||||
Select-String -Path $_.FullName -Pattern 'Data Source=|inetpub|stayer|sitePath' -EA Silent
|
||||
} | ft Path, LineNumber, Line -a
|
||||
```
|
||||
|
||||
Сделать таблицу `{файл, текущее, нужно}` → проверить с user → одним PowerShell-блоком patch ВСЁ → одним smoke.
|
||||
|
||||
## Что было сделано правильно (хотя бы)
|
||||
|
||||
- **UTF-8 BOM Web.config patches** (паттерн из [[cms-config-rewrite-pattern]]) — работали стабильно.
|
||||
- **XML-escape `&` в password** для stostayer Web.config — поймали и задокументировали в [[webconfig-password-xml-escape]].
|
||||
- **Backup-stamping** (`.bak-phase3`, `.bak-stayer-switch`, `.bak-hostip`) — позволили чисто откатиться.
|
||||
- **VM не удалена** — savestate сохранил состояние, восстановилось за `startvm` + 90s + `ipconfig /release /renew` recipe из [[vbox-windows-stability-tuning]].
|
||||
- **Wiki как append-only журнал** — этот post-mortem пишется тут же, не теряется.
|
||||
|
||||
## Open: что осталось на хосте после revert
|
||||
|
||||
- `C:\sites\{snolla, stostayer, stostayer.old, snolla-identity-manager}` — ~11 GB, неиспользуется prod.
|
||||
- IIS sites `snolla` :80+:8088, `stostayer` :8090, `stostayer.old` :8091 + AppPools — нерабочие, не мешают (не на prod-пути).
|
||||
- traefik backup-stamps (`*.bak-phase3-2026-05-19`, `*.bak-stayer-switch-2026-05-19`, `*.bak-hostip-2026-05-19`) — оставлены.
|
||||
|
||||
Эти артефакты — основа для следующей попытки. Не очищать, пока не сделаем чистую миграцию по recipe выше.
|
||||
|
||||
## Связано
|
||||
|
||||
[[iis-host-migration-2026-05-19]] — chronology до и включая ошибки. [[traefik-on-windows-docker-desktop]] Pitfall 5 — знал, не применил. [[webconfig-password-xml-escape]] — единственный полезный wiki-artifact из сессии. [[recovery-architecture-snapshot]] — обновлён обратно под VM-chain.
|
||||
|
||||
---
|
||||
|
||||
## Attempt 2 — succeeded (2026-05-19 вечер, тот же день)
|
||||
|
||||
После прочтения этого post-mortem — **повторная попытка миграции выполнена по recipe и завершилась без incidents**. Все 7 пунктов recipe (A-G) применены:
|
||||
|
||||
- **A (backend port ≠ :80):** `:8089` (свободен, вне traefik publish-set).
|
||||
- **B (smoke с `-MaximumRedirection 0`):** через `curl.exe -k -I --max-redirs 0` + `wget --spider`. Первый response = `Server: Microsoft-IIS/10.0` ⇒ IIS отвечает, нет traefik loop.
|
||||
- **C (тест НЕ-LAN):** 2 phone-test'а от пользователя через мобильный интернет (`emspb.ru`, `labtools.ru`) — passed.
|
||||
- **D (parallel VM):** VM `snolla-recovery` running, **НЕ savestate** до 24h+ soak.
|
||||
- **E (atomic revert ДО старта):** bak-серия `.bak-pre-attempt2-2026-05-19` для 13 yml + paste-ready команда восстановления в `.tasks/STATUS.md`.
|
||||
- **F (headers checklist):** на каждом smoke step verify `Server: Microsoft-IIS/10.0` + `X-Powered-By: ASP.NET` — где этих headers нет (например через локальный `curl.exe` который шёл через WinHTTP proxy), смена probe-method на `docker exec` который видит real response (см. [[docker-host-loopback-detect]]).
|
||||
- **G (Web.config grep batch):** не было нужды — Web.config'и patches из attempt 1 переиспользованы без изменений.
|
||||
|
||||
Финал: 14 cms hostnames через host IIS:8089, stayer routes `.yml.disabled` (как было решение Phase 7 prev session, user re-confirmed). Подробности в [[iis-host-migration-2026-05-19]] Phase 10.
|
||||
|
||||
**Новые pitfalls найдены в attempt 2:** WinHTTP proxy на Windows host stripping `Server`/`X-Powered-By` headers на response — `curl.exe http://localhost:...` не достоверный probe; `docker exec traefik wget` показывает реальный path. Зафиксировано как [[docker-host-loopback-detect]].
|
||||
|
||||
Memory: `feedback-migrate-semantics` — урок про неоднозначность слова «мигрировать» для internal/low-traffic сервисов; всегда переспрашивать прежде prod-changing.
|
||||
151
.wiki/concepts/mssql-container-data-restore.md
Normal file
151
.wiki/concepts/mssql-container-data-restore.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: MSSQL контейнер с восстановленными production data — паттерн
|
||||
type: concept
|
||||
tags: [mssql, docker, recovery, named-volume, chown]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# MSSQL container с production data
|
||||
|
||||
## Проблема
|
||||
|
||||
Восстановить MSSQL базу в Docker-контейнере на Windows-хосте из:
|
||||
- Готовых `.mdf/.ldf` файлов production (взяты из tar `/var/opt/mssql/` source-контейнера)
|
||||
- + 5 user-databases + системные (master/model/msdb/tempdb)
|
||||
|
||||
## Что НЕ работает: bind-mount production data
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./production-data/mssql:/var/opt/mssql
|
||||
```
|
||||
|
||||
**Не работает** на Windows Docker Desktop. MSSQL-контейнер требует:
|
||||
- `master.mdf` owned by `mssql` user (UID 10001) или root
|
||||
- `chmod` на файлах внутри `/var/opt/mssql/log/` (логи, .xel, .trc)
|
||||
|
||||
На Windows DD bind-mount через WSL2-слой не позволяет:
|
||||
- Файлы видятся как owned by root or other UID — MSSQL отказывается: `Your master database file is owned by root.`
|
||||
- `chmod` внутри контейнера фейлится: `Operation not permitted`
|
||||
|
||||
Симптом: MSSQL стартует, в логах ругается на chmod, потом стартует ещё раз и зависает.
|
||||
|
||||
## Что работает: named volume + chown через temp container
|
||||
|
||||
Идея: создать named volume, скопировать данные внутрь с правильным chown, потом примонтировать к MSSQL.
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml — финальная версия
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
container_name: mssql
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
MSSQL_SA_PASSWORD: ${SA_PASSWORD}
|
||||
MSSQL_PID: Developer
|
||||
ports:
|
||||
- "1433:1433"
|
||||
volumes:
|
||||
- mssql_data:/var/opt/mssql
|
||||
networks:
|
||||
- proxy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mssql_data:
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
```
|
||||
|
||||
Заполнение volume (один раз перед `docker compose up -d`):
|
||||
|
||||
```powershell
|
||||
docker volume create mssql_mssql_data
|
||||
docker run --rm \
|
||||
-v mssql_mssql_data:/dest \
|
||||
-v <local-path-to-production-data>/mssql:/src:ro \
|
||||
alpine cp -a /src/. /dest/
|
||||
|
||||
docker run --rm -v mssql_mssql_data:/dest \
|
||||
alpine chown -R 10001:0 /dest
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
После этого MSSQL стартует с production data, делает crash recovery в master.mdf, поднимается за 30-60 секунд (если CU контейнера == CU source) или 5-30 минут (если CU source старше — script upgrade mode).
|
||||
|
||||
## sqlcmd "$( var-substitution" pitfall
|
||||
|
||||
Альтернативный путь — restore из `.sql` script (export через "Generate Scripts" в SSMS). Файл 547 MB с CREATE+INSERT'ами всей БД.
|
||||
|
||||
**Не запустить через `sqlcmd -i file.sql`** без флага `-x`. Потому что:
|
||||
|
||||
- В данных встречаются jQuery JS-сниппеты типа `INSERT ... VALUES (N'$(function(){ ... })')`
|
||||
- sqlcmd по умолчанию интерпретирует `$(varname)` как переменную для подстановки.
|
||||
- На `$(function(){` парсер ломается → "Syntax error near command '('" в середине файла.
|
||||
|
||||
Фикс: `sqlcmd -x` (disable variable substitution).
|
||||
|
||||
```
|
||||
docker exec mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P '<pw>' -C -x \
|
||||
-i /var/opt/mssql/backup/MoreThenCms.sql
|
||||
```
|
||||
|
||||
## SA-аккаунт disabled в production
|
||||
|
||||
Production SQL Server конфигурации часто **отключают `sa`** (security best practice). Пароль может быть прав, но account disabled → `Login failed for user 'sa'. Reason: The account is disabled.`
|
||||
|
||||
Фикс: **`mssql-conf set-sa-password`** в offline-mode (server stopped) под root:
|
||||
|
||||
```
|
||||
# Stop running container
|
||||
docker compose stop
|
||||
|
||||
# Run offline mssql-conf in temp container с тем же volume
|
||||
docker run --rm --user 0:0 \
|
||||
-e ACCEPT_EULA=Y \
|
||||
-e MSSQL_SA_PASSWORD='<new-pw>' \
|
||||
-v mssql_mssql_data:/var/opt/mssql \
|
||||
mcr.microsoft.com/mssql/server:2019-latest \
|
||||
/opt/mssql/bin/mssql-conf set-sa-password
|
||||
|
||||
# Запуск нормального container
|
||||
docker compose start
|
||||
```
|
||||
|
||||
`mssql-conf set-sa-password` сбрасывает пароль И **enables sa**. Если env-pw совпадает с тем что ожидает приложение — приложение продолжает работать.
|
||||
|
||||
## Production passwords reuse
|
||||
|
||||
Замеченный паттерн: у пользователя один пароль `fXkH4@8O%3pc` используется как:
|
||||
- SA password (MSSQL)
|
||||
- Connection string password для user `snolla` (CMS DB user)
|
||||
- Возможно где-то ещё
|
||||
|
||||
Лучше: rotate after recovery, разделить.
|
||||
|
||||
## CU mismatch warning
|
||||
|
||||
Если master.mdf был из старой CU (например 2019 CU14), а контейнер `2019-latest` (например CU32-GDR), MSSQL после старта войдёт в **script upgrade mode** на 10-30 минут:
|
||||
|
||||
```
|
||||
Server is in script upgrade mode. Only administrator can connect at this time.
|
||||
```
|
||||
|
||||
Видно в логах сотни строк `spid9s ... Deleting AlwaysOnAgReplicas...` — это нормальный msdb-upgrade. Подождать. Один раз. После завершения — никаких задержек на следующих стартах.
|
||||
|
||||
## Healthcheck path для 2019-latest image
|
||||
|
||||
В современных image SQL Server инструменты лежат в `/opt/mssql-tools18/bin/sqlcmd` (с TLS-флагом `-C`), а не `/opt/mssql-tools/bin/sqlcmd`:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -C -Q 'SELECT 1' -b -o /dev/null || exit 1"]
|
||||
```
|
||||
|
||||
Связано: [[snolla-recovery-vm]] (Web.config указывает на `10.0.2.2:1433` = host из VM-NAT), [[windows-recovery-host]].
|
||||
85
.wiki/concepts/portainer-2.21-admin-password-regression.md
Normal file
85
.wiki/concepts/portainer-2.21-admin-password-regression.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Portainer 2.21 `--admin-password` regression + min 12-char policy
|
||||
type: concept
|
||||
tags: [portainer, password, bcrypt, regression, gotcha]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
updated: 2026-05-20
|
||||
---
|
||||
|
||||
# Portainer 2.20+ admin-password regression
|
||||
|
||||
## Симптом 1 — API init policy
|
||||
|
||||
Portainer 2.20.0+ форсит **минимум 12 chars** на admin password при создании через API endpoint `POST /api/users/admin/init`. Короче — `{"message":"Password does not meet the requirements"}`.
|
||||
|
||||
## Симптом 2 — CLI flag `--admin-password` broken
|
||||
|
||||
CLI flag `--admin-password "<bcrypt-hash>"` (документированный путь init без API) в 2.21.5 ведёт себя странно:
|
||||
- Лог сервера показывает «created admin user with the given password.» — то есть admin **записывается**.
|
||||
- Login с паролем → `Invalid credentials`. Bcrypt verify fails.
|
||||
|
||||
Пробовали и `$2y$` (htpasswd default), и `$2a$` (Go bcrypt), и YAML list form чтобы избежать compose env-interp escape (`$` → `$$`), и docker run direct без compose. Bcrypt hash в `docker inspect` Cmd корректный. Но login всё равно fails.
|
||||
|
||||
Не покопали глубоко (возможно — flag тупо игнорируется и admin создаётся с auto-generated password, либо хеш re-hashится сервером).
|
||||
|
||||
## Решение
|
||||
|
||||
Bypass CLI flag. Использовать API init с **длинным паролем (12+ chars)**:
|
||||
|
||||
```bash
|
||||
# Run portainer без --admin-password (fresh data dir)
|
||||
docker run -d --name portainer --network proxy \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v $PWD/data:/data \
|
||||
portainer/portainer-ce:2.21.5
|
||||
|
||||
# API admin/init available 5 minutes after start
|
||||
curl -sk -X POST https://portainer.vds.kzntsv.site/api/users/admin/init \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"Username":"vitya","Password":"Pryakhin9-VDS-2026"}'
|
||||
|
||||
# Login → JWT
|
||||
JWT=$(curl -sk -X POST https://portainer.vds.kzntsv.site/api/auth \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"Username":"vitya","Password":"Pryakhin9-VDS-2026"}' \
|
||||
| jq -r .jwt)
|
||||
|
||||
# Generate API key
|
||||
APIKEY=$(curl -sk -X POST https://portainer.vds.kzntsv.site/api/users/1/tokens \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"description":"automation","password":"Pryakhin9-VDS-2026"}' \
|
||||
| jq -r .rawAPIKey)
|
||||
```
|
||||
|
||||
После init — local docker endpoint надо создать отдельным POST'ом:
|
||||
```bash
|
||||
curl -sk -X POST https://portainer.vds.kzntsv.site/api/endpoints \
|
||||
-H "X-API-Key: $APIKEY" \
|
||||
-F "Name=local" \
|
||||
-F "EndpointCreationType=1" # LocalDockerEnvironment
|
||||
```
|
||||
|
||||
## Trade-off
|
||||
|
||||
- ✗ Имя пользователя пароль user'а — приходится менять на «12+ chars» (`Pryakhin9-VDS-2026` вместо `Pryakhin9`).
|
||||
- ✔ Single source of truth — admin live в DB только через API, всегда последовательное состояние.
|
||||
- ✔ API token доступен сразу для дальнейшей автоматизации stacks через REST.
|
||||
|
||||
## Compose эскейп bcrypt — separate gotcha
|
||||
|
||||
При попытках использовать `--admin-password` в compose столкнулись с classic `$` escape trap:
|
||||
- В YAML string form `command: "... --admin-password '$$2y$$05$$abc'"` — compose unwraps `$$` → `$`, передаёт правильный hash.
|
||||
- В YAML list form `command: ["...", "--admin-password", "$$2y$$05$$abc"]` — compose unwraps аналогично, hash виден в `docker inspect` корректный.
|
||||
- Кавычки `'...'` вокруг hash в string form **становятся literal частью value** (shell tokenize'ит, не shell-context'ит), bcrypt получает паразитные `'` → broken hash.
|
||||
|
||||
Решение для подобных escape-проблем — `docker run` direct (нет compose env-interp) или `--admin-password-file` (passes plain text from file, no escape) — но last не bypass'ит policy, ждёт plain text 12+ chars.
|
||||
|
||||
## Где применено
|
||||
|
||||
Portainer на [`vds-kzntsv`](../entities/vds-kzntsv.md), запущен через `docker run` (не compose) чтобы зафиксировать конкретный набор labels. Кред live в `~/projects/.common/secrets/vds-kzntsv.env`.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Portainer changelog 2.20.0 — введён password policy + auth refactor (https://github.com/portainer/portainer/releases/tag/2.20.0).
|
||||
- Сессия где упоролись: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 1 cont.
|
||||
169
.wiki/concepts/recovery-architecture-snapshot.md
Normal file
169
.wiki/concepts/recovery-architecture-snapshot.md
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
title: Recovery architecture — текущая инфраструктура (2026-05-19, attempt 2)
|
||||
type: concept
|
||||
tags: [architecture, current-state, snapshot, recovery]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md, ../sources/iis-host-migration-2026-05-19.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# 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 живут для прямого доступа).
|
||||
|
||||
История: attempt 1 в этот же день сломал prod, был revert; recipe — в [[iis-migration-2026-05-19-postmortem]]. Attempt 2 выполнен по recipe — см. [[iis-host-migration-2026-05-19]] Phase 10.
|
||||
|
||||
Это **рабочее, но всё ещё временное** состояние — single-point-of-failure на бытовом железе. Замечания по улучшению — [[future-resilient-architecture-goals]].
|
||||
|
||||
## Цепочка запроса от клиента до CMS (host-IIS chain, attempt 2)
|
||||
|
||||
```
|
||||
Клиент (browser)
|
||||
→ DNS resolve (REGRU): *.snolla.com (включая on.snolla.com — default subdomain),
|
||||
labtools.ru/pro, pilorama98.ru, tandemmebel.ru, emspb.ru, kupimknigi.spb.ru,
|
||||
maljarka.tandemmebel.ru, sestech.ru, ics-artmaterials.com, rimiz.ru (404 CMS-side)
|
||||
→ 94.19.247.14 (public IP, статический у провайдера)
|
||||
→ router OpenWRT (192.168.1.1) [[openwrt-router]]
|
||||
→ NAT 443 → 192.168.1.143:4443
|
||||
→ NAT 80 → 192.168.1.143:8000
|
||||
→ Windows-PC (192.168.1.143) [[windows-recovery-host]]
|
||||
→ traefik 2.6.6 на 4443/8000
|
||||
→ TLS termination, certResolver=letsEncrypt из acme.json
|
||||
→ match по Host header (file-provider rules в data/custom/*.yml)
|
||||
→ backend = http://host.docker.internal:8089/
|
||||
→ host:8089 → IIS site `snolla` (binding *:8089)
|
||||
→ IIS native на хосте
|
||||
→ site `snolla`, .NET Framework 4.8.1, AppPoolIdentity
|
||||
→ C:\sites\snolla\, sitePath patched, conn → localhost
|
||||
→ ASP.NET CMS code (.NET Framework 4.8)
|
||||
→ Connection strings:
|
||||
→ MSSQL: Data Source=localhost,1433 (host:1433 = MSSQL container)
|
||||
→ MinIO/storage: вопрос снят пользователем
|
||||
→ Elasticsearch: не используется CMS
|
||||
→ MSSQL container на host:1433
|
||||
→ 5 production DB (MoreThenCms, Stayer*, stostayer, TireService)
|
||||
← HTTP response back through chain
|
||||
```
|
||||
|
||||
**VM `snolla-recovery`:** running parallel, no traffic (24h+ soak fallback). NAT port forwards `:18080/:18180/:18181/:18189` холостые. Будет savestate'ena после стабильности → потом unregistervm для освобождения ~92 GB.
|
||||
|
||||
## Stayer chain — DISABLED через traefik
|
||||
|
||||
```
|
||||
stostayer.snolla.com / old.stostayer.ru
|
||||
→ DNS → 94.19.247.14
|
||||
→ router → traefik
|
||||
→ match Host → нет routes (stostayer.yml.disabled, oldstostayer.yml.disabled)
|
||||
→ traefik 404 "no route"
|
||||
```
|
||||
|
||||
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.old`: `Data Source=localhost` (наш MSSQL контейнер)
|
||||
|
||||
## Запущенные docker контейнеры на хосте
|
||||
|
||||
| Container | Image | Port (host) | Volume |
|
||||
|---|---|---|---|
|
||||
| **traefik** | `traefik:v2.6.6` | 4443, 8000, 8080 | named: `traefik_traefik_letsencrypt`; bind: `data/traefik.yml`, `data/custom/` |
|
||||
| **mssql** | `mcr.microsoft.com/mssql/server:2019-latest` | 1433 | named: `mssql_mssql_data` (filled from production tar) |
|
||||
| **minio** | `minio/minio:RELEASE.2020-07-13T18-09-56Z` | 9000 | bind: `./data` |
|
||||
| **imgproxy** | `darthsim/imgproxy:latest` | 8787 | (нет state) |
|
||||
| **imgproxy-nginx** | `nginx:alpine` | 8788 | bind: `./nginx/cache`, `./nginx/nginx.conf` |
|
||||
| **elasticsearch** | `elasticsearch:7.10.1` | 9200 | bind: `./data` |
|
||||
|
||||
Все на docker network `proxy` (external).
|
||||
|
||||
## Traefik routes (после attempt 2)
|
||||
|
||||
11 cms yml репойнтены на host IIS:8089. 2 stayer yml — `.disabled`.
|
||||
|
||||
| File | Hosts | Backend | Статус |
|
||||
|---|---|---|---|
|
||||
| snolla.yml | snolla.com + 10 *.snolla.com subdomains (rule explicit) | host.docker.internal:8089 | active → host IIS |
|
||||
| rimiz.yml | rimiz.ru, www.rimiz.ru | host.docker.internal:8089 | active (но CMS-side 404 — known) |
|
||||
| labtools.yml | labtools.ru, www.labtools.ru | host.docker.internal:8089 | active → host IIS |
|
||||
| labtoolspro.yml | labtools.pro, www.labtools.pro | host.docker.internal:8089 | active → host IIS |
|
||||
| pilorama98.yml | pilorama98.ru, www.pilorama98.ru | host.docker.internal:8089 | active → host IIS |
|
||||
| tandemmebel.yml | tandemmebel.ru, www.tandemmebel.ru | host.docker.internal:8089 | active → host IIS |
|
||||
| emspb.yml | emspb.ru, www.emspb.ru | host.docker.internal:8089 | active → host IIS (canary 1, phone-test ✅) |
|
||||
| kupimknigi.yml | kupimknigi.spb.ru | host.docker.internal:8089 | active → host IIS |
|
||||
| maljarka.yml | maljarka.tandemmebel.ru | host.docker.internal:8089 | active → host IIS |
|
||||
| sestech.yml | sestech.ru, www.sestech.ru | host.docker.internal:8089 | active → host IIS |
|
||||
| isc-artmaterials.yml | ics-artmaterials.com, www.ics-artmaterials.com | host.docker.internal:8089 | active → host IIS (CMS-side 404 — known) |
|
||||
| **stostayer.yml.disabled** | stostayer.snolla.com | (n/a, route disabled) | **DISABLED**, host IIS :8090 локально |
|
||||
| **oldstostayer.yml.disabled** | old.stostayer.ru | (n/a, route disabled) | **DISABLED**, host IIS :8091 локально |
|
||||
|
||||
**Backup-stamp файлы** (накопились за обе попытки):
|
||||
- `*.yml.bak-2026-05-19` — самый ранний backup (до session).
|
||||
- `*.yml.bak-phase3-2026-05-19` — rollback baseline (attempt 1 → revert state, всё на VM `:18080`).
|
||||
- `*.yml.bak-hostip-2026-05-19` — failed attempt 1 (host.docker.internal:80 ⇒ Docker NAT loop).
|
||||
- `*.yml.bak-stayer-switch-2026-05-19` — stayer switch attempt artefact (Phase 5/6 prev session).
|
||||
- **`*.yml.bak-pre-attempt2-2026-05-19`** — текущая live conf attempt 2 (host:8089 backend). Это baseline для **atomic revert** этой попытки.
|
||||
|
||||
Плюс file-provider маршруты для инфраструктурных хостов:
|
||||
|
||||
| File | Host | Backend |
|
||||
|---|---|---|
|
||||
| elasticsearch.yml | elasticold.kzntsv.site | http://elasticsearch:9200 + basicAuth `books:...` |
|
||||
| minio.yml | minio.kzntsv.site | http://minio:9000 |
|
||||
| imgproxy.yml | imgproxy.kzntsv.site | http://imgproxy-nginx:80 |
|
||||
|
||||
И мёртвые (не отключены, но смотрят в никуда):
|
||||
- `disk.yml` → 192.168.1.10:5005 (Synology disk на мёртвой синке)
|
||||
- `dsm.yml` → 192.168.1.10:5000 (DSM мёртвой синки)
|
||||
|
||||
## Host IIS configuration (active prod)
|
||||
|
||||
| Site | Bindings | Physical path | Pool identity | Прим. |
|
||||
|---|---|---|---|---|
|
||||
| **snolla** | `*:80`, `*:8089` | `C:\sites\snolla` | `ApplicationPoolIdentity` (.NET v4.0 Integrated) | **active prod** — catch-all для 11 cms hosts, traefik backend `:8089` |
|
||||
| **stostayer** | `*:8090` | `C:\sites\stostayer` | `ApplicationPoolIdentity` | local-only (traefik route disabled), conn → `www.stostayer.ru,1433` |
|
||||
| **stostayer.old** | `*:8091` | `C:\sites\stostayer.old` | `ApplicationPoolIdentity` | local-only (traefik route disabled), conn → `localhost,1433` |
|
||||
| Default Web Site | (stopped, autoStart=false) | — | — | — |
|
||||
|
||||
ACL: `IIS AppPool\<site>:(OI)(CI)M` рекурсивно на каждом site root.
|
||||
|
||||
## DNS
|
||||
|
||||
Все домены остались указывать на **94.19.247.14** (public IP, статический). REGRU как registrar/DNS.
|
||||
|
||||
## Backup инфраструктура
|
||||
|
||||
Текущая (на момент 2026-05-19, после attempt 2):
|
||||
|
||||
- [[kreknin-synology]] держит Hyper Backup репо мёртвой синки (~430 GB). Новых бэкапов с recovered Windows-PC **НЕТ**.
|
||||
- Локально на Windows-PC: `C:\nas-recovery\vm-sites\` (~11 GB, дамп IIS sites из VM до patches) — резерв если host-IIS сломается катастрофически. После 48h+ uptime можно почистить.
|
||||
- `C:\nas-recovery\backup\snolla\snolla.ova` (42.5 GB) — оригинал OVA. После cleanup VM можно удалить.
|
||||
|
||||
**Дыра:** если Windows-PC сгорит — всё ляжет. Никакой репликации, никакого off-site backup для нового рабочего состояния.
|
||||
|
||||
## SSH ключи и доступ
|
||||
|
||||
- [[windows-recovery-host]] → [[kreknin-synology]]: `id_ed25519_kreknin` (vitya@195.19.90.188)
|
||||
- [[windows-recovery-host]] → [[openwrt-router]]: `id_ed25519_openwrt` (root@192.168.1.1)
|
||||
- [[windows-recovery-host]] → [[snolla-recovery-vm]]: `id_ed25519_snolla_vm` (vitya@127.0.0.1:8022)
|
||||
|
||||
После recovery — отозвать публичные ключи Claude из этих 3 машин (`~/.ssh/authorized_keys` или эквивалент). См. соответствующие entity-страницы.
|
||||
|
||||
## Известные открытые баги
|
||||
|
||||
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]].
|
||||
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).
|
||||
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.
|
||||
7. **WinHTTP proxy на хосте strips response headers** для `curl.exe http://localhost:...` — для loop-detect/IIS confirmation использовать `docker exec traefik wget` или `Invoke-WebRequest`. См. [[docker-host-loopback-detect]].
|
||||
|
||||
## Single Points of Failure
|
||||
|
||||
- Один Windows-PC (если сгорит — всё ляжет)
|
||||
- Один публичный IP / провайдер
|
||||
- Один WiFi-канал
|
||||
- Один OpenWRT-роутер
|
||||
- Один **host IIS instance** обслуживает весь cms-трафик (VM остаётся parallel fallback ещё 24-48h)
|
||||
- Один MSSQL контейнер (single primary, нет replica)
|
||||
- Один MinIO (single drive, не distributed)
|
||||
|
||||
Каждый SPOF — кандидат на улучшение в [[future-resilient-architecture-goals]].
|
||||
115
.wiki/concepts/registry-gc-mount-and-modify-flag.md
Normal file
115
.wiki/concepts/registry-gc-mount-and-modify-flag.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Docker Registry garbage-collect mount layout + `-m` flag
|
||||
type: concept
|
||||
tags: [docker-registry, garbage-collect, gotcha, storage]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
updated: 2026-05-20
|
||||
---
|
||||
|
||||
# Registry GC — mount path и `-m` flag
|
||||
|
||||
Распознаваемая пара ошибок при `registry garbage-collect` на offline-restored backup data. Один shoot — `-m` flag для реального удаления, второй — правильный mount path.
|
||||
|
||||
## Симптом 1 — `Path not found: /docker/registry/v2/repositories`
|
||||
|
||||
Запуск:
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v /volume1/docker/infrastucture/registry/docker:/var/lib/registry \
|
||||
registry:2.8.3 garbage-collect /etc/docker/registry/config.yml
|
||||
```
|
||||
|
||||
Ошибка: `failed to garbage collect: failed to mark: filesystem: Path not found: /docker/registry/v2/repositories`.
|
||||
|
||||
### Root cause
|
||||
|
||||
Default config файл `/etc/docker/registry/config.yml` в registry image имеет:
|
||||
```yaml
|
||||
storage:
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/registry
|
||||
```
|
||||
|
||||
Registry expect'ит файлы в `/var/lib/registry/docker/registry/v2/...`. Mounting только `docker/` subdir host'а к `/var/lib/registry/` положит данные в `/var/lib/registry/registry/v2/...` (один уровень потерян).
|
||||
|
||||
### Fix — mount PARENT directory
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v /volume1/docker/infrastucture/registry:/var/lib/registry \ # <-- parent dir, не /docker
|
||||
registry:2.8.3 garbage-collect /etc/docker/registry/config.yml
|
||||
```
|
||||
|
||||
Теперь internal path = `/var/lib/registry/docker/registry/v2/...` ✅.
|
||||
|
||||
(Original kreknin compose использовал `./:/var/lib/registry` — mount whole registry parent dir — что было корректно но для running registry, не для offline GC.)
|
||||
|
||||
## Симптом 2 — GC ничего не удаляет, размер прежний
|
||||
|
||||
После первого GC прохода видим лог:
|
||||
```
|
||||
blob eligible for deletion: sha256:087b41...
|
||||
time="..." level=info msg="Deleting blob: /docker/registry/v2/blobs/sha256/08/087b41..."
|
||||
```
|
||||
|
||||
Лог говорит «Deleting» — но `du -sh` показывает **прежний 99G**. Размер не изменился.
|
||||
|
||||
### Root cause
|
||||
|
||||
`registry garbage-collect` без флагов работает в **dry-run mode** (incident-free). Лог «Deleting blob» — `info` level намерения, не actual unlink.
|
||||
|
||||
Подобный intent vs action разделение типично для batch tools (`apt-get -s`, `git rm --dry-run`, etc.) но в registry CLI нет attention-grabbing `--dry-run` flag — а опция «реально делать» named cryptically.
|
||||
|
||||
### Fix — `-m` (modify) flag
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v /volume1/docker/infrastucture/registry:/var/lib/registry \
|
||||
registry:2.8.3 garbage-collect -m /etc/docker/registry/config.yml
|
||||
```
|
||||
|
||||
С `-m` после прохода размер реально уменьшается. На kreknin backup'е: **99G → 35G** (64G freed), 2-3 минуты обработки.
|
||||
|
||||
## Полный рецепт offline GC restored backup data
|
||||
|
||||
```bash
|
||||
# 1. Pull registry image что соответствует production версии (compatibility)
|
||||
docker pull registry:2.8.3
|
||||
|
||||
# 2. (Optional) dry-run для отчёта — что будет удалено
|
||||
docker run --rm \
|
||||
-v /path/to/restored/registry:/var/lib/registry \
|
||||
registry:2.8.3 garbage-collect /etc/docker/registry/config.yml \
|
||||
> gc-dry-run.log 2>&1
|
||||
|
||||
# 3. Реальный GC
|
||||
docker run --rm \
|
||||
-v /path/to/restored/registry:/var/lib/registry \
|
||||
registry:2.8.3 garbage-collect -m /etc/docker/registry/config.yml
|
||||
|
||||
# 4. Measure delta
|
||||
du -sh /path/to/restored/registry/docker
|
||||
```
|
||||
|
||||
## Online GC (production live registry) — важные дополнения
|
||||
|
||||
Если делаем GC на **running** registry — нужен read-only mode чтобы избежать race condition'ов (новый push во время GC может потерять blobs):
|
||||
|
||||
```yaml
|
||||
# Add to running registry config / env vars
|
||||
storage:
|
||||
maintenance:
|
||||
readonly:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
Затем restart registry, run GC `-m`, отключить read-only, restart again. Окно downtime — продолжительность GC (~10-30 min на средних объёмах).
|
||||
|
||||
## Где применено
|
||||
|
||||
[`vds-kzntsv`](../entities/vds-kzntsv.md) Phase 3.3 — GC kreknin'овского backup'а 99G → 35G. После того как user decided abandon миграцию и fresh install — GC оказался полезной экономией если бы tar/rsync'или (но в финале rsync не запустился).
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Registry docs: [Garbage collection](https://distribution.github.io/distribution/about/garbage-collection/)
|
||||
- Сессия: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 3.3.
|
||||
92
.wiki/concepts/rusonyx-vps-onboarding-quirks.md
Normal file
92
.wiki/concepts/rusonyx-vps-onboarding-quirks.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: Rusonyx VPS onboarding quirks (Astra Облако / myvm.rusonyx.ru)
|
||||
type: concept
|
||||
tags: [rusonyx, vds, bootstrap, onboarding, gotchas, vendor]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
updated: 2026-05-20
|
||||
---
|
||||
|
||||
# Rusonyx VPS onboarding quirks
|
||||
|
||||
Quirks first-boot опыта на Rusonyx VDS (Astra Облако, https://myvm.rusonyx.ru) — для memory будущих сессий. Зафиксировано при активации [`vds-kzntsv`](../entities/vds-kzntsv.md) 2026-05-20.
|
||||
|
||||
## 1. VNC console не открывается с первого раза
|
||||
|
||||
В Управление сервером → Консоль HTML5 noVNC может не загружаться (зависание на индикаторе соединения). Помогает кнопка **«Остановить VNC»** в той же панели — force-disconnect stale attachment'а на hypervisor side. После клика подождать ~5 сек, открыть консоль заново.
|
||||
|
||||
Если не помогает — ticket в support, без VNC реальной возможности зайти в box нет (см. quirk 2).
|
||||
|
||||
## 2. Stale системный образ Ubuntu — обязательный upgrade через VNC
|
||||
|
||||
Поставка содержит сотни pending updates, включая `openssh-server` и kernel. Письмо при активации **прямо рекомендует** последовательность:
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt upgrade
|
||||
apt --fix-broken install
|
||||
apt upgrade
|
||||
```
|
||||
|
||||
И ключевая часть: **«строго через VNC-консоль»**, потому что `openssh-server` upgrade'у переключают сервис, что разрывает SSH session и бьёт upgrade на половине.
|
||||
|
||||
По дороге будет **5+ dpkg interactive prompts** (conffile conflicts). Ответы:
|
||||
|
||||
| Prompt | Правильный ответ | Reasoning |
|
||||
|---|---|---|
|
||||
| `/etc/ssh/sshd_config` | **2** (keep local) | Rusonyx-modified sshd_config форсит `PermitRootLogin yes` + `PasswordAuthentication yes` для первого захода. Maintainer-версия дефолтит `prohibit-password` — потеряешь SSH-доступ если ключ ещё не залит. |
|
||||
| `/etc/cloud/cloud.cfg` | **N** (keep current) | Rusonyx модифицировал под свой provisioning (network, ssh-key inject). Maintainer-версия может сломать их хуки. |
|
||||
| `grub-pc /dev/vdaX target` | **1** (`/dev/vda` whole-disk MBR) | Опция 2 (на partition) — blocklist mechanism, less reliable. Опция 3 (skip) = box не загрузится. |
|
||||
| `cloud-init local config` (если появится) | keep | По той же логике. |
|
||||
|
||||
После завершения — `reboot` (или ждать пока apt сам запустит).
|
||||
|
||||
## 3. SSH initial password — одноразовый
|
||||
|
||||
Активационное письмо даёт `root` + одноразовый пароль. Первый шаг bootstrap'а (после VNC-upgrade) — push своего SSH key и harden sshd:
|
||||
|
||||
```
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
```
|
||||
|
||||
Через **drop-in file `/etc/ssh/sshd_config.d/00-hardening.conf`** (префикс `00-` чтобы выиграть first-match precedence над `50-cloud-init.conf` который форсит `PasswordAuthentication yes`).
|
||||
|
||||
## 4. Подключение через SSH с password на Windows
|
||||
|
||||
OpenSSH for Windows **не имеет** `-pw` flag (как `plink`/PuTTY) или `sshpass`. Для одноразового password-bootstrap:
|
||||
|
||||
- **plink.exe** (`C:\Program Files\PuTTY\plink.exe`) — supports `-pw`, доступен если PuTTY установлен.
|
||||
- Pipe `echo y | plink ...` для auto-accept first-time host key (plink кэширует в registry).
|
||||
- После push pubkey → переключаемся на native OpenSSH `ssh -i` (plink больше не нужен).
|
||||
|
||||
## 5. Hypervisor-side VNC ≠ guest-side VNC service
|
||||
|
||||
В гостевой системе **нет** VNC service'а — VNC console работает на qemu/KVM hypervisor side. Поэтому request «зайди и передёрни vnc service из гостя» невозможен. Управляется только через Rusonyx web panel.
|
||||
|
||||
## 6. Default firewall — open?
|
||||
|
||||
Из наблюдений 2026-05-20: после reboot SSH:22 поднимался автоматом, не было видно guest-side ufw default-deny. Но **ICMP ping проходил до VNC-fix**, при том что **все TCP-порты были filtered**. Похоже, Rusonyx имеет perimeter firewall, который автоматически allow'ит TCP только когда VPS становится «active» в их учёте — корреляция с моментом первой VNC-сессии. Не воспроизводимо post-factum; для будущих VDS — стоит сначала открыть VNC, потом ждать что SSH станет доступен.
|
||||
|
||||
## 7. Tariff именования
|
||||
|
||||
«160 SSD» переименован в «160 NVMe» (та же цена, апгрейд по IOPS). Если в старой переписке/доках видите «160 SSD» — это actually 160 NVMe. Аналогично могут быть переименования для 80/220+ тарифов.
|
||||
|
||||
## 8. Welcome-email рекомендации
|
||||
|
||||
Содержит только команды apt upgrade, **не упоминает**:
|
||||
- Что VNC может быть stale.
|
||||
- Какие dpkg prompts и правильные ответы.
|
||||
- Initial password — одноразовый, нужно сменить на ключ.
|
||||
|
||||
См. также: support-ticket draft в [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) с конкретными suggestions для Rusonyx.
|
||||
|
||||
## Когда применять
|
||||
|
||||
- Любой новый Rusonyx VDS (Astra Облако).
|
||||
- Возможно частично применимо к другим Russian VDS-providers с custom OS templates (REG.RU, FirstByte, RuVDS), но конкретные dpkg prompt'ы вендор-specific.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) — где впервые столкнулись.
|
||||
- [`vds-kzntsv`](../entities/vds-kzntsv.md) — текущий live host.
|
||||
68
.wiki/concepts/traefik-file-watch-wsl2-broken.md
Normal file
68
.wiki/concepts/traefik-file-watch-wsl2-broken.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Traefik file-watch broken under Docker Desktop Windows (WSL2 9p mount)
|
||||
type: concept
|
||||
tags: [traefik, docker-desktop, windows, gotcha, wsl2]
|
||||
sources: []
|
||||
updated: 2026-05-21
|
||||
---
|
||||
|
||||
# Traefik file-watch broken под Docker Desktop Windows
|
||||
|
||||
Traefik file provider's `watch: true` **не работает** для bind mounts из Windows host через Docker Desktop WSL2 9p (виртуальная файловая система). Изменения на disk **не доходят** до traefik. Config остаётся frozen на startup state до явного `docker restart traefik`.
|
||||
|
||||
## Симптомы
|
||||
|
||||
1. Rename `.yml → .yml.disabled` — route ОСТАЁТСЯ active в traefik runtime, продолжает отвечать.
|
||||
2. Edit content of `.yml` — изменения не подхватываются, runtime использует старый snapshot.
|
||||
3. New `.yml` файл в `/custom/` — игнорируется, route не добавляется.
|
||||
4. `touch` обновление mtime — нет reload.
|
||||
5. В logs (`--log.level=DEBUG`) — никаких "Configuration reloaded" сообщений.
|
||||
|
||||
## Root cause
|
||||
|
||||
Docker Desktop на Windows монтирует bind volumes через WSL2 9p protocol (`/run/desktop/mnt/host/c/...`). 9p **не пропагирует inotify events** — fsnotify watchers внутри контейнера не получают уведомлений об изменениях. Traefik file-watcher использует `fsnotify` → молчит.
|
||||
|
||||
Это известная архитектурная проблема Docker Desktop Windows. Linux native Docker, Docker on macOS (через osxfs/virtiofs новый) — работают по-разному.
|
||||
|
||||
## Подтверждение
|
||||
|
||||
```powershell
|
||||
# 1. Mount bind path inside traefik
|
||||
docker inspect traefik --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
|
||||
# Покажет: /run/desktop/mnt/host/c/... -> /custom/ <-- WSL2 9p
|
||||
|
||||
# 2. Rename one yml to .disabled, проверить route ещё активный
|
||||
docker exec traefik wget --header='Host: <some-host-in-disabled-yml>' --spider https://traefik/
|
||||
# 200 OK даже после rename (т.к. config не перезагружен)
|
||||
|
||||
# 3. После docker restart traefik — то же запрос вернёт 404
|
||||
docker restart traefik
|
||||
docker exec traefik wget --header='Host: <some-host-in-disabled-yml>' --spider https://traefik/
|
||||
# 404 ✅
|
||||
```
|
||||
|
||||
## Последствия
|
||||
|
||||
- **Любое изменение в `/custom/*.yml` требует `docker restart traefik`.**
|
||||
- Atomic revert: backup .yml → restart → rollback означает restore + restart.
|
||||
- "Hot reload" workflow невозможен под этой config'ом — нужно или native Linux Docker, или migrate на docker provider via labels (отдельные изменения сразу видны через container restart events).
|
||||
|
||||
## Workarounds
|
||||
|
||||
1. **Always restart traefik after config changes** — single source of truth для team is restart, не file-edit. Документировать.
|
||||
2. **Periodic auto-restart** — cron внутри traefik container (`docker exec traefik <restart-mechanism>`), e.g. каждый час. Кustомные disruption для уже работающих routes.
|
||||
3. **Migrate to docker provider** (labels) — labels на сервисах меняются вместе с container restart, traefik догоняет docker events корректно. Big migration работа.
|
||||
4. **Use traefik file provider's `pollInterval`** — НЕ поддерживается в file provider (только в HTTP provider). Не вариант.
|
||||
5. **Move traefik в WSL native** — запускать traefik внутри WSL2 distro (Ubuntu), mount /etc/traefik внутри WSL native fs, traefik видит inotify нормально. Требует переезд compose stack в WSL.
|
||||
|
||||
## Применено
|
||||
|
||||
[`traefik-maljarka-502-bug`](../../.tasks/traefik-maljarka-502-bug.md) — обнаружено при debugging. После рестарта traefik 2026-05-21:
|
||||
- 2 dead routes (sestech, ics-artmaterials) реально 404'нулись (до restart были active despite .disabled rename).
|
||||
- maljarka 502 не ушло — другая root cause (CMS-side HTTPS-mode crash для maljarka.tandemmebel.ru, см. cms-maljarka-https-mode-bug если создана).
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Docker Desktop Windows mount perf: https://docs.docker.com/desktop/windows/wsl/
|
||||
- fsnotify limitations: https://github.com/fsnotify/fsnotify/issues/611 (9p/WSL2)
|
||||
- Setup на этом стэке: [[traefik-on-windows-docker-desktop]]
|
||||
171
.wiki/concepts/traefik-on-windows-docker-desktop.md
Normal file
171
.wiki/concepts/traefik-on-windows-docker-desktop.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: Traefik на Windows Docker Desktop — нюансы
|
||||
type: concept
|
||||
tags: [traefik, docker, windows, letsencrypt, reverse-proxy]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Traefik на Windows Docker Desktop
|
||||
|
||||
Запуск traefik 2.6.6 на Windows Docker Desktop (WSL2 backend) с импортированным production-конфигом и сертификатами — пара ловушек.
|
||||
|
||||
## Pitfall 1: traefik.yml не загружается автоматом
|
||||
|
||||
Symptom: traefik стартует, но routers из `data/custom/*.yml` ругаются на "non-existent resolver: letsEncrypt", сертификаты не отдаются.
|
||||
|
||||
Reason: traefik 2.x при отсутствии `--configFile=` ищет в дефолтных путях (`/etc/traefik/`, `./traefik.yml`), но Linux-контейнер с CWD=`/` и bind-mount `./data/traefik.yml:/traefik.yml` — почему-то не подхватывает.
|
||||
|
||||
**Fix:** явно указать в compose `command:`:
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- "--configFile=/traefik.yml"
|
||||
- "--log.level=DEBUG"
|
||||
```
|
||||
|
||||
Подтверждение в логах: `Configuration loaded from file: /traefik.yml`.
|
||||
|
||||
## Pitfall 2: acme.json permissions через bind-mount
|
||||
|
||||
Symptom: traefik ругается `permissions 777 for /letsencrypt/acme.json are too open, please use 600` и **отключает** letsEncrypt resolver (даже если у него есть валидные certs).
|
||||
|
||||
Reason: bind-mount Windows-файла в Linux-контейнере **всегда** показывает permissions `0777`. Изменить через `chmod` нельзя — bind-mount не транслирует POSIX-perms на Windows-side.
|
||||
|
||||
**Fix:** named volume вместо bind для `/letsencrypt`:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "traefik_letsencrypt:/letsencrypt" # вместо ./letsencrypt:/letsencrypt
|
||||
- "./data/traefik.yml:/traefik.yml:ro"
|
||||
- "./data/custom/:/custom/:ro"
|
||||
|
||||
# и снизу:
|
||||
volumes:
|
||||
traefik_letsencrypt:
|
||||
```
|
||||
|
||||
Population volume единоразово:
|
||||
|
||||
```powershell
|
||||
docker volume create traefik_traefik_letsencrypt
|
||||
docker run --rm \
|
||||
-v traefik_traefik_letsencrypt:/dest \
|
||||
-v <local-path>/traefik/letsencrypt:/src:ro \
|
||||
alpine sh -c "cp /src/acme.json /dest/acme.json && cp /src/acme.old.json /dest/acme.old.json && chmod 600 /dest/*"
|
||||
```
|
||||
|
||||
## Pitfall 3: docker.sock provider не работает
|
||||
|
||||
Symptom:
|
||||
```
|
||||
Failed to retrieve information of the docker client and server host: Error response from daemon:
|
||||
providerName=docker
|
||||
```
|
||||
|
||||
С `-v /var/run/docker.sock:/var/run/docker.sock:ro` сокет монтируется (видно `srw-rw---- 1 root root` внутри контейнера), но соединение с daemon обрывается.
|
||||
|
||||
Reason: Docker Desktop на Windows транслирует docker.sock через WSL2 layer. Иногда permission-моэль клиента (libdocker) не принимает то, что предоставляет Desktop's proxy.
|
||||
|
||||
**Workaround:** **полностью отключить docker-provider, использовать только file-provider** для всех routes.
|
||||
|
||||
Маршруты, которые в производстве были как traefik labels на контейнерах (minio, elasticsearch, imgproxy с `traefik.http.routers...labels`), переписываются в `data/custom/<name>.yml` руками:
|
||||
|
||||
```yaml
|
||||
# data/custom/minio.yml
|
||||
http:
|
||||
routers:
|
||||
minio:
|
||||
entryPoints: [https]
|
||||
rule: Host(`minio.kzntsv.site`)
|
||||
tls:
|
||||
certResolver: letsEncrypt
|
||||
service: minio
|
||||
services:
|
||||
minio:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://minio:9000 # docker DNS name (работает потому что все на одной network=proxy)
|
||||
```
|
||||
|
||||
Подобно для elasticsearch (с basicAuth middleware), imgproxy (→ imgproxy-nginx:80), etc.
|
||||
|
||||
## Pitfall 4: file-provider не подхватывает изменения через bind-mount
|
||||
|
||||
`traefik.yml` имеет `providers.file.watch: true`, но на Windows bind-mount inotify не работает через WSL2-слой. Изменения в `data/custom/*.yml` не подхватываются автоматически.
|
||||
|
||||
**Workaround:** `docker compose restart traefik` после изменений в custom/. Несколько секунд downtime, ничего страшного.
|
||||
|
||||
## Production порты vs нашa конфигурация
|
||||
|
||||
Production traefik compose биндил на host:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "8000:80" # router forwards public 80 → host 8000 → container 80
|
||||
- "4443:443"
|
||||
- "8080:8080"
|
||||
```
|
||||
|
||||
То есть **роутер делает port-translation** 80→8000, 443→4443. Не стандартные порты, но работает.
|
||||
|
||||
На Windows-хосте оставили те же порты (8000/4443) — не конфликтуют с локальным IIS на 80 (который не используется, но не выключен). И не требуют admin для bind.
|
||||
|
||||
## host.docker.internal
|
||||
|
||||
Из traefik-контейнера достучаться до VM (которая в VBox NAT, не в docker network) — через **`host.docker.internal`**:
|
||||
|
||||
```yaml
|
||||
servers:
|
||||
- url: http://host.docker.internal:18080/ # 18080 = VBox NAT-forwarded port → VM:80
|
||||
```
|
||||
|
||||
Docker Desktop резолвит `host.docker.internal` в IP хоста (обычно 172.x.x.1 из docker bridge perspective). Дальше Windows-host обрабатывает 18080 → VBox NAT → VM:80 → IIS → CMS.
|
||||
|
||||
## Pitfall 5: X-Forwarded не используется ASP.NET — port utечка в admin URLs (RESOLVED 2026-05-19)
|
||||
|
||||
**Симптом:** CMS-admin генерирует URLs с internal IIS portом (`:8089` после attempt 2; ранее `:4443` от traefik) → browser HSTS auto-upgrade ломает TLS на нестандартном портe → admin SPA broken.
|
||||
|
||||
**Root cause:** Traefik 2.x **шлёт** `X-Forwarded-Proto: https` / `X-Forwarded-Host` / `X-Forwarded-Port` по-default (когда entrypoint https). НО CMS-код (`MoreThenCms.Admin\Mis\Web\Mvc\UrlHelpers\UrlHelpers.cs`) читает **socket-level** `SERVER_PORT` / `SERVER_PORT_SECURE`, **игнорируя** X-Forwarded-* headers. На VM работало случайно потому что IIS binding `:80` → `SERVER_PORT=80` стрипалось whitelist'ом `{80,443}` в коде.
|
||||
|
||||
**Fix:** URL Rewrite 2.1 + `<serverVariables>` rule на host IIS — переписывает `HTTPS`/`SERVER_PORT`/`SERVER_PORT_SECURE` на основе `X-Forwarded-Proto`. Детально в [[cms-server-port-leak-fix]].
|
||||
|
||||
Path B (поменять traefik http entrypoint :80 inside container чтобы IIS-binding :80 не получал loop от Docker NAT) — **не работает** на Windows Docker Desktop из-за WSL2 NAT quirk, см. там же.
|
||||
|
||||
## DNS-01 challenge через REGRU
|
||||
|
||||
`traefik.yml` имеет HTTP-01 challenge:
|
||||
|
||||
```yaml
|
||||
certificatesResolvers:
|
||||
letsEncrypt:
|
||||
acme:
|
||||
email: vitya.kuznetsov@gmail.com
|
||||
storage: /letsencrypt/acme.json
|
||||
httpChallenge:
|
||||
entryPoint: http
|
||||
```
|
||||
|
||||
В compose env уже:
|
||||
```
|
||||
REGRU_USERNAME=OpeItcLoc03
|
||||
REGRU_PASSWORD=ytyYqC%u%QAJ
|
||||
```
|
||||
|
||||
Эти креды для **DNS-01** через REGRU API. Просто закомментировать `httpChallenge` и активировать `dnsChallenge` в traefik.yml:
|
||||
|
||||
```yaml
|
||||
certificatesResolvers:
|
||||
letsEncrypt:
|
||||
acme:
|
||||
email: ...
|
||||
storage: /letsencrypt/acme.json
|
||||
dnsChallenge:
|
||||
provider: regru
|
||||
```
|
||||
|
||||
DNS-01 более надёжный (не требует public:80 reachable для validation) и работает даже если HTTP-01 challenge не пройдёт (например, домен временно на другой хостинге).
|
||||
|
||||
**Текущие 40 LE-сертификатов из acme.json валидны ~3 месяца** (LE default). Когда подойдут к истечению — пора переключать на DNS-01.
|
||||
|
||||
Связано: [[snolla-recovery-vm]], [[windows-recovery-host]], [[recovery-architecture-snapshot]].
|
||||
66
.wiki/concepts/traefik-tcp-passthrough-vs-starttls.md
Normal file
66
.wiki/concepts/traefik-tcp-passthrough-vs-starttls.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Traefik TCP passthrough vs STARTTLS-protocols
|
||||
type: concept
|
||||
tags: [traefik, tls, tcp, sni, postgres, mariadb, mongo, redis, gotcha]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
updated: 2026-05-20
|
||||
---
|
||||
|
||||
# Traefik TCP passthrough не работает с STARTTLS
|
||||
|
||||
## Симптом
|
||||
|
||||
Traefik TCP router с `HostSNI(<hostname>)` + `tls.passthrough=true`. Mongo / Redis (TLS-from-start) — TLS handshake проходит, виден правильный cert. **Postgres / MariaDB — TLS connection hangs / timeout**, никакого handshake'а не происходит.
|
||||
|
||||
Probe `openssl s_client -connect postgres.vds.kzntsv.site:5432 -servername postgres.vds.kzntsv.site -starttls postgres` → timeout 10s.
|
||||
|
||||
## Root cause
|
||||
|
||||
`tls.passthrough=true` означает: traefik не терминирует TLS, а маршрутизирует TCP-connection raw. Для маршрутизации по `HostSNI` traefik читает SNI **из TLS ClientHello** — первого TLS-сообщения, отправляемого клиентом сразу после TCP handshake.
|
||||
|
||||
**Mongo / Redis** (TLS-from-start) — клиент шлёт TLS ClientHello как первое сообщение. SNI там присутствует. Traefik читает, маршрутизирует, остаток TCP forwarding'ом. Работает.
|
||||
|
||||
**Postgres / MariaDB / MySQL** — используют **STARTTLS pattern**:
|
||||
1. Клиент после TCP-handshake шлёт `SSLRequest` (plaintext, специфичный для протокола).
|
||||
2. Сервер отвечает `'S'` (готов на TLS).
|
||||
3. **Только после этого** клиент начинает TLS ClientHello.
|
||||
|
||||
Первые байты от клиента — **plaintext protocol bytes**, не TLS ClientHello. Traefik ищет SNI в TLS ClientHello, не находит → не может смаршрутизировать → connection hangs (висит на чтении от клиента).
|
||||
|
||||
## Решение — Raw TCP forward + HostSNI(*)
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- "traefik.tcp.routers.postgres.rule=HostSNI(`*`)"
|
||||
- traefik.tcp.routers.postgres.entrypoints=postgres
|
||||
- traefik.tcp.routers.postgres.service=postgres
|
||||
- traefik.tcp.services.postgres.loadbalancer.server.port=5432
|
||||
# NO tls.* — traefik forwards raw TCP без inspect
|
||||
```
|
||||
|
||||
Каждая DB — на **своей dedicated entrypoint port** (`postgres:5432`, `mariadb:3306`, `mongo:27017`, `redis:6379`). Routing — по entrypoint, не по SNI. Traefik просто forwards bytes к backend без чтения TLS ClientHello. STARTTLS protocols договариваются о TLS напрямую с DB-контейнером.
|
||||
|
||||
`HostSNI(\`*\`)` — единственное значение для TCP router'а **без** `tls.*` секции (traefik требует какое-то правило, wildcard catch-all уместен здесь).
|
||||
|
||||
## Trade-off
|
||||
|
||||
- ✔ Работает для всех 4 protocols (STARTTLS + TLS-from-start).
|
||||
- ✔ Uniform config, не нужно ветвить compose под тип protocol'а.
|
||||
- ✗ Каждая DB требует своего entrypoint:port в traefik static config. 4 DB → 4 entrypoints. Если хотим много инстансов одного движка — теряем muxing на один port через SNI.
|
||||
- ✗ Traefik в этом случае не видит контент трафика — только TCP-bytes мимо. Невозможно сделать middleware (rate limit, IP allowlist на уровне traefik) — это нужно делать на DB side.
|
||||
|
||||
## Альтернативный путь (отвергнут)
|
||||
|
||||
Traefik TLS termination (без passthrough) + SNI muxing на 443 entrypoint. Не работает для PG/MariaDB по той же причине — STARTTLS встроен в protocol stack, и traefik терминирующий TLS отдаёт plaintext бэкенду, который ждёт SSLRequest и предлагает свой TLS handshake. Двойная TLS обёртка вокруг STARTTLS — broken.
|
||||
|
||||
LE-cert provisioning при passthrough TCP — на DB side, не traefik. Сейчас self-signed; follow-up — lego sidecar extracting из traefik acme.json.
|
||||
|
||||
## Где применено
|
||||
|
||||
Все 4 DBs на [`vds-kzntsv`](../entities/vds-kzntsv.md): postgres:5432, mariadb:3306, mongo:27017, redis:6379. Traefik v2.11 LTS. См. также [`db-tls-self-signed-via-traefik-raw-tcp`](db-tls-self-signed-via-traefik-raw-tcp.md) — связанный паттерн self-signed cert provisioning'а.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Traefik docs: [TCP routers](https://doc.traefik.io/traefik/routing/routers/#configuring-tcp-routers)
|
||||
- Сессия где обнаружили: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 2.
|
||||
140
.wiki/concepts/vbox-windows-stability-tuning.md
Normal file
140
.wiki/concepts/vbox-windows-stability-tuning.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: VirtualBox + Windows-гость — нюансы стабильности при cross-hypervisor миграции
|
||||
type: concept
|
||||
tags: [virtualbox, windows, kvm, migration, troubleshooting]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# VirtualBox + Windows-гость: cross-hypervisor migration
|
||||
|
||||
Импорт OVA с Synology VMM (KVM-based) в VirtualBox прошёл через 3 круга проблем. Записано чтобы в следующий раз не танцевать. Касается [[snolla-recovery-vm]].
|
||||
|
||||
## Симптом исходный
|
||||
|
||||
OVA импортируется через `VBoxManage import`, VM стартует, Windows валится в WinRE ("Восстановление при загрузке не удалось восстановить компьютер"). Startup Repair не помогает.
|
||||
|
||||
## Цепочка фиксов (применять по порядку)
|
||||
|
||||
### 1. Storage controller: SCSI LsiLogic → SATA AHCI
|
||||
|
||||
OVA с KVM-источника обычно имеет SCSI LsiLogic. Windows-гость не имеет встроенного boot-driver для VBox's LsiLogic emulation. SATA AHCI — generic, поддерживается любым Windows из коробки.
|
||||
|
||||
```
|
||||
VBoxManage controlvm "<vm>" poweroff
|
||||
VBoxManage storageattach "<vm>" --storagectl "SCSI" --port 0 --device 0 --medium none
|
||||
VBoxManage storagectl "<vm>" --name "SCSI" --remove
|
||||
VBoxManage storagectl "<vm>" --name "SATA" --add sata --controller IntelAhci --portcount 4
|
||||
VBoxManage storageattach "<vm>" --storagectl "SATA" --port 0 --device 0 --type hdd --medium "<vmdk-path>"
|
||||
```
|
||||
|
||||
После этого Windows бутится дальше WinRE → доходит до login screen.
|
||||
|
||||
### 2. Hyper-V Virtualization Infrastructure Driver — disable в Safe Mode
|
||||
|
||||
После login Windows зависает в **чёрный экран после Welcome**. Виновник — Microsoft Hyper-V virtualization infrastructure driver, который остался от Synology VMM/KVM. Под VBox он не находит свой target hypervisor → виснет.
|
||||
|
||||
Доступ: жмёшь power → Shift+Restart → Recovery → Troubleshoot → Advanced Options → Startup Settings → Restart → **4 (Safe Mode)** или **5 (Safe Mode with Networking)**.
|
||||
|
||||
В Safe Mode:
|
||||
- Device Manager → Системные устройства → **Драйвер инфраструктуры виртуализации Microsoft Hyper-V** → правый клик → **Отключить устройство** (Disable, не удалять — потом можно вернуть).
|
||||
- Параллельно почистить "Другие устройства" с жёлтыми треугольниками (audio-controller, base system device) — uninstall, без удаления драйверов.
|
||||
|
||||
Reboot нормально → Windows загружается до desktop.
|
||||
|
||||
### 3. Paravirt provider: default → kvm
|
||||
|
||||
После пары часов uptime VM начинает виснуть рандомно. Корень — несоответствие paravirt-интерфейса между source-гипервизором (Synology VMM = KVM) и VBox default (`default`/`auto`, который пытается подружиться с гостем но не угадывает).
|
||||
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --paravirtprovider kvm
|
||||
```
|
||||
|
||||
Windows-гость, который изначально загружал KVM paravirt drivers (virtio?), теперь под VBox видит знакомый интерфейс → стабильнее.
|
||||
|
||||
### 4. OS type: Other_64 → Windows10_64
|
||||
|
||||
VBox по умолчанию ставит OS type = `Other/Unknown (64-bit)` при импорте OVA с неузнанной маркировкой. Это означает дефолтные acceleration settings, которые могут не подходить Windows.
|
||||
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --ostype Windows10_64
|
||||
```
|
||||
|
||||
Включает VBox-внутренние оптимизации для Windows (HPET off, large pages on, и др.).
|
||||
|
||||
### 5. HPET off, vCPU 2 (а не 4), RAM 4 GB
|
||||
|
||||
- HPET (High Precision Event Timer) — для Windows-гостя на VBox чаще создаёт jitter чем помогает. Off:
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --hpet off
|
||||
```
|
||||
- vCPU: 4 на 2-ядерном/4-ядерном хосте может создавать contention. Снизить до 2:
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --cpus 2
|
||||
```
|
||||
- RAM: 4 GB достаточно для IIS + CMS + Windows на легкой нагрузке.
|
||||
|
||||
### 6. Установить **VirtualBox Guest Additions**
|
||||
|
||||
Без GA Windows использует generic Microsoft драйверы для VBox-эмулированного железа. С GA — нативные оптимизированные VBox-драйверы для сети/видео/storage/устройств.
|
||||
|
||||
**Установить через VRDE-консоль** (mstsc к `localhost:13389`), а не через сетевой RDP — потому что сеть может умереть до установки GA.
|
||||
|
||||
Шаги:
|
||||
1. На хосте: `VBoxManage storagectl "<vm>" --name "IDE" --add ide --controller PIIX4` (новый контроллер для DVD)
|
||||
2. `VBoxManage storageattach "<vm>" --storagectl "IDE" --port 0 --device 0 --type dvddrive --medium "C:\Program Files\Oracle\VirtualBox\VBoxGuestAdditions.iso"`
|
||||
3. В VM: Win+E → D: drive → запустить `VBoxWindowsAdditions.exe` → Next/Install/доверять Oracle publisher → Restart.
|
||||
|
||||
После: `GuestAdditionsRunLevel=3` (полностью активны). VM существенно стабильнее.
|
||||
|
||||
## Network nuances
|
||||
|
||||
### Bridged WiFi нестабильно
|
||||
|
||||
Если хост-машина подключена по WiFi и VBox NIC = bridged через WiFi adapter — частые проблемы с promiscuous mode. Симптомы:
|
||||
- VM получает IP по DHCP
|
||||
- Работает 10-60 минут
|
||||
- Network "повисает": TCP-handshake проходит, но трафик не идёт
|
||||
- ARP-table показывает MAC, но State=`Stale`
|
||||
|
||||
Решение: **NAT с port forwarding** вместо bridged.
|
||||
|
||||
```
|
||||
VBoxManage modifyvm "<vm>" --nic1 nat
|
||||
VBoxManage controlvm "<vm>" natpf1 "rdp,tcp,127.0.0.1,23389,,3389"
|
||||
VBoxManage controlvm "<vm>" natpf1 "ssh,tcp,127.0.0.1,8022,,22"
|
||||
VBoxManage controlvm "<vm>" natpf1 "http,tcp,127.0.0.1,18080,,80"
|
||||
# ...и так далее на нужные порты
|
||||
```
|
||||
|
||||
VM получает 10.0.2.15 (default NAT subnet). Host достижим из VM по 10.0.2.2 (NAT gateway).
|
||||
|
||||
### Network recovery inside Windows VM
|
||||
|
||||
Если внутри VM network "повис" (бывает даже с GA + NAT):
|
||||
|
||||
```
|
||||
VBoxManage guestcontrol "<vm>" run --exe "C:\Windows\System32\cmd.exe" \
|
||||
--username vitya --password '<pw>' --wait-stdout --wait-stderr \
|
||||
-- cmd.exe /c "ipconfig /release && ipconfig /renew"
|
||||
```
|
||||
|
||||
Через GA это работает не требуя SSH/RDP связи.
|
||||
|
||||
## VRDE backup-доступ
|
||||
|
||||
Всегда включён в нашей конфигурации как fallback:
|
||||
|
||||
```
|
||||
VBoxManage controlvm "<vm>" vrde on
|
||||
VBoxManage controlvm "<vm>" vrdeport 13389
|
||||
```
|
||||
|
||||
`mstsc → localhost:13389` показывает VM-консоль независимо от состояния сети в VM. Полезно для recovery когда RDP в VM умер.
|
||||
|
||||
## Что не сработало
|
||||
|
||||
- Hyper-V на хосте — рассматривался как cleaner альтернатива для Windows-гостя, но требует доустановки Hyper-V Manager и перезагрузки хоста. Отложено как Plan B, не понадобилось.
|
||||
- VMware Workstation Pro 17 — бесплатен с 2024, но та же migration-головная боль на Windows-госте с другого гипервизора.
|
||||
|
||||
Связано: [[snolla-recovery-vm]], [[windows-recovery-host]].
|
||||
80
.wiki/concepts/verdaccio-prune-semantics.md
Normal file
80
.wiki/concepts/verdaccio-prune-semantics.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Verdaccio prune semantics — proxied vs locally-published
|
||||
type: concept
|
||||
tags: [verdaccio, npm, storage, gc, gotcha]
|
||||
sources: []
|
||||
updated: 2026-05-21
|
||||
---
|
||||
|
||||
# Verdaccio prune — proxied vs locally-published
|
||||
|
||||
Прежде чем удалять `.tgz` из verdaccio storage, надо отличать proxied (cached from upstream — безопасно удалить, можно re-fetch) от locally-published (authoritative copy — удаление = permanent loss).
|
||||
|
||||
## Storage layout
|
||||
|
||||
`/storage/data/<package>/`:
|
||||
- `package.json` — packument (metadata, дешёвый)
|
||||
- `<package>-<version>.tgz` — binary tarballs (дорогие)
|
||||
|
||||
Для scoped: `/storage/data/<scope>/<package>/...`. Scope в filename `.tgz` **НЕ** включается — `@snollajs/snolla/snolla-0.2.4.tgz`.
|
||||
|
||||
## Detection rule
|
||||
|
||||
В `package.json` есть три relevant fields:
|
||||
|
||||
```yaml
|
||||
_uplinks: { npmjs: { etag, fetched } } # upstream registries verdaccio queried
|
||||
_distfiles: { "<pkg>-<v>.tgz": { url, sha, registry } } # ключи — ИМЯ tarball, не version
|
||||
_attachments: { "<pkg>-<v>.tgz": { shasum } } # cached + locally-uploaded tarballs
|
||||
```
|
||||
|
||||
**Detection**:
|
||||
- `_distfiles[<tgzName>]` defined → **proxied**, tarball mirrored from upstream URL. Удалять `.tgz` файл безопасно — verdaccio re-fetch'нет при следующем `npm install pkg@v`.
|
||||
- `_distfiles[<tgzName>]` undefined, but `_attachments[<tgzName>]` defined и `.tgz` есть на диске → **locally-published** (`npm publish` в verdaccio через `verdaccio` registry). **НЕ удалять** — это единственная копия.
|
||||
|
||||
## Gotcha: keying
|
||||
|
||||
Ключи `_distfiles` — это **filename** (`lodash-0.1.0.tgz`), **НЕ** version string (`0.1.0`). Lookup `_distfiles["0.1.0"]` всегда возвращает undefined, ложно классифицируя ВСЕ versions как locally-published.
|
||||
|
||||
Easy mistake: первый draft prune скрипта проверял `_distfiles[v]` → dry-run показал 4597/7021 packages "защищены" (на самом деле ВСЕ proxied lodash/react/etc). Fix: `_distfiles[<tgzBase>-<v>.tgz]` где `tgzBase = name.replace(/^@[^/]+\//, '')` (strip scope для filename).
|
||||
|
||||
## Safe prune algorithm
|
||||
|
||||
```
|
||||
для каждого package:
|
||||
keepSet = dist-tagged versions # {latest, next, beta, ...}
|
||||
others = versions − keepSet, отсортированные по time[v] desc
|
||||
keepSet += first (N - |keepSet|) из others # N = 10
|
||||
для v в (versions − keepSet):
|
||||
tgzName = "<scope-stripped-name>-<v>.tgz"
|
||||
if _distfiles[tgzName] undefined:
|
||||
continue # locally-published, skip
|
||||
if .tgz file exists: delete file
|
||||
if _attachments[tgzName] exists: delete entry
|
||||
если что-то изменили в _attachments:
|
||||
bump _rev (`<num+1>-<random_hex>`)
|
||||
atomic rewrite package.json (tmp + rename)
|
||||
```
|
||||
|
||||
## Atomic write + _rev
|
||||
|
||||
`package.json` rewrite через `fs.writeFileSync(tmp); fs.renameSync(tmp, real)` — atomic on POSIX same-fs. Race window между «file gone» и «package.json updated» ничтожен.
|
||||
|
||||
`_rev` field в формате `<num>-<hash>` (CouchDB-style). Verdaccio проверяет его для optimistic concurrency. Bump = increment number + new random hex. Это invalidates npm client packument cache.
|
||||
|
||||
Дополнительная подстраховка от race: **stop verdaccio перед prune, start после** (~1 min downtime weekly). Альтернатива — atomic rewrite без stop — допустима, но cache invalidation менее clean.
|
||||
|
||||
## Что НЕ трогать
|
||||
|
||||
- `versions{}` и `time{}` — оставляем metadata in-place даже после удаления tarballs. Для proxied packages npm re-fetch'нет tgz при request; metadata лёгкая (~KB).
|
||||
- `_uplinks{}` — agent-level state о fetch timestamps; не относится к individual versions.
|
||||
- `readme`, `users`, `_id` — irrelevant для prune.
|
||||
|
||||
## Применено
|
||||
|
||||
[`vds-gc-cron`](../../.tasks/vds-gc-cron.md) — weekly cron Sun 03:30 MSK на VDS. Smoke 2026-05-21: 8.5G→6.8G freed, 4524 tgz deleted, 73 locally-published versions защищены (`@snollajs/*` + ~40 другие internal packages).
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Verdaccio local-storage docs: https://verdaccio.org/docs/configuration#storage
|
||||
- Algorithm в репо: `/opt/stacks/gc/scripts/verdaccio-prune.js` на VDS.
|
||||
76
.wiki/concepts/wd40efax-smr-cascade.md
Normal file
76
.wiki/concepts/wd40efax-smr-cascade.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18
|
||||
type: concept
|
||||
tags: [hardware, raid, smr, failure-mode, wd, lessons]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# WD40EFAX SMR Cascade
|
||||
|
||||
Сценарий смерти RAID 5 на WD Red WD40EFAX (SMR-диски). Случился 2026-05-18 на [[dead-synology-diskstation]]. Известная community-проблема, не уникальная.
|
||||
|
||||
## Что такое SMR
|
||||
|
||||
**SMR** (Shingled Magnetic Recording) — способ записи на пластины, где дорожки наезжают друг на друга как черепица. Плюс: больше плотность, дешевле гигабайт. Минус: запись на одну дорожку **физически портит соседние** → нужна re-write whole "zone". Запись стала зональной (вместо random-access).
|
||||
|
||||
**CMR** (Conventional Magnetic Recording) — стандарт, дорожки независимые.
|
||||
|
||||
### Где SMR ломается
|
||||
|
||||
| Сценарий | CMR | SMR |
|
||||
|---|---|---|
|
||||
| Sequential write | ✅ | ✅ |
|
||||
| Чтение | ✅ | ✅ |
|
||||
| Random writes (БД, активная FS) | ✅ | ❌ просаживается |
|
||||
| **RAID rebuild** | ✅ | ❌❌ **смертельно** |
|
||||
|
||||
Во время RAID-rebuild диск получает **долгую sustained-нагрузку**: parity-чтение + запись на новый диск. SMR-firmware пытается жонглировать зонами; внутренний CMR-кэш (есть в начале диска) забивается. Performance падает в 5-10 раз → **command timeouts** → RAID-контроллер выкидывает диск из массива.
|
||||
|
||||
## Скандал WD
|
||||
|
||||
WD в 2018-2020 **тихо** перевёл часть линейки "WD Red" (заточена под NAS) на SMR, не указав в маркировке. Community catch'нуло (Reddit, Servethehome, forum.synology). Class-action в США 2020, WD settled. После — WD переименовал CMR-варианты в "WD Red **Plus**" / "WD Red **Pro**"; "WD Red" без Plus остался SMR.
|
||||
|
||||
**WD40EFAX-68JH4N1, WD40EFAX-68JN4N0** — главные жертвы. Если в RAID — лотерея.
|
||||
|
||||
## Конкретный путь к смерти (наш кейс)
|
||||
|
||||
1. **3-диск RAID 5** на WD40EFAX. Storage pool в DSM на `cachedev_0`, 7 TB usable.
|
||||
2. **Начало мая 2026** — один из 3 дисков вылетел (вероятно тайм-аут под нагрузкой, не физический отказ).
|
||||
3. **Пул degraded.** RAID 5 на 3 дисках теперь толерирует 0 дополнительных отказов.
|
||||
4. **2 недели пользователь не заменил failed диск.** Пул работал в degraded; оставшиеся 2 SMR-диска делают parity-чтение для любого запроса.
|
||||
5. **Под этой нагрузкой второй WD40EFAX накопил тайм-ауты** (известный паттерн для SMR в degraded-RAID).
|
||||
6. **2026-05-18 ~17:00 MSK** — второй вылет. Пул past redundancy, "Сбой сборки" в DSM Storage Manager.
|
||||
7. **Виден только Disk 3 + Disk 8** (третий, который вылетел первым, физически не определяется системой даже).
|
||||
|
||||
## Ключевая ошибка
|
||||
|
||||
**2 недели в degraded не лечатся.** В нормальном RAID 5 на CMR — можно прожить недели без последствий (всё работает). На SMR — каждый день в degraded **повышает шанс второго отказа** из-за SMR-induced таймаутов.
|
||||
|
||||
Правило: при SMR в RAID 5 — **24-48 часов** на замену failed диска. Дольше — лотерея. Поэтому SMR в RAID **запрещён de facto** для серьёзных продакшнов.
|
||||
|
||||
## Что НЕ делать после второго отказа
|
||||
|
||||
- ❌ Repair / Online Assembly в DSM — пул past redundancy, mdadm не соберёт.
|
||||
- ❌ Менять disks в degraded-пуле — может ускорить deterioration оставшихся.
|
||||
- ❌ Запускать `mdadm --assemble` руками с force — без знания внутренней структуры → разрушение partial-data.
|
||||
|
||||
## Что МОЖНО (но дорого)
|
||||
|
||||
- **Pro data recovery** (Storelab/R.LAB/Ace Lab клиенты): $500-3000 typical. Контора берёт диски (все 3, включая failed), делает offline-reconstruct parity, восстанавливает file-tree. Подходит для возврата 9-дневного окна между последним бэкапом (2026-05-09) и инцидентом (2026-05-18).
|
||||
- **Условие:** диски физически живы (головки не упали). У нас 2 видимых "Исправно" + 1 не определяемый — стандартный кейс для recovery service.
|
||||
|
||||
## Lessons для будущего
|
||||
|
||||
Для следующего NAS-пула:
|
||||
|
||||
1. **CMR-only.** Никаких WD40EFAX/EFRX/EFAZ/EFGX (если они SMR). Кандидаты:
|
||||
- WD Red **Plus** (CMR, маркировка "Plus" — важно)
|
||||
- WD Red **Pro** (CMR, enterprise-grade)
|
||||
- Seagate IronWolf 4TB+ (CMR — модели <4TB могут быть SMR, проверять по datasheet)
|
||||
- HGST/WD Ultrastar (enterprise CMR)
|
||||
2. **RAID 6 / SHR-2** при 4+ дисках — толерирует 2 отказа. Один отказ + один SMR-cascade не убивает массив.
|
||||
3. **Дисциплина replace failed disk в 24-48 часов** — в degraded долго не сидеть.
|
||||
4. **Hot spare** если есть place в шасси.
|
||||
5. **Hyper Backup ежедневно** (а не "по триггеру") + retention 30+ дней.
|
||||
6. **Тест восстановления раз в квартал** — мы впервые узнали что наш backup рабочий **только когда случился инцидент**. Это нехорошо.
|
||||
70
.wiki/entities/dead-synology-diskstation.md
Normal file
70
.wiki/entities/dead-synology-diskstation.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Мёртвая Synology DiskStation (source NAS)
|
||||
type: entity
|
||||
tags: [hardware, nas, xpenology, raid, dead]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Dead Synology DiskStation
|
||||
|
||||
Source NAS, на котором хостились клиентские сайты MoreThenCms до 2026-05-18. **Сейчас off / data unrecoverable средствами DSM.**
|
||||
|
||||
## Hardware
|
||||
|
||||
- **Тип:** XPEnology (DSM 7 на самосборном x86, community-loader)
|
||||
- **Шасси:** 6 HDD bay
|
||||
- **Hostname:** `diskstation`
|
||||
- **Bootloader:** USB-флешка (внешняя относительно дисков)
|
||||
- **System SSD:** Netac SSD 120 GB (отдельный) — "Отказ системного раздела" к моменту инцидента
|
||||
- **DSM hostname в сети:** `diskstation` (LAN), без публичного DDNS (доступ через клиентские домены через traefik)
|
||||
|
||||
## Storage pool
|
||||
|
||||
- **RAID 5** на **3 дисках** WD Red **WD40EFAX-68JH4N1 / 68JN4N0** (3.6 TB каждый)
|
||||
- ~7 TB usable (`/dev/mapper/cachedev_0` 7.0T)
|
||||
- ⚠️ **WD40EFAX = SMR** — см. [[wd40efax-smr-cascade]] для механики краха.
|
||||
|
||||
## Что было на NAS
|
||||
|
||||
### VMM
|
||||
- **snolla VM:** Windows-VM с IIS + .NET Framework 4.8 + CMS [[snolla-recovery-vm]]
|
||||
- MAC: `02:11:32:2A:7C:B9`, IP `192.168.1.15` (DHCP-резервация на роутере)
|
||||
- OVA-экспорт от 2024-10-27 включён в Hyper Backup → теперь работает на VirtualBox на [[windows-recovery-host]].
|
||||
|
||||
### Container Manager (docker)
|
||||
- **mssql:** Server 2019, 5 БД (`MoreThenCms`, `StayerCalculator`, `StayerPrice`, `stostayer`, `TireService`)
|
||||
- **minio:** RELEASE.2020-07-13T18-09-56Z, 9 бакетов (artmone 2.5 GB, pilorama98 120 MB, books 5 MB, и др.)
|
||||
- **elasticsearch:** 7.10.1, 3 индекса (для books-стека, не MoreThenCms)
|
||||
- **imgproxy + nginx-cache**
|
||||
- **traefik:** 2.6.6, 13 client routes, 40 Let's Encrypt сертификатов
|
||||
- **gitea:** 2.6 GB
|
||||
- Прочее: jellyfin, mongo, owncloud, navidrome, mariadb, и т.д.
|
||||
|
||||
### Shares
|
||||
- `/docker/` — docker-стеки
|
||||
- `/docker/personal/` — большая часть production-сервисов
|
||||
- `/backup/` — куда писались ежедневные дампы:
|
||||
- `/backup/snolla/SQLServer/MoreThenCms<YYYYMMDDHHMM>.zip` — ежедневный sql-script (101 MB compressed)
|
||||
- `/backup/snolla/snolla.ova` — 42.5 GB, экспорт VM (последний 2024-10-27)
|
||||
- `/work/` — рабочая папка разработчика (в восстановление не брали по решению пользователя)
|
||||
|
||||
## Что произошло 2026-05-18
|
||||
|
||||
См. [[wd40efax-smr-cascade]]. Кратко: первый диск умер в начале мая, ~2 недели пул жил degraded, second disk вылетел 2026-05-18 → RAID 5 за пределами redundancy → пул "Сбой сборки" в DSM.
|
||||
|
||||
## Что НЕ делать с этой коробкой
|
||||
|
||||
- ❌ Repair / Online Assembly в DSM на этом пуле — бесполезно.
|
||||
- ❌ Вытаскивать оставшиеся 2 диска до решения "нужно ли pro data recovery".
|
||||
- ❌ Пересоздавать пул на тех же дисках.
|
||||
- ❌ Ставить новые WD40EFAX (если будут запасные) — же баг останется.
|
||||
|
||||
## План восстановления железа (после ремонта инфраструктуры)
|
||||
|
||||
- Заменить все WD40EFAX на CMR-диски (WD Red **Plus** / Seagate IronWolf / WD Red Pro / HGST Ultrastar).
|
||||
- Заменить Netac SSD на нормальный consumer SSD (Samsung 870 EVO / WD Red SA500).
|
||||
- Конфигурация:
|
||||
- **3 CMR в RAID 5** + ежедневный Hyper Backup + дисциплина replace failed disk в течение 24-48 часов
|
||||
- **или 4 CMR в RAID 6** (или SHR-2) — толерирует 2 отказа, рекомендуется после такого опыта
|
||||
- Тест восстановления раз в квартал.
|
||||
59
.wiki/entities/kreknin-synology.md
Normal file
59
.wiki/entities/kreknin-synology.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Kreknin Synology (backup target + DDNS)
|
||||
type: entity
|
||||
tags: [hardware, nas, synology, backup, hyperbackup]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Kreknin Synology
|
||||
|
||||
Удалённая (географически в другом месте) Synology, которая держит Hyper Backup-репо мёртвой синки и сама работает как живой сервер.
|
||||
|
||||
## Доступ
|
||||
|
||||
- **Public IP:** 195.19.90.188
|
||||
- **DDNS:** kreknin.site (резолвится на 195.19.90.188)
|
||||
- **DSM web:** http://kreknin.site:5000 (HTTP; HTTPS 5001 наружу НЕ проброшен)
|
||||
- **SSH:** vitya@195.19.90.188:22 (был пароль, сейчас SSH-ключ установлен — `id_ed25519_kreknin`)
|
||||
- **Канал:** "не очень надёжный" по словам пользователя — поэтому неудобно держать там production-сайты.
|
||||
|
||||
## Ограничения SFTP
|
||||
|
||||
- **SFTP-подсистема DSM запускается отдельно от SSH** — Control Panel → File Services → FTP → SFTP. Изначально не была включена.
|
||||
- **После включения SFTP — DSM jail-chroot'ит подсистему в home юзера.** То есть `vitya` через SFTP видит только `/volume1/homes/vitya/`, не `/volume1/backup/...`.
|
||||
- **Обход:** `scp -O` (legacy SCP протокол) использует чистый SSH-channel мимо SFTP-subsystem → даёт доступ ко всему, что shell-пользователь видит.
|
||||
|
||||
## Структура
|
||||
|
||||
- **Volume:** один том `/volume1`, 7.0 TB, ~1.2 TB used до восстановления.
|
||||
- **/volume1/NetBackup/diskstation_1.hbk** — Hyper Backup репо с мёртвой синки. 430 GB compressed (deduplicated), последняя успешная backup-версия 2026-05-09 05:06.
|
||||
- **Hyper Backup Vault** установлен как пакет на этой синке (см. [[hyper-backup-structure-and-recovery]]).
|
||||
- Owner данных в репо — `vitya:users` (POSIX) с ACL под `+`. ACL даёт vitya read, но individual файлы `.bak`/`.acme.json` могут иметь `-rw-------` — для них нужен `chmod -R a+rX` из root SSH.
|
||||
|
||||
## VMM статус
|
||||
|
||||
- Установлен (виден `@SavedVM` в `/volume1/`).
|
||||
- В сессии 2026-05-18 рассматривался вариант поднять `snolla.ova` прямо здесь через VMM как альтернатива переезду на Windows — отвергнут потому что канал не надёжный.
|
||||
|
||||
## Роль в recovery
|
||||
|
||||
- **Источник всех данных:** OVA, sql дампы, docker volumes (mssql, minio, elasticsearch, imgproxy/nginx) — всё тащилось отсюда.
|
||||
- **Не было записи на этот NAS** во время recovery — только чтение / Hyper Backup restore во временную папку `/volume1/NetBackup/restore-tmp/`, потом backup-shares `/volume1/backup/`, `/volume1/docker/`, `/volume1/work/`.
|
||||
|
||||
## Гипотеза по будущему backup pipeline
|
||||
|
||||
- Эта синка остаётся как backup target, на ней нет SMR-дисков (тип неизвестен на момент сессии, но кратко проверить через `ls /dev/sd*` + smartctl до тяжёлой нагрузки).
|
||||
- Будущая [[future-resilient-architecture-goals]]: добавить второй backup target (или облачный — Backblaze B2 / S3 Glacier), чтобы не зависеть от одной коробки.
|
||||
|
||||
## Роль источника для миграции на VDS (2026-05-20)
|
||||
|
||||
В сессии [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) kreknin сыграл вторую роль — **источник данных** для миграции инфра-сервисов на [`vds-kzntsv`](vds-kzntsv.md). Из restored backup'а ([Hyper Backup](../concepts/hyper-backup-structure-and-recovery.md) `.hbk` мёртвой синки):
|
||||
|
||||
- **Gitea** — `tar c -C /volume1/docker/gitea data postgres docker-compose.yml | ssh vds tar x` (sudo `Pryakhin9` для read postgres datadir uid 999). 2.7G total за 8 мин.
|
||||
- **Verdaccio** — `rsync /volume1/docker/personal/verdaccio/{storage,config,plugins} → vds:/opt/stacks/verdaccio/`. 9G за 18 мин.
|
||||
- **Registry** — GC на kreknin (`registry:2.8.3 garbage-collect -m`) сжал 99G → 35G; затем user принял решение **abandon миграцию** и fresh install на VDS. Старый registry data остаётся на kreknin как backup-reference.
|
||||
|
||||
## Roadmap как backup target для VDS
|
||||
|
||||
Планируется ежедневный pull rsync VDS → kreknin в `/volume1/NetBackup/vds-kzntsv/` (см. follow-up task `.tasks/vds-backup-rsync-kreknin.md`). Дополнительный pipe — backup pipe для production-CMS [`windows-recovery-host`](windows-recovery-host.md) → тут же на kreknin — пока не реализован.
|
||||
62
.wiki/entities/openwrt-router.md
Normal file
62
.wiki/entities/openwrt-router.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: OpenWRT Router (192.168.1.1)
|
||||
type: entity
|
||||
tags: [hardware, networking, openwrt, nat, dhcp]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# OpenWRT Router
|
||||
|
||||
Домашний роутер, через который идёт весь публичный трафик к клиентским сайтам.
|
||||
|
||||
## Hardware / OS
|
||||
|
||||
- **Hardware:** MediaTek MT7622 (aarch64), 6 disk-bay шасси (с роутером не связано)
|
||||
- **OS:** OpenWRT 23.05.4 (r24012-d8dd03c46f)
|
||||
- **Uptime:** 13+ дней на момент recovery
|
||||
- **LAN-IP:** 192.168.1.1
|
||||
- **LAN-subnet:** 192.168.1.0/24
|
||||
- **WAN-public:** 94.19.247.14
|
||||
|
||||
## Access
|
||||
|
||||
- SSH: `root@192.168.1.1:22` — ssh-key `id_ed25519_openwrt` установлен в `/etc/dropbear/authorized_keys`
|
||||
- LuCI web: http://192.168.1.1
|
||||
- Конфиг через `uci` (UCI infrastructure)
|
||||
|
||||
## Текущий port forwarding setup (после recovery patch)
|
||||
|
||||
Активные:
|
||||
- `firewall.@redirect[0]` (SSL): WAN **443** → 192.168.1.143:**4443** (Windows-PC, где traefik)
|
||||
- `firewall.@redirect[1]` (HTTP): WAN **80** → 192.168.1.143:**8000** (Windows-PC, где traefik)
|
||||
- `firewall.@redirect[9]` (Wireguard): WAN 48820 → 192.168.1.239 (отдельный хост, не трогали)
|
||||
|
||||
Старые (всё ещё активны, но смотрят на мёртвую синку 192.168.1.10 — на которой ничего нет):
|
||||
- DSM 5000 → 192.168.1.10:5000
|
||||
- FTP 21, SFTP 22, MariaDB 36063, SQLServer 23056, PassiveFTP 55536-55899, Cloud Station 6690, SSH 1322
|
||||
|
||||
В рамках recovery эти **не отключали** (по решению пользователя). Можно отключить через `uci set firewall.@redirect[N].enabled='0'` для снижения шума атак.
|
||||
|
||||
## DHCP-резервации (актуальные)
|
||||
|
||||
| Хост | IP | MAC | Назначение |
|
||||
|---|---|---|---|
|
||||
| `windows-recovery-pc` | 192.168.1.143 | 88:66:5A:2F:AA:68 | [[windows-recovery-host]] |
|
||||
| `snolla` (исторически) | 192.168.1.15 | 02:11:32:2A:7C:B9 | Раньше — VM мёртвой синки. Сейчас MAC присвоен новой [[snolla-recovery-vm]], но VM в NAT-режиме и LAN-IP не получает. |
|
||||
| `diskstation` | 192.168.1.10 | 22:06:7C:32:00:6F (+ ...:70) | Мёртвая синка [[dead-synology-diskstation]] |
|
||||
| Прочие | разное | разное | wled-1, hifiberry, и т.д. — не трогаем |
|
||||
|
||||
## Firewall zones
|
||||
|
||||
- **lan:** input=ACCEPT, output=ACCEPT, forward=ACCEPT (внутри LAN всё открыто)
|
||||
- **wan:** input=REJECT, output=ACCEPT, forward=REJECT, masq=1 (стандарт)
|
||||
- LAN→WAN forwarding: ALLOW
|
||||
|
||||
Дополнительные input rules для wan (стандартные OpenWRT): Allow-DHCP-Renew, Allow-Ping, Allow-IGMP, Allow-DHCPv6, Allow-MLD, Allow-ICMPv6-*, Allow-IPSec-ESP.
|
||||
|
||||
## Что bgужно сделать (на потом)
|
||||
|
||||
- Отключить устаревшие redirects на 192.168.1.10 (DSM/FTP/SQL/Cloud Station) — снижает attack surface.
|
||||
- Запланировать DDNS для случая смены публичного IP (REGRU domains всё ещё указывают на 94.19.247.14).
|
||||
- При планировании [[future-resilient-architecture-goals]] — добавить **second WAN** (3G/4G/LTE через USB-modem) на роутер? OpenWRT поддерживает multi-WAN.
|
||||
94
.wiki/entities/snolla-recovery-vm.md
Normal file
94
.wiki/entities/snolla-recovery-vm.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Snolla Recovery VM (VirtualBox) — savestate'нута 2026-05-21 после 36h успешного soak
|
||||
type: entity
|
||||
tags: [vm, virtualbox, windows, iis, cms, recovery, savestate]
|
||||
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
|
||||
---
|
||||
|
||||
# Snolla Recovery VM
|
||||
|
||||
VirtualBox-VM на [[windows-recovery-host]], в которой работает CMS-стек (IIS + .NET + код CMS). Импортирован из OVA-снапшота 2024-10-27, который лежал в Hyper Backup репо.
|
||||
|
||||
**Статус (2026-05-21 05:13 MSK):** VM **savestate'нута** после ~36h soak attempt 2 миграции (`VBoxManage controlvm snolla-recovery savestate`, 45.6s, VMState=`saved`). Savestate file: `Snapshots\2026-05-21T05-12-43-636491200Z.sav` = 1.73 GB (compressed RAM). VBoxHeadless процессы исчезли — освобождено ~4 GB private memory. Disk usage +1.7 GB.
|
||||
|
||||
Resume в любой момент через `VBoxManage startvm snolla-recovery --type headless` (~30 сек). После соак ещё одной недели — `unregistervm --delete` (освободит ~95 GB на C: — `snolla-disk1.vmdk` 95.8 GB + .sav).
|
||||
|
||||
**Предыстория:** все 11 cms hosts мигрированы на host-IIS:8089 ([[iis-host-migration-2026-05-19]] Phase 10), 36h soak passed без rollbacks (Phase 11), 8/8 наших sites зеленые через full traefik HTTPS chain. VM держалась как parallel fallback (recipe-D из [[iis-migration-2026-05-19-postmortem]]) — fallback не понадобился.
|
||||
|
||||
История: утром 2026-05-19 attempt 1 миграции сломал prod (Docker NAT loop), был revert на VM. Через тот же вечер — attempt 2 по recipe (backend port `:8089` вместо `:80`, smoke `-MaximumRedirection 0`, phone-test не-LAN) — succeeded. Подробности обеих попыток в `iis-host-migration-2026-05-19` Phases 1-10.
|
||||
|
||||
## Параметры
|
||||
|
||||
- **VBox name:** `snolla-recovery`
|
||||
- **UUID:** `66aac8bb-fe70-4ced-87f6-2291cd0e8b74`
|
||||
- **Расположение:** `C:\Users\vitya\VirtualBox VMs\snolla-recovery\`
|
||||
- **OS внутри:** Windows (вероятно Server 2016/2019, hostname `SNOLLA`)
|
||||
- **RAM:** 4096 MB
|
||||
- **vCPU:** 2 (после оптимизации [[vbox-windows-stability-tuning]] — было 4)
|
||||
- **Disk:** `snolla-disk1.vmdk`, max 120 GB, реально ~92 GB на хосте
|
||||
- **Storage controller:** SATA AHCI (после миграции с SCSI LsiLogic, см. [[vbox-windows-stability-tuning]])
|
||||
- **Network:** NAT (после миграции с bridged WiFi из-за нестабильности)
|
||||
- **Paravirt:** `kvm` (matching исходному гипервизору Synology VMM)
|
||||
- **OS type:** Windows10_64 (исходно был `Other_64`, поправили для оптимальных дефолтов)
|
||||
- **Guest Additions:** 7.2.8 r173730, RunLevel=3 (полностью активны)
|
||||
|
||||
## NAT Port Forwards
|
||||
|
||||
| Host port | VM port | Назначение |
|
||||
|---|---|---|
|
||||
| 13389 | (console) | **VRDE** (VBox Remote Display) — для отладки, не зависит от Windows RDP |
|
||||
| 23389 | 3389 | RDP внутри VM (Windows Remote Desktop) |
|
||||
| 8022 | 22 | SSH (OpenSSH Server в VM) |
|
||||
| 18080 | 80 | IIS Default — основной HTTP CMS |
|
||||
| 18180 | 8080 | IIS site `stostayer` |
|
||||
| 18181 | 8081 | IIS site `stostayer.old` |
|
||||
| 18189 | 8089 | (запасной) |
|
||||
|
||||
## Учётка
|
||||
|
||||
- **Admin:** vitya (домен SNOLLA)
|
||||
- **Default shell для sshd:** PowerShell (зарегистрирован в `HKLM:\SOFTWARE\OpenSSH` → `DefaultShell`)
|
||||
- **Authorized SSH key для admin-users:** `C:\ProgramData\ssh\administrators_authorized_keys` (особое место для admin Windows OpenSSH; permissions через `icacls`, group `Администраторы:F` + `СИСТЕМА:F`)
|
||||
|
||||
## IIS-сайты
|
||||
|
||||
| Site | Path | Bindings | Прим. |
|
||||
|---|---|---|---|
|
||||
| **MoreThenCms.Web** | `C:\inetpub\wwwroot\MoreThenCms.Web` | `*:80` | **active prod** — catch-all для 11 главных доменов; traefik backend `host.docker.internal:18080` |
|
||||
| **Snolla.IdentityManager** | `C:\inetpub\wwwroot\Snolla.IdentityManager` | `*:8089` | публично не используется |
|
||||
| **stostayer** | `C:\stayer\MoreThenCms.Web` | `*:8080` | **active prod** — traefik backend `host.docker.internal:18180`; conn → внешний `89.253.219.2,1433` (но user в session 2026-05-19 поменял на `www.stostayer.ru,1433` для host-копии; **в VM остался старый**) |
|
||||
| **stostayer.old** | `C:\stayer\stostayer.old` | `*:8081` | **active prod** — traefik backend `host.docker.internal:18181` |
|
||||
| **stostayer.old/calc** | `C:\stayer\Mis.StoStayer.Calculator.Web` | (sub-app под `:8081`) | существует, sub-app pool `calc` |
|
||||
| **stostayer.old/price** | `C:\stayer\Mis.StoStayer.Price.Api` | (sub-app под `:8081`) | существует, sub-app pool `price` |
|
||||
| **stostayer.old/price/tireService** | `C:\stayer\Mis.StoStayer.TireService.Api` | (sub-app под `:8081`) | существует, sub-app pool `tireService` |
|
||||
|
||||
CMS распознаёт клиента по **Host header** — все 11 клиентских доменов идут на `:80` и роутятся внутри CMS-кода.
|
||||
|
||||
**Важно для следующей попытки миграции:** stostayer Web.config в VM указывает на старый `89.253.219.2,1433`, а на host-копии (`C:\sites\stostayer\Web.config`) уже patched на `www.stostayer.ru,1433` с XML-escape `&` в password. Если когда-то будем сводить эти конфиги — host-копия правильнее (старый сервер мёртв).
|
||||
|
||||
## Web.config — критичные настройки (после recovery patch)
|
||||
|
||||
- **Connection string:** `Data Source=10.0.2.2;Initial Catalog=MoreThenCms;User Id=snolla;Password=fXkH4@8O%3pc;...`
|
||||
- `10.0.2.2` = NAT gateway в VBox = адрес хоста [[windows-recovery-host]] изнутри VM
|
||||
- До патча было `Data Source=192.168.1.10` (старая мёртвая синка)
|
||||
- Тот же пароль `fXkH4@8O%3pc` совпадает с SA-паролем MSSQL контейнера (production password из старого compose)
|
||||
- **Encoding файла:** UTF-8 with BOM (важно — см. [[cms-config-rewrite-pattern]])
|
||||
- 4 файла пропатчены аналогично: `MoreThenCms.Web/Web.config`, `Snolla.IdentityManager/Web.config`, `stostayer.old/web.config`, и stayer-проектов (если применимо)
|
||||
|
||||
## Связь со внешним миром
|
||||
|
||||
- VM в NAT-режиме → не имеет LAN-IP
|
||||
- Из traefik (на хосте) достижима по `host.docker.internal:18080` → NAT-форвард в Windows → VM:80
|
||||
- Раньше (когда было bridged) — VM имела IP `192.168.1.15` с MAC `02:11:32:2A:7C:B9`. snolla.yml в traefik/data/custom/ исходно ссылался на `http://192.168.1.15/` — пропатчен на `host.docker.internal:18080/`.
|
||||
|
||||
## Стабильность
|
||||
|
||||
- Нестабильна на bridged-WiFi → переведена в NAT (стало лучше, но всё равно требует осторожности).
|
||||
- Один случай (после нескольких часов uptime): network adapter в VM "повис" — все TCP-handshake проходили, но через них трафик не шёл. Лечится `ipconfig /release && /renew` внутри VM через VBoxManage guestcontrol.
|
||||
- **Долгосрочно**: пора планировать scheduled task внутри VM, который при детекции downtime автоматически перезагружает network adapter / iisreset. Или вообще переезд на Hyper-V — рекомендация Microsoft для Windows-гостей.
|
||||
|
||||
## Известные баги в текущей конфигурации
|
||||
|
||||
- **X-Forwarded-Proto/Host headers** не передаются с traefik в IIS → CMS делает redirect на `http://www.<domain>:4443/` (mixing HTTP scheme with HTTPS port). Не критично, но требует фикса.
|
||||
- **Логи в C:\inetpub\logs\** растут (~6 GB на момент recovery) — нужна ротация.
|
||||
127
.wiki/entities/vds-kzntsv.md
Normal file
127
.wiki/entities/vds-kzntsv.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: VDS kzntsv — Rusonyx 160 NVMe cloud server
|
||||
type: entity
|
||||
tags: [hardware, vds, cloud, rusonyx, infrastructure, gitea, verdaccio, registry, postgres, mariadb, mongo, redis]
|
||||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||||
updated: 2026-05-20
|
||||
---
|
||||
|
||||
# VDS kzntsv
|
||||
|
||||
Облачный VDS у Rusonyx, активирован 2026-05-20. Цель — вынести инфраструктурные сервисы (gitea / verdaccio / docker-registry / shared DBs / в будущем seafile, hermes, ntfy) с одной железной коробки на отдельный host. Это первый шаг по closing SPOF gap из [`future-resilient-architecture-goals`](../concepts/future-resilient-architecture-goals.md).
|
||||
|
||||
Production CMS (MoreThenCms) **остаётся на** [`windows-recovery-host`](windows-recovery-host.md) и обсуждается отдельно.
|
||||
|
||||
## Hardware / tariff
|
||||
|
||||
- **Vendor:** Rusonyx (Astra Облако), https://myvm.rusonyx.ru
|
||||
- **Tariff:** 160 NVMe (заказан 2026-05-19, активирован 2026-05-20)
|
||||
- **vCPU:** 6 × 2.6 GHz
|
||||
- **RAM:** 8 GiB
|
||||
- **Disk:** 160 GiB NVMe
|
||||
- **OS:** Ubuntu 24.04 LTS (Noble)
|
||||
- **IPv4:** 1 шт (free)
|
||||
- **Backup от Rusonyx:** 0 (свой backup pipeline через `[[vds-backup-rsync-kreknin]]`)
|
||||
|
||||
## Доступ
|
||||
|
||||
- **Public IP:** `89.253.255.94`
|
||||
- **Vendor hostname:** `vps-21075162-534388.host4g.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)
|
||||
|
||||
## Software stack
|
||||
|
||||
| Слой | Компонент | Версия | Где |
|
||||
|---|---|---|---|
|
||||
| OS | Ubuntu | 24.04.4 LTS | host |
|
||||
| Kernel | Linux | 6.8.0-117-generic | host |
|
||||
| Firewall | ufw | active | host (allow 22, 80, 443, 5432, 3306, 27017, 6379) |
|
||||
| Brute-protect | fail2ban | active (sshd jail) | host |
|
||||
| Engine | Docker CE | 29.5.1 | host (official APT repo) |
|
||||
| Compose | docker-compose-plugin | v5.1.3 | host |
|
||||
| Reverse proxy | Traefik | v2.11 LTS | `/opt/stacks/traefik/` |
|
||||
| Container mgmt | Portainer CE | 2.21.5 | `/opt/stacks/portainer/` |
|
||||
| Postgres | postgres | 16 (Debian) | `/opt/stacks/databases/postgres/` |
|
||||
| MariaDB | mariadb | 11.4 | `/opt/stacks/databases/mariadb/` |
|
||||
| MongoDB | mongo | 7.0 | `/opt/stacks/databases/mongo/` |
|
||||
| Redis | redis | 7.4-alpine | `/opt/stacks/databases/redis/` |
|
||||
| Git | gitea | 1.25.5 | `/opt/stacks/gitea/` |
|
||||
| NPM | verdaccio | 6 | `/opt/stacks/verdaccio/` |
|
||||
| Docker registry | registry | 2.8.3 + joxit UI | `/opt/stacks/registry/` |
|
||||
|
||||
## Docker networks (external)
|
||||
|
||||
- `proxy` — traefik + всё что выставляется наружу через HTTPS
|
||||
- `shared-dbs` — DB-park + любой контейнер, который к DBs ходит по DNS-имени `postgres` / `mariadb` / `mongo` / `redis`
|
||||
|
||||
## Hostnames (live, 2026-05-20)
|
||||
|
||||
| Hostname | Service | Auth | Назначение |
|
||||
|---|---|---|---|
|
||||
| `portainer.vds.kzntsv.site` | Portainer | vitya / `Pryakhin9-VDS-2026` (18 chars, см. [`portainer-2.21-admin-password-regression`](../concepts/portainer-2.21-admin-password-regression.md)) | Container management |
|
||||
| `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-ui.vds.kzntsv.site` | Joxit Registry UI | (proxied к registry, та же auth) | GUI cleanup |
|
||||
| `postgres.vds.kzntsv.site:5432` | Postgres TLS | postgres / hex32 | Shared DB |
|
||||
| `mariadb.vds.kzntsv.site:3306` | MariaDB TLS | root / hex32 | Shared DB |
|
||||
| `mongo.vds.kzntsv.site:27017` | MongoDB TLS | root / hex32 | Shared DB |
|
||||
| `redis.vds.kzntsv.site:6379` | Redis TLS | hex32 (requirepass) | Shared cache |
|
||||
|
||||
DB TLS: self-signed certs (CN matches hostname), клиент с `verify-none` / `tlsAllowInvalidCertificates`. Pattern см. [`db-tls-self-signed-via-traefik-raw-tcp`](../concepts/db-tls-self-signed-via-traefik-raw-tcp.md).
|
||||
|
||||
## File layout
|
||||
|
||||
```
|
||||
/opt/stacks/
|
||||
├── traefik/
|
||||
│ ├── data/
|
||||
│ │ ├── traefik.yml (static config)
|
||||
│ │ └── dynamic/
|
||||
│ │ └── middlewares.yml (basicAuth для dashboard)
|
||||
│ ├── letsencrypt/
|
||||
│ │ └── acme.json (LE certs, 0600)
|
||||
│ └── docker-compose.yml
|
||||
├── portainer/
|
||||
│ ├── data/ (portainer.db, chisel keys)
|
||||
│ └── docker-compose.yml (note: run via `docker run`, не compose, чтобы bypass'нуть env-interp на --admin-password)
|
||||
├── databases/
|
||||
│ ├── postgres/{data,certs,docker-compose.yml}
|
||||
│ ├── mariadb/{data,certs,docker-compose.yml}
|
||||
│ ├── mongo/{data,certs,docker-compose.yml}
|
||||
│ └── redis/{data,certs,docker-compose.yml}
|
||||
├── gitea/
|
||||
│ ├── data/{git,gitea,ssh} (mount → /data в контейнере)
|
||||
│ └── docker-compose.yml
|
||||
├── verdaccio/
|
||||
│ ├── storage/ (npm packages, 8.6 GB, 2063 packages)
|
||||
│ ├── config/{config.yaml,htpasswd}
|
||||
│ ├── plugins/
|
||||
│ └── docker-compose.yml
|
||||
└── registry/
|
||||
├── docker/ (registry blobs storage)
|
||||
├── auth/htpasswd
|
||||
└── docker-compose.yml (registry + registry-ui в одном compose)
|
||||
```
|
||||
|
||||
## Что входит в backup pipeline (planned)
|
||||
|
||||
См. [`vds-backup-rsync-kreknin`](../../.tasks/vds-backup-rsync-kreknin.md): daily 05:00 MSK rsync → `kreknin.site:/volume1/NetBackup/vds-kzntsv/` с `--link-dest` incremental. DB dumps первым шагом (`pg_dumpall` / `mariadb-dump` / `mongodump` / `redis-cli --rdb`), потом rsync `/opt/stacks/`. Email-нотификация на `vitya.kuznetsov@gmail.com` через SMTP smtp.yandex.ru:465 (noreply@snolla.com / pass в `noreply-snolla-smtp.env`), плюс [`vds-ntfy-push`](../../.tasks/vds-ntfy-push.md) на Android.
|
||||
|
||||
## Связь с другими сущностями
|
||||
|
||||
- Источник данных для миграции gitea/verdaccio — restored backup на [`kreknin-synology`](kreknin-synology.md) (через tar+ssh-pipe и rsync). Registry — fresh install без миграции старых images (user accepted loss).
|
||||
- Заменяет старый CMS-инфра-host [`windows-recovery-host`](windows-recovery-host.md) **только для инфраструктурных сервисов** (gitea/verdaccio/registry/DBs); production CMS остаётся на recovery-host.
|
||||
- Не зависит от [`dead-synology-diskstation`](dead-synology-diskstation.md) (тот мёртв).
|
||||
|
||||
## 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)).
|
||||
- ntfy push — TODO ([`vds-ntfy-push`](../../.tasks/vds-ntfy-push.md)).
|
||||
- Hermes — defer, ждёт уточнения user.
|
||||
91
.wiki/entities/windows-recovery-host.md
Normal file
91
.wiki/entities/windows-recovery-host.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: Windows Recovery Host (рабочий PC пользователя)
|
||||
type: entity
|
||||
tags: [hardware, windows, docker, virtualbox, iis, recovery]
|
||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# Windows Recovery Host
|
||||
|
||||
Личный Windows-PC пользователя, который во время recovery стал production-сервером для всех клиентских сайтов.
|
||||
|
||||
## Hardware / OS
|
||||
|
||||
- **Hostname:** DESKTOP-NSEF0UK
|
||||
- **OS:** Windows 11 Pro (предположительно — поддерживает Hyper-V, IIS, .NET Framework)
|
||||
- **LAN-MAC:** 88:66:5A:2F:AA:68 (Broadcom 802.11ac WiFi)
|
||||
- **LAN-IP:** 192.168.1.143 (DHCP-резервация на [[openwrt-router]])
|
||||
- **Диск C:** ~700 GB total, на старте recovery ~352 GB free, после — ~150 GB free.
|
||||
- **Юзер:** vitya (admin)
|
||||
|
||||
## Установленный стек
|
||||
|
||||
- **.NET Framework:** 4.8.1
|
||||
- **IIS** (W3SVC, на 80, мы планировали остановить — но не остановили, traefik на 8000/4443 не конфликтует)
|
||||
- **Docker Engine 29.3.1** (Docker Desktop с WSL2 backend)
|
||||
- **VirtualBox 7.2.8** (установлен в сессии 2026-05-18, через winget)
|
||||
- **OpenSSH client** (для ssh/scp к kreknin и VM)
|
||||
- **FileZilla client** (для пользовательских SFTP-перетаскиваний)
|
||||
|
||||
## Сетевое положение
|
||||
|
||||
- За **OpenWRT 23.05.4** [[openwrt-router]]
|
||||
- Соединение с интернетом через **WiFi** (Broadcom 802.11ac, 288 Mbps)
|
||||
- За **VLESS-клиентом v2rayN** (роутер default-route шёл через VPN, пришлось настроить **Bypass LAN** правило с `geoip:private` → direct, иначе входящие 80/443 терялись через asymmetric routing)
|
||||
|
||||
## Что хостит сейчас (после attempt 2 успешной миграции на host-IIS, 2026-05-19 вечер)
|
||||
|
||||
**Active prod:**
|
||||
|
||||
| Что | Где | Порт (host) | Прим. |
|
||||
|---|---|---|---|
|
||||
| **Native IIS — site `snolla`** | `C:\sites\snolla` (pool `snolla`, .NET v4.0 Integrated, `ApplicationPoolIdentity`) | `*:80`, `*:8089` | **active prod** — catch-all для 11 cms hosts; traefik backend `host.docker.internal:8089` |
|
||||
| **Native IIS — site `stostayer`** | `C:\sites\stostayer` (pool `stostayer`) | `*:8090` | local-only, traefik route `.yml.disabled` (user: «внутренний, наружу не светить») |
|
||||
| **Native IIS — site `stostayer.old`** | `C:\sites\stostayer.old` (pool `stostayer.old`) | `*:8091` | local-only, traefik route `.yml.disabled` |
|
||||
| **traefik 2.6.6** | Docker, network `proxy` | 8000 (http), 4443 (https), 8080 (dashboard) | 11 cms routes → host IIS:8089; 2 stayer routes DISABLED; 3 infra routes |
|
||||
| **MSSQL 2019** | Docker, named volume `mssql_mssql_data` | 1433 | 5 production DB |
|
||||
| **MinIO** | Docker, bind-mount `./data` | 9000 | |
|
||||
| **Elasticsearch 7.10.1** | Docker, bind-mount `./data` | 9200 | books-стек, не CMS |
|
||||
| **imgproxy** | Docker | 8787 | |
|
||||
| **imgproxy-nginx** | Docker | 8788 | |
|
||||
|
||||
**Parallel fallback (running, без traffic):**
|
||||
|
||||
| Что | Где | Порт (host) | Прим. |
|
||||
|---|---|---|---|
|
||||
| **VirtualBox `snolla-recovery` VM** | `C:\Users\vitya\VirtualBox VMs\snolla-recovery\` | NAT 13389/23389/8022/18080/18180/18181/18189 | parallel fallback на 24-48h soak; затем savestate; см. [[snolla-recovery-vm]] |
|
||||
|
||||
**Прочие inert:**
|
||||
|
||||
| Что | Где | Прим. |
|
||||
|---|---|---|
|
||||
| **`C:\sites\snolla-identity-manager\`** | папка без IIS-сайта | deploy-артефакт, не активен |
|
||||
| **traefik backups** | `data/custom/*.yml.bak-*-2026-05-19` (5 серий) | atomic-revert artefacts; `.bak-pre-attempt2-2026-05-19` — baseline текущей prod conf |
|
||||
|
||||
`Default Web Site` stopped (`autoStart=false`). URL Rewrite + ARR **не установлены**.
|
||||
|
||||
## Ключевые папки
|
||||
|
||||
- `C:\Users\vitya\projects\docker\diskstation\` — compose'ы и данные docker-стеков
|
||||
- `mssql/`, `minio/`, `elasticsearch/`, `imgproxy/`, `traefik/`
|
||||
- `traefik/data/custom/*.yml` — file-provider routes для всех 13 client доменов + ES/MinIO/imgproxy
|
||||
- `C:\Users\vitya\VirtualBox VMs\snolla-recovery\` — VM home, **active prod** (running)
|
||||
- `C:\nas-recovery\backup\snolla\snolla.ova` — оригинал OVA (45.6 GB, для повторного импорта если что)
|
||||
- `C:\nas-recovery\vm-sites\wwwroot\`, `C:\nas-recovery\vm-sites\stayer\` — резервная копия IIS-сайтов из VM (использована для миграции на хост; пока хранится как safety net)
|
||||
- `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)
|
||||
- `snolla-identity-manager\` — deploy-артефакт, IIS-сайта нет
|
||||
- `C:\Users\vitya\projects\MoreThenCms\` — git-репо с исходниками CMS
|
||||
|
||||
## SSH-ключи на этой машине
|
||||
|
||||
- `~/.ssh/id_ed25519_kreknin` — для [[kreknin-synology]] (vitya@195.19.90.188)
|
||||
- `~/.ssh/id_ed25519_openwrt` — для [[openwrt-router]] (root@192.168.1.1)
|
||||
- `~/.ssh/id_ed25519_snolla_vm` — для [[snolla-recovery-vm]] (vitya@127.0.0.1:8022, в admin-keys VM)
|
||||
|
||||
## Что должно случиться, если этот PC погаснет
|
||||
|
||||
- **Всё** — все сайты лягут. **Single point of failure** — главное замечание для [[future-resilient-architecture-goals]].
|
||||
@@ -8,11 +8,33 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
|
||||
|
||||
## Entities
|
||||
|
||||
<!-- (none yet) -->
|
||||
- [dead-synology-diskstation](entities/dead-synology-diskstation.md) — мёртвая Synology DiskStation (source NAS)
|
||||
- [kreknin-synology](entities/kreknin-synology.md) — Kreknin Synology (backup target + DDNS)
|
||||
- [openwrt-router](entities/openwrt-router.md) — OpenWRT Router (192.168.1.1)
|
||||
- [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)
|
||||
- [windows-recovery-host](entities/windows-recovery-host.md) — Windows Recovery Host (рабочий PC пользователя)
|
||||
|
||||
## Concepts
|
||||
|
||||
- [admin-infra-project](concepts/admin-infra-project.md) — admin-infra-project
|
||||
- [admin-infra-project](concepts/admin-infra-project.md) — design + migration plan для OpeItcLoc03/admin (canonical)
|
||||
- [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 не петля
|
||||
- [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
|
||||
- [mssql-container-data-restore](concepts/mssql-container-data-restore.md) — MSSQL контейнер с восстановленными production data — паттерн
|
||||
- [portainer-2.21-admin-password-regression](concepts/portainer-2.21-admin-password-regression.md) — Portainer 2.21 `--admin-password` regression + min 12-char policy
|
||||
- [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
|
||||
- [rusonyx-vps-onboarding-quirks](concepts/rusonyx-vps-onboarding-quirks.md) — Rusonyx VPS onboarding quirks (Astra Облако / myvm.rusonyx.ru)
|
||||
- [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 миграции
|
||||
- [verdaccio-prune-semantics](concepts/verdaccio-prune-semantics.md) — Verdaccio prune semantics — proxied vs locally-published
|
||||
- [wd40efax-smr-cascade](concepts/wd40efax-smr-cascade.md) — WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18
|
||||
|
||||
## Packages
|
||||
|
||||
@@ -20,4 +42,6 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
|
||||
|
||||
## Sources
|
||||
|
||||
<!-- (none yet) -->
|
||||
- [iis-host-migration-2026-05-19](sources/iis-host-migration-2026-05-19.md) — IIS Host Migration Session 2026-05-19 chronology
|
||||
- [nas-recovery-session-2026-05-18](sources/nas-recovery-session-2026-05-18.md) — NAS Recovery Session 2026-05-18/19 chronology
|
||||
- [vds-kzntsv-bootstrap-2026-05-20](sources/vds-kzntsv-bootstrap-2026-05-20.md) — VDS bootstrap session 2026-05-20 chronology
|
||||
|
||||
@@ -9,3 +9,7 @@ Append-only log of wiki operations (ingests, promotions, lints, migrations).
|
||||
## [2026-05-21] ingest | concepts/admin-infra-project
|
||||
|
||||
## [2026-05-21] seed | CLAUDE.md Domain conventions — design-context pointers (task: admin-infra-project-pointers)
|
||||
|
||||
## [2026-05-21] migrate | subtree-import 6 entities + 17 concepts + 3 sources from MoreThenCms (history preserved via subtree-split + temp-prefix merge)
|
||||
|
||||
## [2026-05-21] regen | index.md catalog refresh after subtree import
|
||||
|
||||
275
.wiki/sources/iis-host-migration-2026-05-19.md
Normal file
275
.wiki/sources/iis-host-migration-2026-05-19.md
Normal file
@@ -0,0 +1,275 @@
|
||||
---
|
||||
title: IIS Host Migration Session 2026-05-19
|
||||
type: source
|
||||
tags: [migration, iis, windows, recovery, traefik]
|
||||
ingested: 2026-05-19
|
||||
raw_path: ../../.tasks/iis-on-host-migration.md
|
||||
updated: 2026-05-21
|
||||
---
|
||||
|
||||
# IIS Host Migration Session 2026-05-19
|
||||
|
||||
Перенос основного CMS-сайта (`MoreThenCms.Web`, обслуживает 11 клиентских доменов через Host header) с IIS внутри [[snolla-recovery-vm]] на нативный IIS [[windows-recovery-host]]. Цель — убрать VBox как слой нестабильности.
|
||||
|
||||
## Хронология
|
||||
|
||||
**Старт сессии** (после [[nas-recovery-session-2026-05-18]]) — VM один раз отвалилась сетью, оживили через `VBoxManage guestcontrol` + `ipconfig /release /renew` (recipe в [[vbox-windows-stability-tuning]] подтверждён ещё раз).
|
||||
|
||||
**Phase 1 — discovery (read-only):**
|
||||
- В VM через SSH (NAT-forward 127.0.0.1:8022, не bridged 192.168.1.15 — это устарело): `appcmd list site/apppool/app/vdir /xml`.
|
||||
- На хосте: `Get-WindowsOptionalFeature -Online IIS-*` через elevated PS — все нужные фичи (`IIS-WebServer`, `IIS-ASPNET45`, `IIS-NetFxExtensibility45`, `IIS-ISAPIFilter`, `IIS-ManagementConsole`, `IIS-IIS6ManagementCompatibility`, `IIS-Metabase`) уже установлены. `WebAdministration` PS-module грузится из admin-PS.
|
||||
- В источниках на хосте (`grep`): hardcoded `C:\inetpub\wwwroot\` в коде CMS нет — всё через `ConfigurationManager.AppSettings["sitePath"]`. **НО** в prod Web.config (из backup `vm-sites\`) ключ `sitePath` явно проставлен абсолютным путём — патч в Phase 2 необходим.
|
||||
|
||||
**Phase 1 находки (важные расхождения с wiki):**
|
||||
- `Snolla.IdentityManager` в VM **на :8089, не :80** (вики говорила :80). Sub-apps stostayer.old `/calc`, `/price`, `/price/tireService` существуют с отдельными app pools.
|
||||
- `C:\stayer\Snolla.IdentityManager\Web.config` указывает на **`SRV-1135520-1\SQLEXPRESS` + Integrated Security** — мёртвый внешний сервер. Сайт точно не работает, deploy-артефакт без живого консьюмера.
|
||||
- `C:\stayer\MoreThenCms.Web\Web.config` (stostayer) использует **внешний production MSSQL `89.253.219.2,1433`** с user `stostayer` — это **другая инфраструктура**, не наш контейнер.
|
||||
|
||||
**Phase 2 — миграция (admin PS):**
|
||||
- `Default Web Site` остановлен (`autoStart=false`).
|
||||
- `robocopy` из `C:\nas-recovery\vm-sites\` → `C:\sites\MoreThenCms.Web` (8.7 GB / 44.7k файлов / 1m13s) и → `C:\stayer\` (2.2 GB / 13.9k файлов / 21s).
|
||||
- Web.config patch только для `C:\sites\MoreThenCms.Web\Web.config`: `sitePath` `C:\inetpub\wwwroot\MoreThenCms.Web\` → `C:\sites\MoreThenCms.Web\` + `Data Source=10.0.2.2` → `Data Source=localhost`. UTF-8 with BOM через `[System.IO.File]::WriteAllText` (паттерн из [[cms-config-rewrite-pattern]]).
|
||||
- 3 AppPool (`.NET v4.0` Integrated, `ApplicationPoolIdentity`): `MoreThenCms.Web`, `stostayer`, `stostayer.old`.
|
||||
- 3 IIS-сайта: `MoreThenCms.Web *:80` (catch-all), `stostayer *:8090`, `stostayer.old *:8091`.
|
||||
- ACL `icacls /grant 'IIS AppPool\<site>:(OI)(CI)M' /T` на каждый physical root.
|
||||
- Skip: `Snolla.IdentityManager` (по решению — публично не нужен), 3 sub-apps stostayer.old (по решению).
|
||||
- Smoke test через `http://localhost/` с Host header: все 11 хостов отвечают (большинство 302 redirect — CMS работает). `rimiz.ru` — 404 (CMS-side, не инфра). stostayer/oldstostayer на :8090/:8091 — timeout, **в текущей сессии не разбирались**.
|
||||
|
||||
**Phase 3 — traefik switch (file-provider auto-reload):**
|
||||
- Backup `*.yml.bak-phase3-2026-05-19` для 11 файлов в `C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\`.
|
||||
- Patch: `host.docker.internal:18080` → `host.docker.internal:80` (11 yml: snolla, rimiz, labtools, labtoolspro, pilorama98, tandemmebel, emspb, kupimknigi, maljarka, sestech, isc-artmaterials). UTF-8 без BOM.
|
||||
- **Не трогали** `stostayer.yml` (`:18180`) и `oldstostayer.yml` (`:18181`) — stayer-сайты остаются на VM.
|
||||
- Public smoke через `https://localhost:4443/` с Host header: **10/11 → HTTP 200**. Только `rimiz.ru` → 404 (как и в локальном тесте).
|
||||
|
||||
## Результат
|
||||
|
||||
Production-трафик 10 главных доменов теперь идёт **полностью без VM**: client → OpenWRT → traefik → host-IIS native → MSSQL container на хосте. VM продолжает обслуживать только stostayer/oldstostayer через старые NAT port forwards (`:18180/:18181`).
|
||||
|
||||
## Открытые вопросы / нюансы
|
||||
|
||||
- **`rimiz.ru` отдаёт 404** на host-IIS (и на VM до миграции тоже отдавал?). CMS-routing не знает этот host. Проверить mapping в БД / CMS-сайт-таблице.
|
||||
- **stostayer + stostayer.old timeout** на host-IIS (`http://localhost:8090/`, `:8091`). Возможно cold-start ASP.NET, возможно реальный bug. Сейчас не разбирали — traefik для них всё ещё на VM.
|
||||
- **Snolla.IdentityManager не мигрирован** — если что-то внутри CMS дёргает его через `localhost:8089`, может тихо ломаться (пока симптомов не видно).
|
||||
- **VM можно глушить** только после: либо доделать stostayer на host, либо явное решение не мигрировать stayer на этот хост.
|
||||
- **Phase 5 не сделан**: URL Rewrite + ARR (фикс `:4443` в редиректах CMS), Application Initialization (warm-start), log rotation `C:\inetpub\logs\`.
|
||||
|
||||
## Технические артефакты
|
||||
|
||||
- `.tasks/iis-on-host-migration.md` — детальный план фаз, обновлён со статусом Phase 1-3 = done.
|
||||
- Backup конфигов traefik: `*.yml.bak-phase3-2026-05-19`.
|
||||
- Скрипты Phase 2 и Phase 3 — в истории чата сессии (не сохранены отдельным файлом, идемпотентны на повторный запуск).
|
||||
|
||||
## Phase 4 — реорг `C:\sites\` + перенос stayer'ов (вечер 2026-05-19)
|
||||
|
||||
Пользователь возмутился разбросом (`C:\sites\` только для snolla, `C:\stayer\` для stayer). Реорганизация ради единой иерархии:
|
||||
|
||||
- `C:\sites\MoreThenCms.Web` → `C:\sites\snolla` (rename)
|
||||
- `C:\stayer\MoreThenCms.Web` → `C:\sites\stostayer` (move)
|
||||
- `C:\stayer\stostayer.old` → `C:\sites\stostayer.old` (move)
|
||||
- `C:\stayer\Snolla.IdentityManager` → `C:\sites\snolla-identity-manager` (move + kebab-rename)
|
||||
- Удалены: `C:\stayer\Mis.StoStayer.{Price,TireService,Calculator}.{Api,Web}` (sub-apps не нужны).
|
||||
- `C:\stayer\` полностью удалён.
|
||||
|
||||
IIS sync:
|
||||
- Site `MoreThenCms.Web` → переименован в `snolla` (`Set-ItemProperty IIS:\Sites\... -Name name`)
|
||||
- AppPool `MoreThenCms.Web` — **rename невозможен in-place**, пересоздан как `snolla` с тем же набором properties (.NET v4.0 Integrated, `ApplicationPoolIdentity`), site rebound, старый pool удалён.
|
||||
- `physicalPath` всех 3 sites обновлён под `C:\sites\<name>`.
|
||||
- Web.config sitePath patches:
|
||||
- `C:\sites\snolla\Web.config`: `C:\sites\MoreThenCms.Web\` → `C:\sites\snolla\`
|
||||
- `C:\sites\stostayer\Web.config`: `C:\stayer\MoreThenCms.Web\` → `C:\sites\stostayer\`
|
||||
- ACL re-grant + cleanup stale ACE для `IIS AppPool\MoreThenCms.Web`.
|
||||
|
||||
## Phase 5 — stostayer.old DB-conn патч (запутанная история conn-string'ов в stayer\)
|
||||
|
||||
Локальный smoke `:8091` падал в timeout. Причина: `C:\sites\stostayer.old\web.config` имел `Data Source=10.0.2.2` (адрес VBox NAT gateway, на хосте не резолвится) с user=`snolla` — фактически идентичная нашему MSSQL контейнеру конфигурация, только адрес неправильный. Patch: `10.0.2.2` → `localhost`. После — `:8091` → 200.
|
||||
|
||||
Это **наша ошибка** Phase 2 — пропустили patch этого файла, так как фокус был только на `MoreThenCms.Web\Web.config`.
|
||||
|
||||
## Phase 6 — stostayer DB-conn миграция на новый сервер (`www.stostayer.ru`)
|
||||
|
||||
Локальный smoke `:8090` тоже timeout — но по другой причине. `C:\sites\stostayer\Web.config` указывал на `89.253.219.2,1433` (внешний production MSSQL), оказался **полностью недоступен**: TCP timeout, ping fail, traceroute затухает на 8-м hop у `139.45.230.171`. На VM (через тот же путь) тоже не работал — пользователь подтвердил.
|
||||
|
||||
Пользователь предоставил новые creds: `www.stostayer.ru,1433` / `stayer_site` / `^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF`. Тест из не-elevated PS: TCP reachable, SQL login OK (SQL Server 2022). Patched Web.config.
|
||||
|
||||
**Pitfall:** после patch IIS отдавал HTTP 500. Причина — символ `&` в пароле, который **в XML является зарезервированным**. ASP.NET Web.config XML парсер падал на парсинг conn-string. Fix: `&` → `&` (XML entity escape). См. [[webconfig-password-xml-escape]].
|
||||
|
||||
## Phase 7 — отключение traefik routes для stayer'ов
|
||||
|
||||
Пользователь решил: stayer'ы наружу светить не нужно вообще (даже после миграции на хост). `stostayer.yml` и `oldstostayer.yml` переименованы в `*.yml.disabled` — traefik file-provider не подхватывает, route gone. Site'ы на хосте `:8090/:8091` остаются для возможного внутреннего использования.
|
||||
|
||||
Backup yml для этих изменений: `*.yml.bak-stayer-switch-2026-05-19` (содержит вариант с переключением на `:8090/:8091` — на случай если решим включить обратно).
|
||||
|
||||
## Phase 8 — VM savestate
|
||||
|
||||
После того как 100% prod-трафика идёт через host-IIS, VM `snolla-recovery` заморожена через `VBoxManage controlvm "snolla-recovery" savestate`. VMState = `saved`. Конфигурация и диски сохранены — resume за 5-10 сек если что-то понадобится. Не удалена.
|
||||
|
||||
## Финальное состояние (2026-05-19 конец сессии)
|
||||
|
||||
```
|
||||
C:\sites\
|
||||
├── snolla\ (was MoreThenCms.Web in C:\inetpub\wwwroot, was in vm-sites\wwwroot\MoreThenCms.Web)
|
||||
├── stostayer\ (was C:\stayer\MoreThenCms.Web)
|
||||
├── stostayer.old\ (was C:\stayer\stostayer.old)
|
||||
└── snolla-identity-manager\ (was C:\stayer\Snolla.IdentityManager — deploy artifact, без живого consumer'а)
|
||||
|
||||
IIS sites/pools (имя=имя=пуло):
|
||||
snolla *:80 → C:\sites\snolla (pool snolla, .NET v4.0 Integrated, AppPoolIdentity)
|
||||
stostayer *:8090 → C:\sites\stostayer (pool stostayer)
|
||||
stostayer.old *:8091 → C:\sites\stostayer.old (pool stostayer.old)
|
||||
|
||||
Connection strings:
|
||||
snolla: Data Source=localhost → MSSQL container на хосте (DB MoreThenCms)
|
||||
stostayer: Data Source=www.stostayer.ru,1433 (user stayer_site) → внешний SQL Server 2022
|
||||
stostayer.old: Data Source=localhost → MSSQL container на хосте (DB stostayer, user snolla)
|
||||
|
||||
Traefik active routes: 10 главных доменов (snolla.com и др.) + rimiz.ru → host:80.
|
||||
Stayer routes (stostayer.snolla.com, old.stostayer.ru) DISABLED — приватны.
|
||||
Mёртвые: disk.yml, dsm.yml (192.168.1.10).
|
||||
|
||||
VM snolla-recovery: VMState=saved (frozen, ~92 GB на диске + ~3 GB savestate RAM dump).
|
||||
```
|
||||
|
||||
## Открытые вопросы (что унесли в следующую сессию)
|
||||
|
||||
- **`rimiz.ru` отдаёт 404** на host-IIS. CMS-routing не знает этот host — нужно копнуть БД (таблица CMS-сайтов, mapping host header → site).
|
||||
- **Snolla.IdentityManager** на хосте: папка перенесена, но IIS-сайт не создавался (по решению). Если CMS-код где-то дёргает `localhost:8089` или подобное — будет тихий fail. Симптомов пока нет.
|
||||
- **Phase 5 (из старого плана)**: URL Rewrite + ARR для фикса `:4443` в редиректах, Application Initialization (warm-start), log rotation `C:\inetpub\logs\`. Не блокеры.
|
||||
- **VM-cleanup**: если через ~неделю стабильной работы host'а проблем не будет — `VBoxManage unregistervm "snolla-recovery" --delete` освободит ~92 GB. NAT port forwards в OpenWRT (host:18080/18180/18181/18189) можно удалить.
|
||||
|
||||
## Связано
|
||||
|
||||
[[recovery-architecture-snapshot]] обновлён под полностью-host chain. [[snolla-recovery-vm]] — VMState=saved, все sites unused. [[windows-recovery-host]] — финальный layout `C:\sites\`. [[webconfig-password-xml-escape]] — новый concept про XML entity escape в conn-string'ах. [[cms-config-rewrite-pattern]] — UTF-8 BOM подтверждён ещё много раз.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — RЕVERT (вечер 2026-05-19, после провала)
|
||||
|
||||
**Что случилось:** через ~10 минут после моего commit'a "done" пользователь открыл `https://www.pilorama98.ru/` → `502 Bad Gateway`. Дальше остальные домены показали `TOO_MANY_REDIRECTS`.
|
||||
|
||||
**Root cause** (расписан полностью в [[iis-migration-2026-05-19-postmortem]]):
|
||||
- Phase 3 я patched traefik backend `host.docker.internal:18080` → `host.docker.internal:80`.
|
||||
- Внутри traefik-контейнера `host.docker.internal:80` через Docker Desktop NAT резолвится **обратно в сам traefik** на его HTTP entrypoint :80 (potential Docker publish-port loopback gotcha).
|
||||
- traefik `https.yml` имеет http-catchall middleware `redirect-to-https` → traefik отвечает 301 на свой же запрос → loop.
|
||||
|
||||
**Реактивная цепочка ошибок** (~1 час):
|
||||
- Restart-WebAppPool, nuke workers, docker restart traefik → only sделали хуже (503).
|
||||
- Patched `host.docker.internal:80 → 192.168.1.143:80` → тот же loop (LAN-IP через NAT тоже возвращается в traefik).
|
||||
- Patched на `:8088` + добавил IIS binding → нужен `Stop+Start Website` чтобы binding applied, изначально забыл → IIS не listen → 503.
|
||||
- Думал что 301 от CMS (Pitfall 5 X-Forwarded-Proto), копал CMS код — реальный источник был самим traefik (headers без `Server: Microsoft-IIS` подсказывали).
|
||||
|
||||
**Revert (выполнен):**
|
||||
- `VBoxManage startvm "snolla-recovery"` — VM поднята из savestate (~10s + 90s warmup + recipe `ipconfig /release /renew` для застрявшего network adapter из [[vbox-windows-stability-tuning]]).
|
||||
- 11 главных yml восстановлены из `*.bak-phase3-2026-05-19` (`host.docker.internal:18080` → VM).
|
||||
- Stayer yml: `stostayer.yml.disabled / oldstostayer.yml.disabled` удалены, восстановлены из `*.bak-stayer-switch-2026-05-19` (`:18180/18181` → VM).
|
||||
- `docker restart traefik` (file-provider не подхватил reload автоматом).
|
||||
- Public smoke через VM-chain → 7/11 хостов отвечают (2 c 200, 5 с CMS-side редиректами на canonical — нормально для CMS-логики); проверено пользователем в браузере → работает.
|
||||
|
||||
## Финальное состояние (после revert)
|
||||
|
||||
Production снова на VM-chain (как было в начале сессии 2026-05-19). На хосте осталось:
|
||||
- `C:\sites\{snolla, stostayer, stostayer.old, snolla-identity-manager}` — ~11 GB, **inert** (не на prod-пути)
|
||||
- IIS sites/pools `snolla, stostayer, stostayer.old` — не получают трафика (traefik backend назад на VM)
|
||||
- traefik backups: `*.bak-phase3-2026-05-19`, `*.bak-stayer-switch-2026-05-19`, `*.bak-hostip-2026-05-19`
|
||||
- VM `snolla-recovery` — running, IIS активен, обслуживает 11 главных доменов + 2 stayer публично через port forwards.
|
||||
|
||||
**Артефакты для следующей попытки** (НЕ удалять):
|
||||
- Web.config'и в `C:\sites\` пропатчены правильно (sitePath, conn-strings, stostayer на новый DB) — можно переиспользовать.
|
||||
- IIS sites/pools уже настроены — переключение требует только traefik backend patch + tested correctly.
|
||||
- Recipe для правильной миграции — в [[iis-migration-2026-05-19-postmortem]].
|
||||
|
||||
---
|
||||
|
||||
## Phase 10 — Attempt 2 (вечер 2026-05-19, после прочтения post-mortem)
|
||||
|
||||
Повторная попытка миграции, выполненная **по recipe из [[iis-migration-2026-05-19-postmortem]]**. Все 5 главных правил соблюдены, миграция прошла без incidents.
|
||||
|
||||
### Что сделано
|
||||
|
||||
1. **Read-only sanity (без prod-touches):** VM running confirmed, port-scan на хосте показал `:18090` и `:8089` свободны, traefik dir contains 4 серии bak-stamp'ов сохранённых от attempt 1, IIS sites/pools (`snolla, stostayer, stostayer.old`) живы.
|
||||
2. **Backend port: `:8089`** (recipe-A — НЕ `:80`, НЕ совпадает с traefik publish `:8000/:4443/:8080`, НЕ совпадает с VM NAT forwards `:18080/:18180/:18181`). Выбор user'а (был кандидат `:18090`, user предпочёл `:8089`).
|
||||
3. **Atomic revert plan ДО старта** (recipe-E): новая bak-серия `.bak-pre-attempt2-2026-05-19` для всех 13 yml (11 cms + 2 stayer), paste-ready команда восстановления записана в `.tasks/STATUS.md`.
|
||||
4. **IIS binding** `snolla *:8089` добавлен через elevated PS (`New-WebBinding` + `Stop-Website; Start-Website` — recipe-A note: без restart binding не активируется).
|
||||
5. **Loop-detect через `docker exec`** (recipe-A + recipe-F): `docker exec traefik wget --spider -S --header="Host: emspb.ru" http://host.docker.internal:8089/` → first response `301 → http://www.emspb.ru/` от `Server: Microsoft-IIS/10.0`. `host.docker.internal` resolves to `192.168.65.254:8089` — это Docker Desktop host gateway, **не traefik publish-port**. NO NAT loop. См. [[docker-host-loopback-detect]] про общую технику.
|
||||
6. **Smoke test с `-MaximumRedirection 0`** (recipe-B): `curl.exe -k -I --max-redirs 0 -H "Host: emspb.ru" https://localhost:4443/` → first response `301`, `Server: Microsoft-IIS/10.0`, `Location: http://www.emspb.ru/`. Это **expected CMS canonical redirect** (та же логика что была на VM).
|
||||
7. **Canary atomic** — patched ОДИН yml (`emspb.yml`), file-provider auto-reload, public smoke clean → 📱 **phone-test с мобильного интернета** (recipe-C) → `https://emspb.ru/` открылось → ✅.
|
||||
8. **Second canary** — `labtools.yml`, smoke + phone-test → ✅.
|
||||
9. **Batch patch** оставшихся 9 cms yml (snolla, rimiz, labtoolspro, pilorama98, tandemmebel, kupimknigi, maljarka, sestech, isc-artmaterials) одним PS-блоком (recipe-G: batch не item-by-item) → smoke 20 hostnames → все `Server: Microsoft-IIS/10.0` ✅.
|
||||
10. **Stayer routes — disabled** (re-confirmed Phase 7 decision): user подтвердил «stayer'ы локальные, через traefik наружу не светят». Rename `stostayer.yml → stostayer.yml.disabled`, `oldstostayer.yml → oldstostayer.yml.disabled`. `docker restart traefik` (file-provider не подхватил deletion auto-reload). Verify: `stostayer.snolla.com`/`old.stostayer.ru` → `404 text/plain` (traefik no-route). Host IIS sites `:8090`/`:8091` остаются live для прямого/локального доступа.
|
||||
11. **VM running parallel** (recipe-D): VM **НЕ savestate'ить** минимум 24h+. Освободит RAM/disk только после подтверждённой стабильности host'а.
|
||||
|
||||
### Pitfall found and resolved
|
||||
|
||||
- **WinHTTP proxy strips Server header.** Локальный `curl.exe http://localhost:8090/` returned response без `Server: Microsoft-IIS/10.0` и без `X-Powered-By: ASP.NET` (плюс с `Proxy-Connection: keep-alive`) — выглядело как «не IIS отвечает». Это привело к moment'у panic. Но `docker exec traefik wget ...` (real production path) **показал headers корректно**. Вывод: для loop-detect / IIS-confirmation тестов **использовать `docker exec` из traefik container**, не Windows `curl.exe` — последний ходит через Windows-уровневый proxy который headers вырезает.
|
||||
|
||||
### My mistake during this session
|
||||
|
||||
- Я неверно интерпретировал «мигрировать stayer'ов» как «patch traefik backend → host:8090/8091» (т.е. пускать через traefik наружу). User имел в виду «host IIS уже на :8090/:8091, traefik routes должны быть **DISABLED**». Сделал prod-changing patch → user интервент-stop → revert + rename `.yml.disabled`. См. memory `feedback-migrate-semantics`. Lesson — переспрашивать semantics для internal/low-traffic сервисов перед prod-changing.
|
||||
|
||||
### Финальный chain (после attempt 2)
|
||||
|
||||
```
|
||||
Клиент (browser)
|
||||
→ DNS → 94.19.247.14 (public IP)
|
||||
→ OpenWRT NAT 443 → 192.168.1.143:4443
|
||||
→ traefik:4443 (TLS termination)
|
||||
→ match Host → одно из 11 cms-yml → backend
|
||||
→ http://host.docker.internal:8089/
|
||||
↳ Docker Desktop host gateway 192.168.65.254:8089
|
||||
→ Windows host IIS site `snolla` (*:80 + *:8089 bindings)
|
||||
→ C:\sites\snolla\, .NET Framework 4.8.1, ApplicationPoolIdentity
|
||||
→ conn → MSSQL container на host:1433
|
||||
← HTTP response
|
||||
```
|
||||
|
||||
Stayer chain: traefik routes disabled. `stostayer.snolla.com` / `old.stostayer.ru` → traefik 404. Host IIS sites `:8090`/`:8091` живут для локального доступа.
|
||||
|
||||
VM `snolla-recovery`: **running parallel** (24h+ soak), backend в traefik больше не используется — VM NAT port forwards (`:18080/:18180/:18181`) холостые.
|
||||
|
||||
### Артефакты этой сессии
|
||||
|
||||
- `.tasks/STATUS.md` — статус task'а 🔴 active.
|
||||
- traefik backups в `data/custom/`: `*.yml.bak-pre-attempt2-2026-05-19` для 13 yml, плюс `stostayer.yml.disabled`, `oldstostayer.yml.disabled`.
|
||||
- `feedback-migrate-semantics` memory — урок для будущих сессий.
|
||||
- `concepts/docker-host-loopback-detect.md` — новый concept для loop-detect technique.
|
||||
|
||||
## Что осталось open
|
||||
|
||||
- **24h+ soak** до savestate VM (минимум до утра 2026-05-20, лучше 48h до 2026-05-21).
|
||||
- **VM cleanup**: после soak — `savestate` (освободит RAM), позже `unregistervm --delete` (освободит ~92 GB).
|
||||
- **Port forwards OpenWRT** (`host:18080/18180/18181/18189` → VM) — больше не нужны, удалить позже.
|
||||
- **rimiz.ru / ics-artmaterials.com 404** — CMS-side routing issues, не инфра. Открытым.
|
||||
- **X-Forwarded headers** (`:4443` в redirect URL) — известный bug [[traefik-on-windows-docker-desktop]] Pitfall 5, отдельная задача.
|
||||
|
||||
## Phase 11 — close-out 2026-05-21 (~36h soak)
|
||||
|
||||
Soak window 2026-05-19 22:00 MSK → 2026-05-21 ~08:00 MSK ≈ 36 hours. Verify:
|
||||
|
||||
- **Traefik:** `Up 38 hours` (`docker ps`) — zero restarts, zero unscheduled bouncing.
|
||||
- **w3wp.exe** (IIS worker для `snolla` site): PID 10432, CreationDate 2026-05-20 23:19:21 MSK, uptime 8.8h. Это **scheduled IIS app pool recycle** (default 1740 min ≈ 29h), не crash — нормальное поведение pool'а после ~29h работы. Память 522 MB.
|
||||
- **HTTPS smoke** 11 заявленных hosts через `docker exec traefik wget --spider --server-response https://<host>/`:
|
||||
|
||||
| Host | Code | Server | Verdict |
|
||||
|---|---|---|---|
|
||||
| `emspb.ru` | 301→www | Microsoft-IIS/10.0 | ✅ |
|
||||
| `snolla.com` | 301→on.snolla | Microsoft-IIS/10.0 | ✅ |
|
||||
| `on.snolla.com` | 200 | Microsoft-IIS/10.0 | ✅ |
|
||||
| `pilorama98.ru` | 301→www | Microsoft-IIS/10.0 | ✅ |
|
||||
| `labtools.ru` | 301→www | Microsoft-IIS/10.0 | ✅ |
|
||||
| `labtools.pro` | 301→www | Microsoft-IIS/10.0 | ✅ |
|
||||
| `tandemmebel.ru` | 301→www | Microsoft-IIS/10.0 | ✅ |
|
||||
| `kupimknigi.spb.ru` | 200 | Microsoft-IIS/10.0 | ✅ |
|
||||
| `maljarka.ru` | — | DNS bad address | ⚠️ off-infra (nslookup 8.8.8.8 → no A record) |
|
||||
| `sestech.ru` | — | TLS cert `CN=*.domainparking.ru` (expired 2026-05-13) | ⚠️ off-infra (domain parking provider) |
|
||||
| `ics-artmaterials.com` | 200 | `nginx-reuseport/1.21.1` | ⚠️ off-infra (A→87.236.16.28, мигрирован на сторонний WP-хостинг) |
|
||||
|
||||
**Итог:** 8/8 наших sites зеленые. 3 hostname'а из исходного списка task'а оказались **уже не на нашей инфраструктуре** — DNS либо снят, либо переключен на сторонние хостинги. Это не regression миграции; эти domains надо просто убрать из traefik/IIS routing'а как dead.
|
||||
|
||||
Decisions:
|
||||
- iis-host migration close-out: **DONE 2026-05-21**.
|
||||
- VM `snolla-recovery`: готова к savestate (soak passed, zero rollbacks). User-side savestate решает отдельно.
|
||||
- 3 off-infra domains: новая задача — `iis-traefik-dead-routes-cleanup` (low prio).
|
||||
|
||||
## Артефакты Phase 11
|
||||
|
||||
- `.tasks/STATUS.md` — task → 🟢 done.
|
||||
- (existing) traefik bak-серия `.bak-pre-attempt2-2026-05-19` — оставить ещё неделю (atomic revert на случай unforeseen regression).
|
||||
78
.wiki/sources/nas-recovery-session-2026-05-18.md
Normal file
78
.wiki/sources/nas-recovery-session-2026-05-18.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: NAS Recovery Session 2026-05-18/19
|
||||
type: source
|
||||
tags: [recovery, nas, synology, virtualbox, traefik, mssql, minio, elasticsearch]
|
||||
ingested: 2026-05-19
|
||||
raw_path: ../../.tasks/nas-recovery.md
|
||||
updated: 2026-05-19
|
||||
---
|
||||
|
||||
# NAS Recovery Session 2026-05-18/19
|
||||
|
||||
15-часовая сессия восстановления клиентских сайтов после краха NAS Synology, на котором они хостились. Источник истины — лог переписки восстановления + `.tasks/nas-recovery.md`. Здесь сжатая хронология; конкретные паттерны и решения распилены по `concepts/`, инфраструктурные сущности — по `entities/`.
|
||||
|
||||
## Контекст до краха
|
||||
|
||||
- **Source NAS (теперь мёртвый):** Synology DiskStation на XPEnology (самосборное x86 железо + DSM через community-loader). На нём:
|
||||
- VMM (Virtual Machine Manager) → Windows-VM **snolla** с IIS + .NET Framework 4.8 CMS [[snolla-recovery-vm]]
|
||||
- Container Manager → docker-стек: MSSQL Server 2019, MinIO 2020-07-13, Elasticsearch 7.10.1, imgproxy+nginx, traefik 2.6.6, gitea, и др.
|
||||
- 11 клиентских сайтов под одной VM (snolla.com + 10 client TLDs: rimiz.ru, labtools.pro/ru, pilorama98.ru, tandemmebel.ru, emspb.ru, kupimknigi.spb.ru, maljarka.tandemmebel.ru, sestech.ru, ics-artmaterials.com)
|
||||
- **Backup target NAS (живой):** [[kreknin-synology]] на 195.19.90.188 / kreknin.site, держит Hyper Backup репо.
|
||||
- **Бэкап-задача:** "rsync Server 1", последняя успешная — 2026-05-09 05:06 (за 9 дней до краха).
|
||||
|
||||
См. также: [[dead-synology-diskstation]], [[wd40efax-smr-cascade]].
|
||||
|
||||
## Хронология
|
||||
|
||||
**2026-05-18 ~17:00 MSK:** инцидент — второй из 3 дисков RAID 5 на mёртвой синке вышел из строя. Пул past redundancy. Diagnose см. [[wd40efax-smr-cascade]].
|
||||
|
||||
**~17:30:** оценка вариантов. Выбран маршрут "восстановление на локальной Windows-машине пользователя" ([[windows-recovery-host]]) с гибридной архитектурой: VM для CMS (из OVA-экспорта) + docker-контейнеры для backend-сервисов.
|
||||
|
||||
**~18:00:** SSH-доступ к [[kreknin-synology]] (изначально по паролю, потом через ssh-ключ). Разбор структуры `.hbk` репо. См. [[hyper-backup-structure-and-recovery]].
|
||||
|
||||
**~19:00:** старт Hyper Backup restore через DSM UI на удалённой синке во временную папку `/volume1/restore-tmp/`. Restore инициировал также создание новых шар `backup/`, `docker/`, `work/` на root уровне `/volume1/`. Длительность ~3 часа на 277 GB selective набор.
|
||||
|
||||
**~21:00 — параллельно:**
|
||||
- SFTP-pull `snolla.ova` (42.5 GB) на Windows — через FileZilla (SFTP сервис DSM требовалось включить отдельно, ACL/chroot нюансы).
|
||||
- Локальная подготовка: установлены IIS, VirtualBox 7.2.8.
|
||||
- На Windows запущен MSSQL контейнер 2019-latest пустым (для отладки compose).
|
||||
- v2rayN на Windows: настроены routing rules "Bypass LAN" (`geoip:private` → direct), иначе VPN ловил inbound 80/443 traffic.
|
||||
|
||||
**~22:00:** найден `MoreThenCms202605090301.zip` — ежедневный `.sql` дамп БД (101 MB compressed, 547 MB unpacked). Попытка restore через `sqlcmd -i` — упала на строке 457k из-за `$(function(){...})` в данных (jQuery JS в email-template таблицах). Sqlcmd интерпретировал `$()` как переменную. См. [[mssql-restore-pitfalls]].
|
||||
|
||||
**~23:00:** повторный sqlcmd с флагом `-x` (disable var substitution). Параллельно pull `/docker/personal/mssql/` (25 GB) как альтернатива — через ACL-fix `chmod -R a+rX`, потом `scp -O` (legacy SCP, обход chrooted SFTP).
|
||||
|
||||
**2026-05-19 ночь (Claude автономно):**
|
||||
- OVA import в VirtualBox завершился (42.7 мин)
|
||||
- sqlcmd v2 длился ~2.5 часа, тоже падал.
|
||||
- Решение: **bind-mount проблемы → named volume + `chown -R 10001:0`** через temp alpine container. Это сработало. См. [[mssql-container-data-restore]].
|
||||
- Имя dead synology в БД-файлах было всё с одинаковым паролем `fXkH4@8O%3pc` (production SA password из старого `docker-compose.yml`). Аккаунт `sa` оказался **disabled**, потребовался `mssql-conf set-sa-password` под `--user 0:0` (root) → re-enable.
|
||||
|
||||
**~02-08:00 утра:** VM на VBox не загружалась. Кросс-гипервизорный crash. См. [[vbox-windows-stability-tuning]] — путь к стабильности через SCSI→SATA, отключение Hyper-V driver в Safe Mode, `--paravirtprovider kvm`, OS type Windows10_64, Guest Additions.
|
||||
|
||||
**~09:30:** VM стабилизирована. Network drama #1: bridged через WiFi нестабильно (promiscuous mode проблемы у WiFi-адаптера). Switched VM nic1 на NAT + port forwarding в VBoxManage: host:23389→VM:3389, host:8022→VM:22, host:18080→VM:80, host:18180→VM:8080, host:18181→VM:8081, host:18189→VM:8089.
|
||||
|
||||
**~10:00:** OpenSSH server установлен внутри VM. SSH-ключ для Claude в `C:\ProgramData\ssh\administrators_authorized_keys` (особая локация для admin-users, локализованная группа `Администраторы` через icacls). Default shell sshd переключен на PowerShell.
|
||||
|
||||
**~10:30:** Web.config-патч во всех 4 сайтах VM: `Data Source=192.168.1.10` → `Data Source=10.0.2.2` (VBox NAT gateway = host). Encoding ловушка: `Set-Content` без `-Encoding utf8` записал UTF-16 LE — IIS вернул 500.19 invalid XML. Fix: `[System.IO.File]::WriteAllText` с `UTF8Encoding($true)` (BOM). См. [[cms-config-rewrite-pattern]].
|
||||
|
||||
**~11:00:** Traefik 2.6.6 запущен на хосте. Полный цикл правок: `--configFile=/traefik.yml` явно (не находил автоматом), named volume для `letsencrypt/` (bind-mount показывал `0777` Linux-side, traefik требует `0600`), 13 custom yml файлов пропатчены `192.168.1.15` → `host.docker.internal:18080`. docker.sock провайдер не работал (Docker Desktop особенности) → minio/imgproxy/elasticsearch traefik-labels переписаны как file-provider в `data/custom/`. См. [[traefik-on-windows-docker-desktop]].
|
||||
|
||||
**~11:30:** OpenWRT [[openwrt-router]] на 192.168.1.1: DHCP-резервация Windows-PC на 192.168.1.143 (его MAC 88:66:5A:2F:AA:68), port forwards 80→8000 и 443→4443 (host:8000/4443 ↔ traefik). VM получила старый MAC `02:11:32:2A:7C:B9` из DHCP-резервации `snolla` — IP 192.168.1.15 сохранился для совместимости с `snolla.yml` (но потом перешли на NAT, IP стал внутренним).
|
||||
|
||||
**~12:00:** Public test через домен/чужой WiFi: **`https://snolla.com`, `https://pilorama98.ru`, `https://labtools.ru`, `https://labtools.pro`, `https://tandemmebel.ru`, `https://emspb.ru`, `https://kupimknigi.spb.ru`, `https://maljarka.tandemmebel.ru` отвечают `200 OK` end-to-end.** Recovery functionally complete.
|
||||
|
||||
## После полного recovery — резервный pull
|
||||
|
||||
В фоне на Windows: tar+ssh stream `C:\inetpub\wwwroot\` (8.9 GB) и `C:\stayer\` (2.27 GB) из VM в `C:\nas-recovery\vm-sites\` — как фолбэк если VM снова станет нестабильной (был один случай glitch network — лечился `ipconfig /release /renew` через `VBoxManage guestcontrol`).
|
||||
|
||||
## Открытые вопросы / нюансы
|
||||
|
||||
- **X-Forwarded-Proto/Host headers** между traefik и CMS не настроены → CMS делает redirect с `:4443` в URL.
|
||||
- **MinIO / Azure storage** в CMS: connection string использует Azure SDK (AccountName=snolla, AccountKey=...), но в production реально работало с MinIO. Точная схема "не так, как казалось" по словам пользователя — ждёт пояснения.
|
||||
- **acme.json renewal через HTTP-01** фейлится для доменов с DNS не на нашем IP. Решение — DNS-01 через REGRU (creds в `traefik/docker-compose.yml` env уже, в `traefik.yml` закомментировано).
|
||||
- **VM long-term stability**: один случай network glitch уже был. Возможна планка scheduled task внутри VM — auto release/renew при детекции downtime.
|
||||
|
||||
## Архитектурное замечание
|
||||
|
||||
Сохранение работающей конфигурации не равно отказоустойчивости. Текущая инфраструктура [[recovery-architecture-snapshot]] сильнее, чем была (Hyper Backup проверен, восстановление практикой), но **single-point-of-failure всё ещё есть**: один Windows-PC, одна VM, один публичный IP, один роутер. Глобальная задача [[future-resilient-architecture-goals]] — на потом.
|
||||
121
.wiki/sources/vds-kzntsv-bootstrap-2026-05-20.md
Normal file
121
.wiki/sources/vds-kzntsv-bootstrap-2026-05-20.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: VDS kzntsv bootstrap session 2026-05-20
|
||||
type: source
|
||||
tags: [vds, bootstrap, rusonyx, traefik, portainer, gitea, verdaccio, registry, migration, kreknin]
|
||||
ingested: 2026-05-20
|
||||
raw_path: ../../.tasks/vds-kzntsv-bootstrap.md
|
||||
updated: 2026-05-20
|
||||
---
|
||||
|
||||
# VDS bootstrap session — 2026-05-20
|
||||
|
||||
Одна сессия, ~6 часов: активация Rusonyx VDS → 3-фазная подготовка инфра-стека → миграция трёх сервисов (gitea, verdaccio, registry) с восстановленного backup'а на [`kreknin-synology`](../entities/kreknin-synology.md). Решает первый пункт из [`future-resilient-architecture-goals`](../concepts/future-resilient-architecture-goals.md) — вынос инфра-сервисов с single-point-of-failure хоста. Production CMS остаётся на [`windows-recovery-host`](../entities/windows-recovery-host.md).
|
||||
|
||||
Источник истины — `.tasks/vds-kzntsv-bootstrap.md`. Текущий live-state VDS — [`vds-kzntsv`](../entities/vds-kzntsv.md).
|
||||
|
||||
## Контекст до сессии
|
||||
|
||||
После аварии 2026-05-18 ([`dead-synology-diskstation`](../entities/dead-synology-diskstation.md) развалилась), CMS поднята на [`windows-recovery-host`](../entities/windows-recovery-host.md). Но инфра-сервисы (gitea, verdaccio, docker-registry, owncloud, hermes), которые тоже жили на мёртвой синке, остались только как restored backup на [`kreknin-synology`](../entities/kreknin-synology.md). User не хочет держать их на windows-recovery-host (тот уже перегружен CMS-стеком + это рабочая машина).
|
||||
|
||||
Решение — отдельный облачный VDS у Rusonyx. Заказан 2026-05-19. Активирован 2026-05-20 — IP `89.253.255.94`, тариф 160 NVMe (6 vCPU / 8 GB / 160 GB / Ubuntu 24.04).
|
||||
|
||||
## Хронология
|
||||
|
||||
### Фаза 0 — pre-flight (DNS + кreds)
|
||||
|
||||
User проставил DNS records в REGRU **до** активации сервера: `vds.kzntsv.site`, `*.vds.kzntsv.site`, `git.kzntsv.site`, `verdaccio.kzntsv.site`, `registry.kzntsv.site` → 89.253.255.94. DNS-cut решение: сервисы на kreknin остаются как есть, реальный switch произойдёт после standup'а на VDS (DNS уже там).
|
||||
|
||||
Initial креды от Rusonyx: root + одноразовый пароль. Сохранены в `~/projects/.common/secrets/vds-kzntsv.env`.
|
||||
|
||||
### Rusonyx onboarding pain
|
||||
|
||||
VNC консоль изначально не открывалась. Помогла кнопка «Остановить VNC» в Управление сервером → Консоль (force-disconnect stale attachment). После этого VNC заработал.
|
||||
|
||||
`apt update && apt upgrade && apt --fix-broken install && apt upgrade` пришлось гонять через VNC (Rusonyx сами рекомендуют: их virt бьёт SSH session, openssh-server restart рвёт connection). По дороге — 5+ dpkg interactive prompts: sshd_config conffile (chose 2 = keep local), cloud.cfg (chose N = keep), grub-pc target disk (1 = /dev/vda whole-disk MBR). Reboot после.
|
||||
|
||||
Все эти подробности — в [`rusonyx-vps-onboarding-quirks`](../concepts/rusonyx-vps-onboarding-quirks.md).
|
||||
|
||||
### Phase 1 — bootstrap (через SSH с root + pubkey push)
|
||||
|
||||
1. Pubkey push через **plink** (PuTTY) с inline `-pw` (OpenSSH for Windows не поддерживает password в флаге). После push — ssh-key-only login.
|
||||
2. Sudo user `vitya:Pryakhin10~` + group sudo + NOPASSWD грант (для автоматизации).
|
||||
3. sshd harden через **drop-in `/etc/ssh/sshd_config.d/00-hardening.conf`** (`00-` prefix чтобы выиграть first-match precedence над `50-cloud-init.conf` который форсит `PasswordAuthentication yes`). `PermitRootLogin no` + `PasswordAuthentication no`.
|
||||
4. ufw default-deny + allow 22/80/443 + DB-порты (5432/3306/27017/6379).
|
||||
5. fail2ban (default sshd jail).
|
||||
6. Docker CE 29.5.1 official APT repo + buildx + compose-plugin. vitya в группу docker.
|
||||
7. Hostname `vds-kzntsv`, timezone Europe/Moscow.
|
||||
8. Docker networks `proxy` + `shared-dbs` (external).
|
||||
|
||||
### Phase 1 (cont) — Traefik v2.11 LTS + Portainer 2.21.5
|
||||
|
||||
- Traefik static config: 6 entrypoints (web/websecure/postgres/mariadb/mongo/redis), HTTP-01 LE challenge (DNS уже указан, проходит за 5 sec), dashboard на `traefik.vds.kzntsv.site` + basicAuth middleware из dynamic file provider.
|
||||
- LE issued cert at first request (5 validators с разных AWS regions → 200 OK).
|
||||
|
||||
**Portainer admin init — 4 итерации.** Hard min 12-char policy в Portainer 2.20+ (regression от 2.20.0+), CLI flag `--admin-password` + bcrypt **не bypass'ит** policy и фактически выходит broken (bcrypt сохраняется, но login fails). После проб с `$2y$`/`$2a$` prefix, single-quote escape, YAML list form, docker run direct — оказалось CLI flag тупо не работает в 2.21.5. Финал: голый `docker run`, без `--admin-password`, потом API admin/init с длинным паролем `Pryakhin9-VDS-2026` (18 chars). Деталь — [`portainer-2.21-admin-password-regression`](../concepts/portainer-2.21-admin-password-regression.md).
|
||||
|
||||
API key сгенерирован через `/api/users/<id>/tokens`, local docker endpoint создан через `POST /api/endpoints` form-data. Сохранён в `vds-kzntsv.env`.
|
||||
|
||||
### Phase 2 — Shared DB park с TLS через traefik
|
||||
|
||||
User explicitly: «Я хочу доступ снаружи к базам! Через трафик» — поэтому DBs должны быть accessible from public internet с TLS.
|
||||
|
||||
**Решение архитектуры — это main lesson:**
|
||||
- Initial attempt: traefik TCP routers с `HostSNI('<db>.vds.kzntsv.site')` + `tls.passthrough=true` → **работает для Mongo/Redis** (TLS-from-start), **не работает для Postgres/MariaDB** (STARTTLS-protocols, нет SNI в первых байтах).
|
||||
- Switch на `HostSNI(*)` без `tls.*` (raw TCP forward) — работает для всех 4 DBs, traefik просто маршрутизирует по entrypoint port'у. DBs терминируют TLS сами.
|
||||
|
||||
Подробно в [`traefik-tcp-passthrough-vs-starttls`](../concepts/traefik-tcp-passthrough-vs-starttls.md) и [`db-tls-self-signed-via-traefik-raw-tcp`](../concepts/db-tls-self-signed-via-traefik-raw-tcp.md).
|
||||
|
||||
Self-signed certs у каждой DB (CN matches `<db>.vds.kzntsv.site`), strong random hex32 passwords. Postgres alpine использует uid 70 — пришлось перейти на не-alpine `postgres:16` (uid 999, matches наш chown). Mongo 7 требует `--tlsCAFile` (chain of trust enforcement) — добавили self-signed как CA. Все 4 DBs верифицированы через openssl s_client + real protocol probe (pg_dumpall connect, mongosh ping, redis-cli PING, mariadb SELECT VERSION()).
|
||||
|
||||
### Phase 3.1 — Gitea (миграция от Hyper Backup restored data)
|
||||
|
||||
Источник = restored backup на kreknin, **не запущенный контейнер**. Это была ошибочная гипотеза в начале сессии — потеряли время и обиду user'а («ты почему ни хера не читаешь вики?»). Урок зафиксирован в feedback memory `feedback_read_wiki_first.md`.
|
||||
|
||||
Pipeline:
|
||||
1. tar+ssh stream `/volume1/docker/gitea/{data,postgres,docker-compose.yml}` с kreknin → vds (8m19s, ~2.7G total, ~5.4 Mbps). Sudo на kreknin требовал password (`Pryakhin9`) — passed через `echo Pryakhin9 | sudo -S tar c ...` inside ssh quotes.
|
||||
2. На VDS — `docker run -d --rm postgres:9.6` mounted on restored datadir (postgres uid 999, datadir chown'd, chmod 700). Recovery после non-clean shutdown (last May 9 — последний Hyper Backup snapshot).
|
||||
3. `pg_dump -U gitea gitea` → /tmp/gitea.sql (23 MB, 22400 lines).
|
||||
4. Shared postgres 16: `CREATE ROLE gitea LOGIN PASSWORD 'gitea'; CREATE DATABASE gitea OWNER gitea ...;` через `docker exec postgres psql -U postgres -c "..."` (peer auth on Unix socket).
|
||||
5. Restore `psql -U gitea -d gitea < gitea.sql`. Gitea автоматически мигрирует schema 9.6 → 16.
|
||||
6. Stop temp pg9.6.
|
||||
7. **Восстановленная data была вложена на уровень глубже** (`/data/data/git/...` вместо `/data/git/...`) — flatten layout. Gitea на первом старте перезаписал свежий app.ini (env-var driven), нужно было использовать оригинальный app.ini из вложенного `data/gitea/conf/app.ini` который содержал `INSTALL_LOCK=true`, `LFS_JWT_SECRET=...`, `SECRET_KEY=...` оригинала.
|
||||
8. Patch app.ini под VDS: DOMAIN, SSH_DOMAIN, ROOT_URL → git.kzntsv.site; HOST → postgres:5432; SSH_PORT → 2222.
|
||||
9. Gitea compose без `GITEA__database__*` env vars (пусть app.ini рулит).
|
||||
10. Smoke: HTTP 200, `/api/v1/version` → `{"version":"1.25.5"}`, `/api/v1/repos/search` → 3 first repos (OpeItcLoc03/claude-skills и др.). Final: 132 repos, 4 users.
|
||||
|
||||
### Phase 3.2 — Verdaccio (file rsync)
|
||||
|
||||
1. rsync `storage/` + `config/` + `plugins/` от kreknin → /opt/stacks/verdaccio/ (~9G transferred, 17m36s, ~8.17 MB/s). Sudo на kreknin не нужен — vitya owns эти файлы.
|
||||
2. Chown `10001:65533` (verdaccio uid). **Не делать `chown -R` на parent /opt/stacks/verdaccio** — съест permission на root dir, vitya не сможет писать compose. Только sub-dirs.
|
||||
3. Compose с image `verdaccio/verdaccio:6` + traefik labels → `verdaccio.kzntsv.site`.
|
||||
4. **Crashloop** — Node 22 в verdaccio:6 требует secret **точно** 32 chars в `/verdaccio/storage/.verdaccio-db.json` (не `.verdaccio-db` как старая версия). Restored secret был 64 chars (старая verdaccio накопила). Fix: `openssl rand -hex 16` = 32 chars, overwrite secret в JSON.
|
||||
5. User reported: UI пусто. Причина — kreknin'овский config.yaml имеет `access: $authenticated` для всех packages, и анонимный посетитель не видит ничего. Нужно логиниться (`vitya` user в restored htpasswd). User'у объяснил, он залогинился — packages появились.
|
||||
|
||||
### Phase 3.3 — Registry (GC + fresh install)
|
||||
|
||||
1. Registry GC on kreknin локально (mount restored data, run `registry:2.8.3 garbage-collect`). **Mount должен быть PARENT dir, не sub-dir** — registry ожидает `/var/lib/registry/docker/registry/v2/...`, а не `/var/lib/registry/registry/v2/...`. С правильным mount + `-m` (modify=delete) flag — 99G → **35G** (64G freed).
|
||||
2. Запущен rsync 35G с kreknin → /opt/stacks/registry/. ETA ~50 min при 2 MB/s.
|
||||
3. **Mid-flight user reconsiders**: «может зря тащим старые образы? Могу новых наделать». Decision: kill rsync, fresh install. User accepts loss of old images.
|
||||
4. Fresh registry с htpasswd (vitya/Pryakhin9), `REGISTRY_STORAGE_DELETE_ENABLED=true`, CORS headers для UI.
|
||||
5. Joxit Registry UI (joxit/docker-registry-ui) на `registry-ui.vds.kzntsv.site` с `DELETE_IMAGES=true` для manual cleanup через GUI.
|
||||
|
||||
Подробно про GC: [`registry-gc-mount-and-modify-flag`](../concepts/registry-gc-mount-and-modify-flag.md).
|
||||
|
||||
## Decisions log
|
||||
|
||||
- **Tariff 160 NVMe Rusonyx** (a не 80 SSD + addon) — operational simplicity overweights ₽1000/мес savings (decision 2026-05-19).
|
||||
- **Ubuntu 24.04 LTS** vs Debian 12 — LTS support до 2029, docker official APT primary target Ubuntu (decision 2026-05-19).
|
||||
- **Traefik v2.11 LTS** vs v3 — user explicit choice (совместимость с windows-recovery-host где v2.6.6).
|
||||
- **DB access from outside через traefik** — user explicit reversal от docker-network-only Q1 answer.
|
||||
- **DB TLS = self-signed** для starts, LE-cert sidecar deferred — operational simplicity.
|
||||
- **MongoDB include immediately** — user explicit.
|
||||
- **Hermes defer** — user explicit.
|
||||
- **Registry — fresh install, no migration** — user mid-flight reversal (могу новых наделать).
|
||||
- **`HostSNI(*)` + no tls.* для всех DBs** — uniform config работает для всех protocols (STARTTLS + TLS-from-start).
|
||||
- **Portainer admin pass `Pryakhin9-VDS-2026` (18 chars)** вместо запрошенного `Pryakhin9` (9 chars) — Portainer 2.21+ hard min 12-char policy, `--admin-password` CLI flag broken в 2.20+. Сохранено в `vds-kzntsv.env`.
|
||||
|
||||
## Архитектурное замечание
|
||||
|
||||
VDS kzntsv = первый шаг по closing SPOF gap из [`future-resilient-architecture-goals`](../concepts/future-resilient-architecture-goals.md). Раньше всё крутилось на одной физической коробке ([`dead-synology-diskstation`](../entities/dead-synology-diskstation.md) — упала; [`windows-recovery-host`](../entities/windows-recovery-host.md) — тоже SPOF). Теперь инфра-сервисы (git/npm/registry/dbs) живут на отдельном cloud-host'е. Это **не** полное multi-host resilience: VDS сам по себе тоже single host. Но decouples infra и production CMS, что критично.
|
||||
|
||||
Backup pipeline VDS → kreknin (см. follow-up task [`vds-backup-rsync-kreknin`](../../.tasks/vds-backup-rsync-kreknin.md)) — следующий шаг.
|
||||
Reference in New Issue
Block a user