import: .wiki/sources/ from MoreThenCms via subtree-split

git-subtree-dir: .wiki/sources
git-subtree-mainline: d25c0577c8
git-subtree-split: a5e96432bc
This commit is contained in:
2026-05-21 13:46:39 +03:00
4 changed files with 474 additions and 0 deletions

0
.wiki/sources/.gitkeep Normal file
View File

View 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: `&``&amp;` (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 — приватны.
ртвые: 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).

View 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]] — на потом.

View 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)) — следующий шаг.