agentleFS
Sign inSign up

migrate-server-to-server

vefmvai/sysadmin/.claude/skills/migrate-server-to-server/SKILL.md

Перенос Docker-инфраструктуры со старого VPS на новый. 4 стратегии по downtime/cost: live (logical replication, 30-120 сек), backup-restore (10-60 мин), rsync-incremental (5-15 мин), blue-green (0 downtime, 2x cost). Pre-checklist + cutover (DNS, зеркало proxy_pass на старом — ноль split-brain) + verify (row counts, healthchecks, рабочее место оператора). Rollback явный. Триггеры: «переезд на новый сервер», «migrate server», «сменить VPS», «переместить инфру», «новый провайдер», «сменить хостинг». НЕ для bootstrap (это bootstrap-new-server); НЕ для приведения хаоса в порядок (cleanup-existing-server).

Skill1 starsChanged 43 days ago
  • Reads credentials
---
name: migrate-server-to-server
description: |
  Перенос Docker-инфраструктуры со старого VPS на новый. 4 стратегии по downtime/cost:
  live (logical replication, 30-120 сек), backup-restore (10-60 мин), rsync-incremental
  (5-15 мин), blue-green (0 downtime, 2x cost). Pre-checklist + cutover (DNS, зеркало
  proxy_pass на старом — ноль split-brain) + verify (row counts, healthchecks, рабочее
  место оператора). Rollback явный.
  Триггеры: «переезд на новый сервер», «migrate server», «сменить VPS», «переместить инфру»,
  «новый провайдер», «сменить хостинг».
  НЕ для bootstrap (это bootstrap-new-server); НЕ для приведения хаоса в порядок (cleanup-existing-server).
allowed-tools: Bash, Read, Edit, Write
disable-model-invocation: true   # меняет боевую систему — только по явной команде оператора (ADR-0027)
---

<role>
Я провожу миграцию Docker-инфраструктуры со старого VPS на новый, выбирая стратегию
по контексту (терпимость к downtime vs стоимость). Я не делаю миграцию «вслепую» —
pre-migration checklist обязателен, rollback готов в любой момент. Старый сервер
не уничтожается до тех пор, пока новый не отработает safe-период (1-2 недели).
</role>

<context>
Старый и новый VPS оба доступны по SSH одновременно. Новый VPS уже bootstraped
через `bootstrap-new-server` (Docker, UFW, fail2ban, базовое hardening). Свежий
бэкап существует и проверен. Inventory старого сервера актуален (запустить
`inventory-scan`, если давно не обновлялся).
</context>

<goals>
- Все сервисы работают на новом сервере, отвечают healthcheck'ами.
- Нет потерь данных — row counts на старом и новом совпадают.
- DNS переключён, TLS работает на новых endpoint'ах.
- Старый сервер можно отключить через safe-период (1-2 недели) без последствий.
- Откат возможен в любой момент: DNS возвращается на старый IP, который ещё жив.
</goals>

<parameters>
- `OLD_SERVER` — SSH-target старого VPS (например, `user@old.vps.com`).
- `NEW_SERVER` — SSH-target нового VPS.
- `STRATEGY` — `live` / `backup-restore` / `rsync-incremental` / `blue-green`.
- `SERVICES` — список сервисов или `all`.
- `DOWNTIME_TOLERANCE_SEC` — терпимый downtime в секундах (для авто-выбора стратегии).
- `DNS_PROVIDER` — провайдер DNS (для понижения TTL заранее).
</parameters>

# Инструкции

## ⚠️ Зоны риска этого скилла

Большинство шагов миграции — Yellow Zone (брифинг + «ок»). Но две категории операций —
**Red Zone** (4-шаговая процедура ASSESS → PROPOSE → EXECUTE → VERIFY с type-to-confirm,
канон — в персоне и `.claude/agents/references/trust-zones.md`):

1. **`rsync --delete` на целевой сервер** — стирает на приёмнике всё, чего нет на
   источнике. Перепутанные местами хосты = необратимое уничтожение боевого сервера
   одной командой.
2. **Распаковка архива поверх корня** (`tar xzf ... -C /`) — перезаписывает живые
   файлы целевой системы без возможности отката.

Для этих операций «ок» оператора недостаточно — обязателен type-to-confirm после
брифинга, который явно называет: **что будет безвозвратно перезаписано/удалено на
целевом сервере и есть ли свежая копия этих данных**.

## Шаг 1. Pre-migration checklist (БЛОКЕР запуска)

Без этих девяти пунктов миграция не начинается. Это не «лучшая практика», а условие безопасности.

- [ ] **Сверка направления «откуда → куда»** — показать оператору ОБА хоста рядом
      с живыми доказательствами их ролей (`ssh <host> 'hostname && uptime && docker ps
      --format "{{.Names}}" | head -20'` для каждого). Оператор подтверждает явно:
      «источник = X (боевой), целевой = Y (новый/пустой)». Если на целевом обнаружены
      работающие контейнеры или непустые volumes — STOP: целевой не пустой, продолжение
      только после явного письменного одобрения оператора «целевой одобрен к перезаписи».
- [ ] Inventory старого сервера актуален — запусти `inventory-scan` если последний
      снимок старше 7 дней.
- [ ] Размеры volumes известны: `du -sh /var/lib/docker/volumes/*` — нужно для
      оценки времени rsync и свободного места на новом сервере.
- [ ] Зависимости каждого сервиса задокументированы — что нельзя переносить по
      одному (app+postgres, nginx+certificates, redis+app-with-sessions).
- [ ] Свежий бэкап ИСХОДНОГО сервера: pg_dumpall + tar volumes + restic backup —
      offsite. Без бэкапа миграция запрещена даже на «маленьких» сервисах.
- [ ] **Свежий бэкап ЦЕЛЕВОГО сервера** — перед первой записью на целевой
      (`bash scripts/01-pre-migration-backup.sh user@new-server`, тег `pre-migration-target`).
      Целевой обычно «пустой после bootstrap», но именно это предположение и убивает
      данные при перепутанном направлении или недопонятой роли хоста. Если на целевом
      реально нечего бэкапить (нет volumes, нет БД) — зафиксировать это вслух
      результатом команды, а не предположением.
- [ ] Новый сервер прошёл `bootstrap-new-server` — Docker, UFW, fail2ban, SSH
      hardening готовы. Без этого новый сервер уязвим в окне миграции.
- [ ] **Конфиги на исходном сервере совпадают с git (нет drift'а).** Для каждого
      конфига, у которого есть источник истины в git (nginx-vhost'ы, systemd-юниты,
      compose, backup-скрипты), сверь боевую версию с репозиторием
      (`diff <(ssh src 'cat /etc/nginx/sites-available/X') <(git show HEAD:services/nginx/sites-available/X)`
      или по `sha256`). Если сервер ушёл вперёд git (правки вносили руками мимо
      push-to-pull) — **сначала синхронизируй git с боевой версией** (реальное
      состояние сервера — источник истины при расхождении), потом переноси. Иначе на
      новый сервер уедет отставшая версия из git, и часть боевой конфигурации молча
      потеряется. Боевой кейс 2026-07-09: nginx-правки (`/comics/`, `/sw.js`,
      HSTS в location-блоках) жили на сервере, но не были в git-репо инфры — при
      клонировании на новый сервер потерялись бы, если бы не сверка.

- [ ] **Файловая статика nginx учтена в плане переноса.** Compose/volumes покрывают не всё:
      nginx часто раздаёт каталоги прямо с диска (`root`/`alias`-локации — комиксы, лендинги,
      webroot ACME, каталоги отдачи файлов). Найди их все:
      `ssh src 'grep -rE "^\s*(root|alias)" /etc/nginx/sites-enabled/'` — и сверь каждый
      каталог с планом переноса. Такие «невидимки» не видны ни в `docker inspect`, ни в
      volumes, и теряются молча: после cutover локация отдаёт 404, а замечают через недели.
      Боевой кейс 2026-07-09: статичный комикс-ридер `/comics/` (23 МБ на диске) не
      попал в перенос сервисов — дыру нашли только при подготовке зеркала в окне cutover.

Если хотя бы один пункт не закрыт — STOP, оператору сообщается какой именно.

## Шаг 2. Выбор стратегии (decision tree)

```
ЕСЛИ DOWNTIME_TOLERANCE_SEC < 60 И бюджет позволяет 2x временно:
    → blue-green
ИНАЧЕ ЕСЛИ DOWNTIME_TOLERANCE_SEC < 300 И в стеке только PostgreSQL/Redis:
    → live (логическая репликация PG + RDB/AOF Redis)
ИНАЧЕ ЕСЛИ объём данных < 50GB И есть несколько дней на параллельную работу:
    → rsync-incremental (день 1+2 онлайн, день 3 cutover 5-15 мин)
ИНАЧЕ:
    → backup-restore (простой 10-60 мин, минимум setup)
```

После выбора — Yellow Zone брифинг 6 пунктов оператору:
1. **Что меняется:** какие сервисы переносятся, на какой адрес.
2. **Что может пойти не так:** реальные риски конкретно этой стратегии.
3. **Окно простоя:** ожидаемый downtime в секундах/минутах.
4. **Точка невозврата:** до DNS-switch'а откат бесплатный, после — TTL × 2.
5. **Откат:** конкретные команды, которые вернут на старый сервер.
6. **Подтверждение:** оператор пишет согласие явной фразой.

Этот брифинг одобряет миграцию В ЦЕЛОМ. Но если выбранная стратегия содержит
`rsync --delete` или распаковку архива поверх `/` (стратегии rsync-incremental и
backup-restore) — каждая такая операция дополнительно проходит **Red Zone**
непосредственно перед запуском:

1. **ASSESS** — перечитать направление: источник и приёмник в команде, доказательство
   роли каждого (вывод `hostname` с обоих). Что именно на приёмнике будет удалено
   `--delete` / перезаписано распаковкой?
2. **PROPOSE** — брифинг с обязательной строкой: «На целевом `<host>` будет
   безвозвратно удалено/перезаписано: <конкретно что>. Копия этих данных: <есть,
   путь/тег | нечего копировать — доказано выводом такой-то команды>». Затем
   type-to-confirm: `подтверждаю rsync --delete на <целевой-host>, беру риск на себя,
   бэкап целевого проверен` (аналогично для tar).
3. **EXECUTE** — ровно та команда, что в брифинге.
4. **VERIFY** — сверка результата (row counts / списки файлов), запись о факте
   Red Zone-операции в журнал миграции (runbook).

## Шаг 3. Стратегия-специфичная процедура

Пошаговые команды всех четырёх стратегий — в `references/strategies-tradeoffs.md`
(применимость, плюсы, минусы, setup, cutover, rollback по каждой). Здесь не дублирую:
открываю справочник на нужной секции и иду по ней.

| Стратегия | Когда берём | Что критично помнить здесь |
|---|---|---|
| **backup-restore** | БД < 100 ГБ, простой 10–30 мин приемлем, минимум настройки | распаковка архива поверх `/` — 🔴 Red Zone (см. «Зоны риска») |
| **rsync-incremental** | средний объём, есть несколько дней на параллельную работу | КАЖДЫЙ прогон с `--delete` — 🔴 Red Zone, отдельный type-to-confirm |
| **live** (логическая репликация PG) | простой критичен, стек только PostgreSQL или PG + Redis | дожидаться `state='streaming'` и нулевого lag ДО переключения DNS |
| **blue-green** | нулевой простой критичен, бюджет тянет двойную стоимость | старый (blue) держим 1–2 недели, откат = один `nginx -s reload` |

Дамп PostgreSQL берётся **изнутри контейнера** (`docker exec postgres pg_dumpall`), а не
клиентом с хоста: версии клиента и сервера обязаны совпадать.

## Шаг 4. Cutover (`scripts/03-cutover.sh`)

Финальное переключение — порядок критичен.

1. **Снизить TTL заранее** (за 24-48 ч до cutover): A-record TTL → 300 сек.
   ⚠️ У части панелей (nic.ru DNS Master и подобных) правки зоны вступают в силу только
   после отдельной кнопки «Опубликовать» — сверяй факт по авторитативным NS
   (`dig <domain> @<ns-провайдера>`), а не по панели.
2. **Финальный rsync delta / финальный дамп БД → restore** (по стратегии).
3. **Stop сервисов на старом** — но НЕ уничтожать контейнеры (`docker compose stop`,
   не `down`).
4. **Start сервисов на новом** — `docker compose up -d`.
5. **Smoke-test на новом** перед DNS-switch — health-check для каждого сервиса.
6. **Зеркало на старом nginx (канон для сервисов с записью в БД).** Сразу после smoke-test
   заменить vhost'ы переносимых доменов на старом сервере прозрачным прокси на новый IP:
   ```nginx
   location / {
       proxy_pass https://<new-IP>;
       proxy_ssl_server_name on;
       proxy_ssl_name $host;
       proxy_set_header Host $host;
       # + стандартные X-Real-IP / X-Forwarded-For / X-Forwarded-Proto
   }
   ```
   (оригиналы конфигов — в бэкап-каталог ВНЕ `sites-enabled`, иначе nginx подхватит оба и
   получит конфликт server_name). Зачем: DNS переключается волной — часть клиентов часами
   ходит на старый IP; без зеркала обе площадки живут параллельно и пишут каждая в свою БД
   (split-brain, слить расходящиеся базы почти невозможно). С зеркалом база одна с первой
   секунды, downtime = только время дампа+restore (минуты), схема нечувствительна к TTL и
   скорости propagation. Боевой кейс 2026-07-09: простой 8,5 мин вместо часа ожидания
   кэшей. С этого пункта downtime закончен — сайт работает у всех.
7. **DNS switch** — обновить A-record на новый IP (атомарно одним кликом в панели
   или API провайдера; см. п.1 про кнопку «Опубликовать»).
8. **Мониторить propagation** 5-10 мин: `for i in {1..20}; do dig <domain> +short; sleep 30; done`.
   С зеркалом (п.6) этот шаг — наблюдение, не ожидание: клиенты работают в обе фазы.

## Шаг 5. Post-migration verify (`scripts/04-post-migration-verify.sh`)

Проверки сразу после cutover, до отключения старого сервера.

```bash
# Row counts — главная метрика отсутствия потерь
ssh "$OLD_SERVER" 'docker exec postgres psql -U postgres -d <db> \
  -c "SELECT count(*) FROM <main-table>"'
ssh "$NEW_SERVER" 'docker exec postgres psql -U postgres -d <db> \
  -c "SELECT count(*) FROM <main-table>"'
# → должны совпадать

# Healthchecks (подставить реальные сервисы из inventory)
for svc in <service-1> <service-2> <service-3>; do  # ПРИМЕР, замените своими
    curl -sSf https://"$svc".example.com/health || echo "FAIL: $svc"
done

# TLS работает
echo | openssl s_client -connect <domain>:443 -servername <domain> 2>/dev/null \
  | openssl x509 -noout -dates

# Бэкапы работают на новом
ssh "$NEW_SERVER" 'restic snapshots --json | tail -5'

# Cron entries активны на новом
ssh "$NEW_SERVER" 'crontab -l && ls /etc/cron.d/'
```

### Сверка «восстановленные БД ↔ решение по проектам» (обязательная секция verify)

Решение «что переезжает / что в архив» применяй **на уровне БД, а не только сервисов**:
shared-db переносит всё скопом, если явно не отфильтровать. Боевой кейс Bronto
2026-07-10: БД `newsbot` уехала в production «за компанию» с общим дампом, попала под
удаление как бесхозная — а оказалась базой нужного проекта (восстановлена без потерь
только благодаря правилу «архив до DROP»).

```bash
# Список БД на новом сервере
ssh "$NEW_SERVER" 'docker exec postgres psql -U postgres -Atc \
  "SELECT datname FROM pg_database WHERE NOT datistemplate"'
```

- [ ] Каждая восстановленная БД сопоставлена проекту из финального решения
      («переезжает» / «в архив» / «удалить») — лишних нет, недостающих нет
- [ ] У каждой БД на новом сервере есть живой потребитель (контейнер или хост-сервис
      из `host-services.txt` снимка) — БД без потребителя = вопрос оператору ДО verify-close
- [ ] Роли-владельцы восстановленных БД существуют и не осиротели

### Рабочее место оператора (обязательная секция verify)

Сервер переехал — но машина оператора продолжает смотреть на старый IP. Пройди по всей
клиентской обвязке (боевой кейс 2026-07-09: ТРИ ssh-туннеля Мака + MCP-сервер +
деплой-скрипты локальной папки сайта указывали на выключаемый сервер):

- [ ] **SSH-туннели**: `lsof -nP -iTCP -sTCP:LISTEN | grep ssh` + `ps aux | grep -E "autossh|ssh.*-L"`
      на машине оператора — куда ведёт каждый форвард? launchd-плисты
      (`~/Library/LaunchAgents/*.plist`), systemd-user-юниты, скрипты с `ssh -L`.
- [ ] **MCP-серверы и агентские конфиги**: строки подключения к БД/API в конфигах
      Claude Code / IDE (`DATABASE_URL`, хосты туннелей в start-скриптах MCP).
- [ ] **Деплой-контур локальных папок проектов**: deploy-скрипты (`SERVER=`, `REMOTE_DIR=`),
      sync-скрипты, CLAUDE.md проектов с описанием «как деплоить», docker-compose с
      останками старых имён.
- [ ] **`~/.ssh/config`**: алиасы, ProxyJump-цепочки через старый сервер.

⚠️ Грабля туннелей при переключении: если у нового хоста в `~/.ssh/config` включён
`ControlMaster auto`, скриптовые туннели (autossh/launchd) молча вешают форвард на
интерактивный мультиплексор-мастер и умирают вместе с ним (`ControlPersist` истёк — туннели
осыпались, autossh уже мёртв). Всем постоянным туннелям — собственное соединение:
`-o ControlMaster=no -o ControlPath=none`.

## Шаг 6. Rollback (если что-то пошло не так)

Откат построен на том, что **старый сервер ещё жив** — мы его только остановили,
не уничтожили.

```bash
# 1. DNS switch обратно на старый IP (один клик в панели DNS-провайдера)
# 2. На старом — поднять сервисы, которые мы stop'нули в Шаге 4
ssh "$OLD_SERVER" 'cd /opt/<service> && docker compose up -d'
# 3. Подождать TTL × 2 (10 мин) для propagation
# 4. Проверить что старый снова отвечает
curl -sSf https://<domain>/health
# 5. Анализ причин на новом сервере (логи, метрики)
# 6. Повторить миграцию с исправлениями
```

## Шаг 7. Cleanup старого сервера (через safe-период)

Не торопиться — индустриальный стандарт 1-2 недели.

- Дни 0-2: активный мониторинг, логи смотрят постоянно.
- Дни 3-7: пассивно следим, smoke-test раз в день.
- Неделя 2: финальный snapshot старого сервера в архив.
- После 14 дней: можно отключить у провайдера, репо `compose/конфиги` остаётся как
  холодный архив.

## Что помню до чтения справки

Четыре правила обязаны срабатывать сразу — открывать справочник, чтобы их вспомнить, поздно:

- **Направление «откуда → куда» подтверждается живыми доказательствами**, а не памятью.
  Перепутанные хосты в `rsync --delete` уничтожают боевой сервер одной командой.
- **Старый сервер не уничтожается** сразу после cutover — минимум сутки, канон 1–2 недели.
  Это единственный откат, который работает.
- **TTL снижается за 24–48 часов** до переключения, иначе часть клиентов сутками ходит
  на старый IP.
- **Машина оператора — часть миграции.** SSH-туннели, MCP-серверы и деплой-скрипты после
  cutover молча смотрят на старый сервер; «зелёный» статус там проверяет мёртвую копию.

# Bundled Resources

| Файл | Что это и когда открывать |
|---|---|
| `references/strategies-tradeoffs.md` | **Как выполнять выбранную стратегию**: сводная таблица (downtime / cost / complexity / риск потери данных) и по каждой из четырёх — применимость, плюсы, минусы, setup, cutover, rollback с командами. Открывать на Шаге 3 |
| `references/pitfalls-and-examples.md` | **Грабли, граничные случаи и два разобранных примера** (миграция трёх ботов через rsync-incremental; production-стек без простоя через blue-green). Открывать при планировании и когда шаг пошёл не так |
| `scripts/01-pre-migration-backup.sh` | обязательный бэкап перед миграцией — и источника, и цели (Шаг 1) |
| `scripts/02-rsync-incremental.sh` | параметризованный rsync с поддержкой `--link-dest` |
| `scripts/03-cutover.sh` | stop старого + start нового + переключение DNS (Шаг 4) |
| `scripts/04-post-migration-verify.sh` | row counts + healthcheck + проверка TLS (Шаг 5) |
| `templates/migration-runbook.md` | шаблон runbook'а под конкретную миграцию |

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.