# 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 ``)? Если да — путь должен остаться, либо обновить.
- [ ] Authentication / IIS Application Identity — какая учётка должна крутить app pool? (LocalSystem? NetworkService? IIS AppPool\?)
- [ ] Нужен ли .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\` для всех 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).