# Контейнерная поставка 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 не обещаны. - Gitea Container Registry; проверяемая версия сервера — 1.27.3. Runner label — `ubuntu-latest`, runner должен иметь Docker CLI, buildx и daemon. Наличие label само по себе не доказывает Docker capability. - В Actions каждого component repo нужны `REGISTRY_USERNAME` и `REGISTRY_TOKEN` отдельной identity с package write permission. Server использует другую identity только с package read permission. 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 публикуются как `git.personal.ksv741.ru/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, например `git.personal.ksv741.ru/wealthledger/frontend:0.1.0`. Сохраните versions/revisions в deployment record. Release tags не должны переназначаться другим images. ## Первая установка Создайте 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 server identity без помещения token в командную строку: ```bash docker login git.personal.ksv741.ru --username SERVER_PULL_USER 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 последовательность.