# Контейнерная поставка Поставка для одного Docker host, одного backend и краткого простоя. [Топология](containers.md#контейнерная-поставка) и [ADR 015](../adr/015-single-server-sqlite-deployment.md) объясняют границы. Production-комплект находится в отдельном [репозитории deploy](https://git.personal.ksv741.ru/WealthLedger/deploy). Его README — единственная пошаговая инструкция установки, обновления и recovery; [ADR 016](../adr/016-versioned-deployment-bundle.md) объясняет выбор релиза комплекта. Development-команды ниже выполняются из workspace с checkout `frontend/` и `backend/`. ## Предпосылки - Docker Engine с Compose plugin **не ниже 2.24.4** (`!override` в development), доступ к registry и достаточно свободного диска для non-root UID 1000. Production images `0.2.3` опубликованы для Linux amd64. Другую архитектуру проверяют отдельно; multi-platform images не обещаны. - Nexus Docker hosted repository `docker-hosted`, доступный через `nexus.personal.ksv741.ru/docker-hosted/`. Docker Bearer Token Realm включён; reverse proxy передаёт исходные host и HTTPS scheme. Адрес token realm в `WWW-Authenticate` также должен начинаться с `https://`. Deployment policy запрещает redeploy существующих release tags. Runner label — `ubuntu-latest`, runner должен иметь Docker CLI, buildx и daemon. Наличие label само по себе не доказывает Docker capability. - В Actions каждого component repo нужны `NEXUS_USERNAME` и `NEXUS_PASSWORD` пользователя `wealthledger-ci`. Эта же учётная запись используется сервером для загрузки образов. Для публикации нужны browse/read/add/edit на `docker-hosted`; фактически выданный набор прав настраивается в Nexus. Built-in workflow token не заменяет эту настройку. - На сервере Caddy уже завершает TLS и передаёт весь домен на `127.0.0.1:8080` (или выбранный frontend port). Отдельного `/api` upstream нет. DNS, TLS, firewall и provisioning Caddy выполняются вне этого runbook. Серверный login и доступ к images проверяются на целевом сервере. Локальный smoke не доказывает серверные registry permissions. ## Development Из workspace с исходниками обоих компонентов: ```bash docker version docker compose version docker compose --env-file .env.development.example \ -f compose.yaml -f compose.development.yaml config --quiet docker compose --env-file .env.development.example \ -f compose.yaml -f compose.development.yaml up -d --build --wait --wait-timeout 180 ``` Откройте `http://localhost:5173`. Vite отдаёт HMR и проксирует `/api/` без rewrite; Nest работает в watch mode. Монтируются только source/public, `node_modules` и generated files остаются в container filesystem. `.env` с хоста не копируется. После изменения manifest, lockfile, tsconfig или Vite config пересоберите соответствующий image. `up --build` обновляет Linux dependencies без stale named volume для `node_modules`. Для другого порта скопируйте `.env.development.example` в игнорируемый `.env.development` и согласованно поменяйте `FRONTEND_PORT`/`FRONTEND_ORIGIN`. Пример содержит только вымышленные local secrets; server secrets сюда не входят. Поля image/data directory нужны для интерполяции base Compose, но полностью заменяются development override. В итоговом config нет production data path. Project `wealthledger-dev` хранит SQLite в `development-data`. Обычные `restart` и `down` сохраняют его. Не применяйте `down -v` к данным, которые хотите сохранить. Не используйте production project name для development. ## Публикация компонентных images Штатный release process каждого repo создаёт защищённый `vX.Y.Z` после проверенного release commit. `image.yml` проверяет tag против package/lock version, затем выполняет `npm ci`, `npm run verify`; backend дополнительно выполняет `npm run test:e2e`. У каждого image OCI version/revision labels. Images публикуются как `nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:X.Y.Z` и `.../backend:X.Y.Z`. `latest` не создаётся. Workflow отказывается перезаписывать существующие release tags; защита release tags и запрет параллельных publishers обязательны. Если одна публикация уже состоялась, не удаляйте tag для повторного запуска: проверьте опубликованный image и завершите проверку существующего artifact отдельно. Выберите опубликованные version tags, выполните pull и сравните OCI revision с release commit. Выполните smoke с disposable data/config до первого server rollout. В `release.env` комплекта хранится пара полных image references, например `nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:0.2.3`. Сохраните versions/revisions в deployment record. Release tags не должны переназначаться другим images. ## Перенос существующих images из Gitea До переключения CI и server references проверьте HTTPS token realm и настройте пользователя `wealthledger-ci`. В Actions добавьте `NEXUS_USERNAME`/`NEXUS_PASSWORD`; прежние `REGISTRY_USERNAME`/`REGISTRY_TOKEN` сохраните для отката старого workflow. Пароли вводятся интерактивно или через `--password-stdin`, не через аргументы. Скопируйте каждый опубликованный release без пересборки. Например, для `0.2.0` с установленным Skopeo и доступом чтения Gitea/записи Nexus: ```bash skopeo login git.personal.ksv741.ru skopeo login nexus.personal.ksv741.ru for component in frontend backend; do skopeo copy --all --preserve-digests \ "docker://git.personal.ksv741.ru/wealthledger/$component:0.2.0" \ "docker://nexus.personal.ksv741.ru/docker-hosted/wealthledger/$component:0.2.0" || exit 1 done ``` `--all` переносит весь image index, включая платформы и вложенные attestation manifests; `--preserve-digests` прерывает перенос при изменении digest. До копирования проверьте наличие destination tag: если он уже существует, сравните исходный и целевой index digest и вложенные manifests. Совпадающий artifact повторно не публикуйте; несовпадение требует расследования, не удаления или перезаписи tag. Частичный перенос не считается завершённым. После копирования сравните digests через `skopeo inspect --raw` и SHA-256, выполните `docker pull` обоих images пользователем `wealthledger-ci` на сервере приложения, проверьте OCI revision и container smoke на disposable data. Только после этого меняйте production image references и запускайте обычную последовательность обновления ниже. Старые Gitea packages и references сохраните: откат registry возвращает прежние адреса тех же artifacts, не восстанавливает SQLite и не меняет encryption keys. Не переносите Git release tags и не запускайте заново старый tag workflow для копирования существующего image. ## Первая установка На подготовленном сервере оператор клонирует `deploy`, настраивает внешний HTTPS и registry login, размещает server env, сертификат T-Bank и data directory вне checkout. Для новой пустой базы команда вызывается с `--init`. Для действующей установки сохраняются прежние данные и ключи, а первый переход выполняется как полное обновление с backup. Отсутствующая БД без `--init` — ошибка. Точные команды и разрешения каталогов приведены в README комплекта. Git-тег `deploy` закрепляет пару images через `release.env`. Версия комплекта не обязана совпадать с версиями компонентов. Публикация image не выпускает комплект и не обновляет сервер автоматически. Команда получает выбранный тег, загружает images до простоя и использует весь snapshot этого commit. ## Backup при остановленном backend Сначала останавливаются писатели, затем копируется весь каталог данных, включая возможные WAL/SHM. Только успешная копия допускает миграции. Локальные backups находятся рядом с data directory, вне checkout, и автоматически не удаляются. Незавершённая копия помечается `.partial-` и не подходит для restore. Ключ шифрования хранится отдельно. Локальная копия не защищает от потери диска. ## Независимое обновление frontend Если изменились только image или конфигурация frontend, backend не пересоздаётся, backup и migration не выполняются. Возможен короткий перерыв на внешней точке входа; старым browser tabs может потребоваться reload. При неизменных исправных контейнерах повтор команды не пересоздаёт сервисы. ## Независимое обновление backend Изменение image или конфигурации backend запускает stop → backup → migration → readiness. Неизменный frontend не пересоздаётся. Во время простоя возможен 502. Ошибка migration блокирует старт, ошибка готовности останавливает новый backend; ошибка frontend не останавливает исправный backend. Успех фиксируется только после проверок запуска затронутых компонентов. Одна file lock защищает установку от параллельных команд. Маркер незавершённого обновления блокирует повтор до разбора оператором. Он содержит этап, версии и путь к backup, но не секреты и не копию данных. После обновления нужен smoke через HTTPS: healthcheck сам по себе не доказывает работу Secure refresh-cookie. ## Откат image Откат версии и восстановление базы — разные операции. Прежний backend можно запускать только после проверки совместимости с текущей схемой. Автоматического rollback нет; migration service при ручном image rollback не запускается. Порядок ручных Compose-команд и проверки recovery находится в README комплекта. ## Explicit restore Оператор выбирает проверенную копию и явно принимает потерю всех более новых записей. Сначала восстановление проверяется на isolated данных с прежним ключом и совместимым image. На целевой установке останавливаются писатели, текущий каталог сохраняется под новым именем, затем восстанавливается выбранная копия. Backend пересоздаётся, чтобы заново подключить заменённый bind-каталог: простой stop/start может сохранить mount прежних данных. Проверяются целостность БД, auth/cards и расшифровка подключения. Только после подтверждённого recovery можно снять маркер незавершённой операции.