# SSH deployment — Phase 7 `./mcpack deploy` применяет готовый полный immutable release через SSH/rsync. Если artifact ещё не создан, команда вызывает clean `build --release`; если уже есть, проверяет его checksums и provenance. Development artifact с `git.dirty=true` отклоняется, даже если текущий worktree уже чистый. ## Подготовка цели На host нужны Linux x86_64, Node.js 22+, доступный Docker daemon, rsync, GNU tar и zstd. Pinned Compose и небольшой Node agent с закреплённым YAML parser передаются из локального bootstrap/dependencies; системный Docker не устанавливается. У оператора должны быть права на весь runtime и его владельцев; выбранные UID/GID согласовать с владельцем каталога. В `platform/config.yaml` выбрать SSH host/alias и **отдельный root сборки**: ```yaml server: host: crab@192.168.1.61 directory: /home/crab/workspace/minecraft-lab/minecraft-lab-neoforge-spike memory: 2G port: 25565 ``` Host `.env` готовится отдельно, вне releases, с EULA/UID/GID/memory/port и стабильным MC_PROJECT_NAME. Runtime startup читает именно этот файл; build defaults не перезаписывают host settings. Первое применение требует пустого runtime без старого binding; существующий неопознанный мир не принимается автоматически. У разных сборок разные roots, projects и ports. SSH host key должен быть известен заранее. Используются BatchMode, StrictHostKeyChecking=yes и keepalive. Ключи/порты задать в SSH config; password-only сессия может использовать заранее аутентифицированный ControlMaster. Пароль не записывается в config, env, artifact или agent. ```bash ./mcpack deploy --artifact /absolute/full-release # Порт/ControlMaster/known_hosts в отдельном SSH config: ./mcpack deploy --artifact /absolute/full-release --ssh-config /absolute/ssh-config # Node из nvm, которого нет в non-interactive SSH PATH: ./mcpack deploy --artifact /absolute/full-release \ --ssh-config /absolute/ssh-config \ --remote-node /home/crab/.nvm/versions/node/v26.10.0/bin/node ``` `--config FILE` выбирает другой platform config. Timeout берётся из его `deploy.healthcheck_timeout`; он должен быть больше 10 секунд stability. Первый image/dependency download может требовать 600+ секунд. IPv6 host и другие сложные адреса сейчас задавать через SSH alias. ## Выполнение и state Под SSH удерживается одна `.mcpack-runtime.lock` на всё получение релиза, backup и применение. Перед передачей проверяются незавершённый deployment, source/binding, target version collision, host settings/права и свободное место. Релиз передаётся rsync в `releases/.incoming-*`, проверяется на host и публикуется atomic rename. Сбой передачи не останавливает исходный сервер. Agent в `.mcpack/agent-UUID/` также проверяется по полной checksum coverage. Перед применением проверяется каждый replace-managed файл прошлого release. При изменении/missing file или занятом новом managed path возвращается конфликт. Для небольших server-owned text configs выводится content diff, для остальных файлов — expected/actual hash. Неуправляемые файлы не удаляются. Нужно перенести полезные правки в source и построить новую версию; флага force для молчаливого перетирания конфигов нет. Если тот же release уже активен, выполняется `health` без restart; успешный повторный deploy возвращает noop. Одинаковая версия с другим manifest/checksums отклоняется до остановки. Если активный release unhealthy, повторный deploy не перезапускает его автоматически. При смене релиза исходный сервер останавливается, подтверждается остановка, создаётся verified snapshot всего runtime и исходного release. Только после этого apply callback меняет binding, удаляет отсутствующие в новом релизе известные replace-managed файлы и запускает target. Конфликты проверяются повторно непосредственно перед apply. Image применяет pack/server overrides при запуске; runtime-generated/unmanaged данные сохраняются. После стабильного mc-health/status query обновляются previous-release и active-release, каждый через atomic rename. Deployment state записывается перед каждым этапом и связывает source/target identities, прежние pointers, backup path и факт apply. Пара указателей не является единым filesystem transaction: interrupted `committing` требует просмотра state перед recovery. ## Ошибка и ручное восстановление Любой failed/incomplete `deployment-state.json` блокирует следующую операцию. При прерывании SSH во время upload worker сохраняет failed state и освобождает lock; при принудительном убийстве worker/power loss lock может остаться. Не удалять stale lock, пока соответствующий host process действительно жив. После ошибки **до apply** source/runtime binding не переключаются. При ошибке backup сервер может остаться остановленным; проверить logs и backup operation. После подтверждения неизменности мира можно запустить исходный release. Failed state нужно сохранить для аудита и явно разрешить только после recovery. Если `applied=false`, binding совпадает с active manifest, причина исправлена и исходный сервер проверен health, сохранить failed record под отдельным именем в server root, затем повторить deploy. Для ручного config edit новый clean release должен содержать ровно текущие bytes этого managed файла — такой осознанный перенос разрешается. Любое другое содержимое остаётся конфликтом. Пример архивирования **только после перечисленных проверок на host**: ```bash mv "$MC_ROOT/deployment-state.json" \ "$MC_ROOT/deployment-state-resolved-$(date -u +%Y%m%dT%H%M%SZ).json" ``` Не применять этот способ к `applied=true`, неизвестному binding или interrupted committing: сначала восстановить/завершить реальное состояние. После ошибки **apply/startup** target останавливается, source pointers не продвигаются, binding остаётся на фактически применённом target и snapshot сохраняется. Старый pack автоматически не запускается на изменённом мире. Восстановить snapshot в новый root вместе с сохранённым source release по [backups.md](backups.md), подготовить host settings, удалить старый контейнер, запустить восстановленную копию и проверить health. Не менять binding вручную, чтобы обойти защиту существующего мира. Для первого неудачного deploy без snapshot использовать state/release/diagnostics: пустой мир можно повторно инициализировать отдельно либо явно завершить запуск того же target. При ошибке `committing` сначала проверить actual binding, Docker health и оба pointer files. Snapshot recovery реализован в [Phase 8](rollback.md). Простое удаление failed state не является recovery. ## Текущий проверенный host На `crab@192.168.1.61:2228` подготовлен root `/home/crab/workspace/minecraft-lab/minecraft-lab-neoforge-spike`. Release 0.0.5 healthy (обновление публикации порта после Phase 7), UID/GID 1000:1000, memory 2G, MC_PROJECT_NAME=minecraft-lab-neoforge-spike. Offline mode включён. Clean release собран в отдельном checkout и опубликован локально: ```bash ./mcpack deploy \ --artifact dist/minecraft-lab-neoforge-spike-0.0.5 \ --ssh-config .tools/phase7-ssh-target/config \ --remote-node /home/crab/.nvm/versions/node/v26.10.0/bin/node ``` SSH config хранит только соединение/known_hosts/ControlPath. После окончания ControlMaster для password login сначала заново аутентифицировать сессию: ```bash ssh -F .tools/phase7-ssh-target/config minecraft-lab-target true ``` Host settings теперь содержат `MC_BIND_ADDRESS=0.0.0.0`; порт 25565 публикуется на всех IPv4-интерфейсах host. В Prism подключаться напрямую к **192.168.1.61:25565**, без SSH tunnel. Локальный Phase 0 сервер остаётся отдельным на 127.0.0.1:25565. Default для новых host остаётся 127.0.0.1; LAN bind выбирается явно в host .env. Проверка — [network-results.md](network-results.md). Сохранённый remote agent содержит `scripts/server.sh`. Для lifecycle на host выбрать этот agent, задать абсолютные MC_ROOT/MC_RELEASE_DIR, указать nvm Node в PATH и выполнить wrapper: ```bash export PATH=/home/crab/.nvm/versions/node/v26.10.0/bin:$PATH export MC_ROOT=/home/crab/workspace/minecraft-lab/minecraft-lab-neoforge-spike export MC_RELEASE_DIR="$MC_ROOT/releases/$(cat "$MC_ROOT/active-release")" # Подставить один из проверенных каталогов .mcpack/agent-*: /absolute/agent/scripts/server.sh health /absolute/agent/scripts/server.sh logs --tail 100 ``` При failed deployment брать фактически применяемый release из state/binding, а не автоматически active-release. Agents/releases/backups автоматически не удаляются. Свидетельства и границы проверок — [phase7-results.md](phase7-results.md).