Files
admin/.wiki/concepts/mssql-on-vds.md
vitya b332af66e4 fix(vds-backup): MSSQL backup INIT->FORMAT + estate backup audit
VDS daily backup пал с 12.06 (line 108, exit 1): mssql-блок (added 11.06,
commit f8ca0794) использовал `WITH ... INIT`, упирался в компрессованный
media-header майских .bak (созданы WITH COMPRESSION на Developer-источнике).
Express не пишет в compression-форматированный media set -> Msg 1844, молчаливый
провал первым же cron-запуском. 5 боевых CMS-баз 3 недели без offsite-копии.

Fix: INIT -> FORMAT (всегда новый media set, иммунно к остаткам). Прогон
verified зелёным: VDS backup OK 95m41s, все 5 .bak на kreknin
(MoreThenCms 910M, StayerCalculator 528M, StayerPrice 39M, TireService 4.5M,
stostayer 990M), speedup 22.62 (--link-dest хардлинкует).

- scripts/vds-backup-rsync-kreknin/run.sh: синхронизирован с задеплоенным
  (mssql-блока в репо не было); FORMAT
- .wiki/concepts/mssql-on-vds.md: gotcha #2 (INIT vs FORMAT) + backup-gap
- .wiki/concepts/backup-inventory-2026-06.md: новая — карта estate × что реально
  бэкапится (с доказательством); триаж дыр
- openwrt UCI backup настроен (cron 03:30 -> kreknin, restricted forced-command
  key) — документация в entity + inventory
- .tasks/kreknin-self-backup.md: backlog #1 SPOF (приёмник сам не бэкапится)
- STATUS.md: incident + audit summary

Урок: бэкап-шаг не готов, пока не предъявлен лог одного реального успеха;
прод-крон не должен быть первым тестом бэкап-пути.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:21:44 +03:00

7.5 KiB
Raw Blame History

title: MSSQL on vds-kzntsv (Express 2022 Linux) type: concept tags: [vds, mssql, migration, ops] related: ../entities/vds-kzntsv, recovery-architecture-snapshot, traefik-tcp-passthrough-vs-starttls, db-tls-self-signed-via-traefik-raw-tcp updated: 2026-06-12

MSSQL on vds-kzntsv

Запущен 2026-05-22 как часть mssql-vds-migration. SPOF с windows-recovery-host снят для CMS DB park.

Architecture

IIS snolla site (web.config)
  → mssql.kzntsv.site:1433 (LE/DNS A → 89.253.255.94)
    → VDS traefik :1433 (TCP entrypoint, HostSNI(*))
      → mssql container :1433 (self-signed TLS terminated по TDS)
        → /var/opt/mssql/data/{5 prod DBs}

Stack: /opt/stacks/databases/mssql/{docker-compose.yml,.env,data,backups}. Networks proxy only.

Image: mcr.microsoft.com/mssql/server:2022-latest (RTM-CU25, build 16.0.4255.1, on Ubuntu 22.04.5 LTS). Edition: Express (MSSQL_PID=Express) — production licensing OK, 10 GB/DB cap (max .mdf сейчас stostayer 968 MB → 10× запас). Memory: MSSQL_MEMORY_LIMIT_MB=2048 env (Express auto-caps на 1410 в любом случае). TLS: self-signed cert, mssql generates automatically. Client connects with TrustServerCertificate=True. Traefik forwards raw TCP — TLS terminates at mssql.

Migration recipe (one-shot, executed 2026-05-22)

Источник: mcr.microsoft.com/mssql/server:2019-latest Developer на windows-recovery-host (volume mssql_mssql_data 26.6 GB; real data ~2.4 GB; .ldf logs 12 GB inflated).

-- 1. Source: BACKUP DATABASE for each prod DB (5 total)
BACKUP DATABASE [<db>] TO DISK='/var/opt/mssql/backups/<db>.bak'
  WITH COMPRESSION, COPY_ONLY, INIT, STATS=50;
-- Compressed total: 486 MB (10× от .mdf size — built-in compression на UTF-16/varchar pads)
# 2. Extract from container → scp → VDS bind
docker cp mssql:/var/opt/mssql/backups/. C:/tmp/mssql-backups/
scp C:/tmp/mssql-backups/*.bak vitya@vds.kzntsv.site:/tmp/mssql-bak-staging/
ssh vitya@vds.kzntsv.site 'sudo mv /tmp/mssql-bak-staging/*.bak /opt/stacks/databases/mssql/backups/
                           sudo chown 10001:0 /opt/stacks/databases/mssql/backups/*.bak'
# 3. compose + .env (см. /opt/stacks/databases/mssql/)
# MSSQL_SA_PASSWORD из pass show mssql-vds/sa-password (32-char alnum, no special chars)
# 4. Traefik: добавить TCP entrypoint в /opt/stacks/traefik/data/traefik.yml
entryPoints:
  mssql:
    address: ":1433"
# + port 1433 в /opt/stacks/traefik/docker-compose.yml ports list
# + ufw allow 1433/tcp
# + sudo docker compose up -d (RECREATE, не restart — port mapping)
-- 5. RESTORE per DB
RESTORE DATABASE [<db>] FROM DISK='/var/opt/mssql/backups/<db>.bak' WITH REPLACE, STATS=50;
-- 2019 → 2022 schema upgrade автоматический (versions 953→957). DBCC CHECKDB PHYSICAL_ONLY clean.
-- 6. CRITICAL: server-level logins НЕ переезжают в .bak (sysadmin/master DB only).
--    Создать manually + fix orphan users в каждой DB:
CREATE LOGIN snolla WITH PASSWORD = '<from-source-web.config>', CHECK_POLICY = OFF;
USE MoreThenCms;
ALTER USER snolla WITH LOGIN = snolla;
-- Repeat for каждой DB где web.config упоминает данного user'а.

Gotchas

  1. Logins vs users: .bak восстанавливает DB-level users (с их role memberships + permissions), но НЕ server logins. Sites с User Id=X в conn-string ловят Login failed for user 'X' после restore, пока не выполнить CREATE LOGIN + ALTER USER ... WITH LOGIN.
  2. Traefik recreate, не restart: добавление port mapping в docker-compose.yml требует recreate контейнера (docker compose up -d), не restart. restart оставит старые published ports.
  3. PowerShell + pass (bash script): pass show в PowerShell возвращает NULL (PS не запускает bash-скрипты напрямую). Нужно bash -c "pass show ..." или literal. Симптом: Login failed for user 'sa' несмотря на правильный пароль на VDS.
  4. MSSQL_SA_PASSWORD спец-символы: rotation password generator должен excludeавать +/= (Base64) и shell-meta ($, `, ", ', \). Иначе env-var passing через ssh heredoc или sed конструкторы ломаются. Пример безопасного: [Convert]::ToBase64String($bytes) -replace '[+/=]','x'.
  5. MSSQL Linux user uid: mssql user = uid 10001. Bind-mount должен быть chown 10001:0 чтобы контейнер read/write. Parent dir owns vitya:vitya чтобы можно было редактировать compose без sudo.

Backup integration

Реализовано 2026-06-11 в /opt/stacks/backup/scripts/run.sh (см. vds-backup-rsync-kreknin). Блок между Step 1 (DB dumps) и Step 2 (rsync):

source /opt/stacks/databases/mssql/.env   # injects MSSQL_SA_PASSWORD
for DB in MoreThenCms StayerCalculator StayerPrice TireService stostayer; do
  docker exec mssql /opt/mssql-tools18/bin/sqlcmd \
    -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -b \
    -Q "BACKUP DATABASE [$DB] TO DISK='/var/opt/mssql/backups/${DB}.bak' WITH COPY_ONLY, FORMAT"
  mv /opt/stacks/databases/mssql/backups/${DB}.bak "$DUMP_DIR/mssql-${DB}.bak"
done
unset MSSQL_SA_PASSWORD

Gotcha #1 — Express не умеет COMPRESSION: Express Edition не поддерживает WITH COMPRESSION. .bak файлы создаются в bind-mount /var/opt/mssql/backups/ (uid 10001:10001); root mv их в $DUMP_DIR для rsync на kreknin. sqlcmd path: /opt/mssql-tools18/bin/sqlcmd.

Gotcha #2 — FORMAT, не INIT (incident 2026-06-12): первая версия блока (added 2026-06-11) использовала WITH COPY_ONLY, INIT и молча падала первым же cron-запуском. Причина: в /var/opt/mssql/backups/ лежали миграционные .bak от 22.05, созданные WITH COMPRESSION на Developer-источнике. INIT переиспользует существующий media-set header (компрессованный) → Express не может в него писать → Msg 1844 "BACKUP ... WITH COMPRESSION is not supported on Express" (хотя в команде COMPRESSION нет!). NO_COMPRESSION тоже не спасает → Msg 3098 "media formatted with an incompatible structure". Fix: WITH COPY_ONLY, FORMAT — всегда создаёт новый media set, иммунно к остаткам. Урок: при бэкапе на Express в каталог с возможными чужими/старыми .bak — только FORMAT, не INIT.

Backup gap: между 22.05 (миграция) и 12.06 у 5 боевых баз не было ни одной offsite-копии — raw datadir намеренно не rsync'ится (lock), а dump-ветка падала с первого дня. Закрыто 2026-06-12.

Rollback recipe

  1. Revert IIS web.config'ы (см. .bak-pre-vds-cutover-20260522 файлы).
  2. iisreset /restart (или ждать auto-recycle ~30 sec).
  3. Убедиться windows-host MSSQL контейнер всё ещё running (acceptance 48ч uptime до decommission).
  4. Если уже decommissioned: docker start mssql (volume сохранён ещё неделю по acceptance).