# Контейнерная поставка Runbook для одного Docker host, одного backend и краткого простоя. [Топология](containers.md#контейнерная-поставка) и [ADR 015](../adr/015-single-server-sqlite-deployment.md) объясняют границы. Команды выполняются из checkout `WealthLedger/main`, содержащего `compose.yaml`. Development дополнительно требует checkout `frontend/` и `backend/`. ## Предпосылки - Docker Engine с Compose plugin **не ниже 2.24.4** (`!override` в development), доступ к registry и достаточно свободного диска для non-root UID 1000. Локально проверены Engine 29.8.0, Compose 5.5.1, Linux arm64. Другую архитектуру проверяют отдельно; multi-platform images не обещаны. - Nexus Docker hosted repository `docker-hosted`, доступный через `https://nexus.ksv741.keenetic.pro/v2/`. 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. Реальная публикация и server pull-only login требуют отдельной проверки на вашей инфраструктуре. Локальные images не доказывают 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.ksv741.keenetic.pro/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. В production env хранится пара полных image references, например `nexus.ksv741.keenetic.pro/wealthledger/frontend:0.2.0`. Сохраните 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.ksv741.keenetic.pro for component in frontend backend; do skopeo copy --all --preserve-digests \ "docker://git.personal.ksv741.ru/wealthledger/$component:0.2.0" \ "docker://nexus.ksv741.keenetic.pro/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. ## Первая установка Создайте restricted environment file и data directory: ```bash sudo install -d -m 0700 /srv/wealthledger sudo install -d -m 0700 -o 1000 -g 1000 /srv/wealthledger/data sudo install -m 0600 .env.compose.example /srv/wealthledger/production.env ``` Дальнейшие команды выполняет оператор, имеющий доступ к environment file, backup directory и Docker. Заполните env локальным защищённым редактором: полные ссылки `FRONTEND_IMAGE` и `BACKEND_IMAGE` с опубликованными version tags, публичный HTTPS origin и абсолютный data path. Версии в примере — образец формата; перед запуском выберите реально опубликованные releases. Пустые required значения блокируют config; некорректные secrets блокируют backend. Не выводите полный rendered config с настоящими secrets в журналы. Для новой базы создайте JWT secret длиной хотя бы 32 символа и отдельный 32-byte encryption key в base64 (`openssl rand -hex 32` и `openssl rand -base64 32` в защищённом терминале). Сохраните ключ отдельно от data backup. Для существующей установки перенесите прежние JWT secret и encryption key, а согласованную копию **всего** data directory — при остановленном прежнем backend. Файл должен называться `wealthledger.db`; если переименовываете, согласуйте также WAL/SHM companion names до старта. Не запускайте старый и новый backend на одной базе одновременно. Войдите в registry пользователем `wealthledger-ci` без помещения пароля в командную строку: ```bash docker login nexus.ksv741.keenetic.pro --username wealthledger-ci export COMPOSE_FILE=compose.yaml export COMPOSE_PROJECT_NAME=wealthledger export COMPOSE_ENV_FILES=/srv/wealthledger/production.env docker compose config --quiet docker compose pull frontend backend migration docker compose up -d --wait --wait-timeout 120 docker compose ps -a ``` Migration и backend используют один image и один data mount. Migration должна завершиться с кодом 0; иначе backend не запускается. Readiness принимает 401 от существующего `/api/v1/auth/me` без credentials. Frontend становится healthy после backend. Backend host port отсутствует. Runtime non-root; root filesystem read-only, nginx temp files находятся в tmpfs. Проверьте SPA deep link, регистрацию/вход, refresh, существующие карты, чтение старого зашифрованного подключения и один финансовый запрос. Production refresh-cookie имеет Secure flag: проверяйте через HTTPS Caddy; loopback HTTP curl без browser cookie jar не доказывает browser session. При локальном smoke используйте disposable credentials и явную передачу cookie. Направляйте Caddy на новый deployment только после успешной проверки. Прежний runtime удаляйте только после проверки новой установки. ## Backup при остановленном backend До backend update проверьте совместимость новой версии с текущим frontend и схемой, а обратной версии — с новой схемой. При неизвестной совместимости не обещайте image rollback. В выбранном rollout отсутствуют параллельные API. Ниже data path соответствует примеру env; при другом path замените его. Сохраните текущую пару image references/revisions и отдельно — ключи. ```bash docker compose stop backend docker compose ps -a backend install -d -m 0700 /srv/wealthledger/backups BACKUP_DIRECTORY="/srv/wealthledger/backups/$(date -u +%Y%m%dT%H%M%SZ)" mkdir -m 0700 "$BACKUP_DIRECTORY" cp -a /srv/wealthledger/data "$BACKUP_DIRECTORY/data" ``` Перед копированием убедитесь, что backend остановлен, migration не выполняется и никто другой не пишет в каталог. Успех `cp` обязателен перед продолжением. Не копируйте отдельный `.db` из работающего приложения. Проверяйте backup на отдельной копии: migration status, `PRAGMA integrity_check`, auth, карты и расшифровка тестового подключения исходным ключом. Backup без проверяемого ключа недостаточен для восстановления подключения. ## Независимое обновление frontend После проверки API compatibility замените только `FRONTEND_IMAGE` в env: ```bash docker compose pull frontend docker compose up -d --no-deps --wait --wait-timeout 120 frontend ``` Backend container и SQLite не пересоздаются. Входная точка кратко недоступна. Проверьте deep link, login/refresh и новую OCI revision. Старым browser tabs может понадобиться reload: старые hashed chunks не сохраняются между images. ## Независимое обновление backend Выполните stopped-backend backup выше. Замените только `BACKEND_IMAGE` в env. Последовательность не продолжается после failed migration: ```bash docker compose pull backend migration docker compose up --no-deps --force-recreate --abort-on-container-exit \ --exit-code-from migration migration && docker compose up -d --no-deps --wait --wait-timeout 120 backend ``` Frontend не пересоздаётся; nginx повторно разрешает backend DNS. Во время перерыва возможен 502. При failure оставьте backend остановленным, сохраните диагностику migration и выберите совместимый image либо отдельный restore. Не используйте `migrate reset` или автоматический `resolve` как recovery. Проверьте migration status, health, login/refresh, карты и расшифровку подключения. После успешного smoke запишите фактически установленную пару image references. ## Откат image Для frontend верните предыдущий совместимый version tag и повторите frontend update. Для backend сначала остановите процесс и подтвердите совместимость старого image с **текущей** схемой. Верните предыдущий image reference и запустите только backend: ```bash docker compose pull backend docker compose up -d --no-deps --wait --wait-timeout 120 backend ``` Данные и encryption key не заменяются. Migration service здесь намеренно не запускается: старые migrations не являются down-migrations. Прочитайте новые записи, сделанные после backup, и проверьте auth/cards/connection. При несовместимости оставьте приложение остановленным до решения о recovery. ## Explicit restore Это отдельное решение оператора, **не часть image rollback**. Оператор выбирает backup timestamp и явно принимает потерю всех изменений после него, включая сессии, карты и подключения. Сначала воспроизведите restore на isolated project с отдельным data path/port и совместимым image; не используйте production data как fixture. Убедитесь в наличии соответствующего encryption key. После отдельного решения остановите backend и убедитесь, что migration не работает. Сохраните нынешний каталог под новым именем, ничего не удаляя: ```bash docker compose stop backend mv /srv/wealthledger/data /srv/wealthledger/data-before-restore-TIMESTAMP cp -a /srv/wealthledger/backups/CHOSEN_TIMESTAMP/data /srv/wealthledger/data ``` Проверьте owner 1000:1000, permissions, выбранный compatible image и исходный ключ. Запустите backend командой rollback с `--no-deps`, затем выполните smoke. Сверьте наличие записей до backup и отсутствие известных записей потерянного интервала. Сохраните прежний каталог для расследования. При необходимости новой migration сначала выполните обычную backup/update последовательность.