From cead9dc1765700b7e10f9567e38f6d68d4ec9656 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 16 Sep 2026 09:41:58 +0300 Subject: [PATCH] docs: document container deployment and recovery --- README.md | 1 + adr/001-prisma-sqlite-local-mvp.md | 3 + adr/015-single-server-sqlite-deployment.md | 47 +++++ adr/README.md | 1 + architecture/README.md | 1 + architecture/containers.md | 53 ++++- architecture/deployment.md | 227 +++++++++++++++++++++ 7 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 adr/015-single-server-sqlite-deployment.md create mode 100644 architecture/deployment.md diff --git a/README.md b/README.md index 3bd3d73..c48ab8c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ WealthLedger помогает частному инвестору понимат | Понять ошибку или старые данные | [Состояния интерфейса](architecture/cross-cutting/ui-resilience.md), [свежесть данных](architecture/cross-cutting/data-freshness.md) | | Понять визуальные роли и состояния | [Визуальные принципы](design/overview.md) | | Узнать причину архитектурного решения | [Журнал ADR](adr/README.md) | +| Запустить, обновить или восстановить приложение | [Контейнерная поставка](architecture/deployment.md) | ## Границы знаний diff --git a/adr/001-prisma-sqlite-local-mvp.md b/adr/001-prisma-sqlite-local-mvp.md index 1d9b557..1c00b6b 100644 --- a/adr/001-prisma-sqlite-local-mvp.md +++ b/adr/001-prisma-sqlite-local-mvp.md @@ -2,6 +2,9 @@ **Status:** Accepted, baseline decision +Deployment boundary расширена [ADR 015](015-single-server-sqlite-deployment.md) +для одного сервера и одного backend process. Ниже сохранён исходный контекст. + ## Context MVP нужен воспроизводимый локальный доступ к данным без инфраструктуры развёртывания. Доменные таблицы и миграции на baseline отсутствуют. diff --git a/adr/015-single-server-sqlite-deployment.md b/adr/015-single-server-sqlite-deployment.md new file mode 100644 index 0000000..c8c56c9 --- /dev/null +++ b/adr/015-single-server-sqlite-deployment.md @@ -0,0 +1,47 @@ +# 015. SQLite в ограниченном single-server deployment + +**Status:** Accepted + +## Context + +[ADR 001](001-prisma-sqlite-local-mvp.md) ограничивал SQLite локальным MVP. +Контейнерная поставка требует постоянного хранилища на одном сервере, +но не требует высокой доступности или нескольких экземпляров API. +Пользователи, сессии, зашифрованные подключения и банковская история уже +хранятся в SQLite. Кэши, limiter и generation подключений локальны процессу. + +## Decision + +Разрешён один backend process на одном Docker host с SQLite в постоянном +host directory. Development использует отдельный named volume. SQLite +остаётся embedded library без сетевого порта. Это расширяет deployment +boundary ADR 001; его исторический контекст сохраняется. + +Prisma migrations выполняются отдельным одноразовым service из того же image, +что и backend. Перед изменением схемы backend останавливается, всё data +directory копируется вместе с возможными WAL/SHM files. Ключ шифрования +сохраняется отдельно. Неуспешная migration блокирует запуск новой версии. + +Откат image требует совместимости с текущей схемой и не восстанавливает +старую базу. Restore требует отдельного решения о потере данных после backup. +Frontend и backend имеют независимые версии; production фиксирует version tags. +Краткий простой допустим. + +## Alternatives + +PostgreSQL потребовал бы миграции данных и отдельного database lifecycle; +текущие требования этого не оправдывают. Kubernetes, replicas и blue-green +не устраняют ограничения process-local состояния и не входят в решение. + +## Consequences + +Сервер не обещает HA или zero-downtime. Один backend instance — обязательное +ограничение, а не рекомендуемая настройка масштабирования. Нужны контроль +свободного места, согласованные backups и проверяемое восстановление. + +Решение пересматривается при необходимости нескольких replicas, другом +availability target, заметном write contention или потребности в отдельном +database service. Добавление реплик до такого решения недопустимо. + +Топология описана в [архитектуре](../architecture/containers.md#контейнерная-поставка), +операции — в [runbook](../architecture/deployment.md). diff --git a/adr/README.md b/adr/README.md index 87efa5f..8413903 100644 --- a/adr/README.md +++ b/adr/README.md @@ -19,3 +19,4 @@ - [009 — Concurrent refresh rotation policy](009-concurrent-refresh-rotation-policy.md) - [013 — НКР как первый источник кредитных рейтингов облигаций](013-nkr-bond-credit-ratings.md) - [014 — Актуальная OpenAPI-схема как вход frontend](014-trusted-openapi-input.md) +- [015 — SQLite в ограниченном single-server deployment](015-single-server-sqlite-deployment.md) diff --git a/architecture/README.md b/architecture/README.md index 67cb524..3f11f14 100644 --- a/architecture/README.md +++ b/architecture/README.md @@ -16,6 +16,7 @@ | Как обрабатываются ошибки и ожидание? | [HTTP API и ошибки](http-api.md), [состояния интерфейса](cross-cutting/ui-resilience.md) | | Как связать ошибку с обработкой запроса? | [Наблюдаемость](http-api.md#наблюдаемость) | | Как устроено подтверждённое локальное окружение? | [Container](containers.md#представление-container), [качество и ограничения](containers.md#качество-и-ограничения) | +| Как запустить, обновить или восстановить приложение? | [Развёртывание](deployment.md) | | Что означают предметные термины? | [Глоссарий](../glossary.md) | Эта карта покрывает вопросы архитектуры там, где о них есть подтверждённое diff --git a/architecture/containers.md b/architecture/containers.md index a5e2f13..99761a4 100644 --- a/architecture/containers.md +++ b/architecture/containers.md @@ -27,11 +27,50 @@ flowchart LR F -->|"HTTPS; изображения без авторизации"| C ``` -SPA выполняется в браузере пользователя. Backend — отдельное локальное +SPA выполняется в браузере пользователя. Backend — отдельное приложение; SQLite используется внутри его процесса через библиотеку и файл базы. Между API и SQLite нет сетевого database server. Граница системы на схеме показывает принадлежность частей, а не их размещение на одном узле. +## Контейнерная поставка + +Deployment view для одного сервера. Caddy принадлежит серверу; Compose +содержит два постоянных application containers и одноразовую migration. + +```mermaid +flowchart LR + Browser["Браузер · React SPA"] -->|HTTPS| Caddy["Серверный Caddy · TLS"] + subgraph Host["Один Docker host"] + Caddy -->|"HTTP · 127.0.0.1:8080"| Frontend["frontend · nginx · SPA + /api/"] + Frontend -->|"HTTP /api/v1 · internal network"| Backend["backend · NestJS · один process"] + Migration["migration · Prisma · one-shot"] -->|"До старта backend"| Data[("Host directory · SQLite + WAL")] + Backend -->|"Чтение и запись /data"| Data + Secrets["Restricted environment · ключи вне images"] --> Backend + end +``` + +Только frontend публикует host port, привязанный к loopback. Backend не +публикует порт, но имеет исходящий доступ к внешним API. nginx сохраняет +`/api/` prefix, forwarding headers и request ID; Docker DNS разрешается +повторно после backend recreation. HTML требует revalidation, hashed assets +имеют immutable cache. Caddy не содержит отдельного маршрута к API. + +Development заменяет nginx на Vite с HMR, backend — на Nest watch, а host data +path — на отдельный named volume. Browser origin остаётся единым. Linux +node_modules не монтируются с хоста. Production SPA собирается с relative API +base; production cookies остаются Secure, публичный origin использует HTTPS. + +Компоненты имеют независимые images и версии. Production фиксирует image +version tags; migration использует тот же backend image. Успех migration предшествует +backend healthcheck на существующем auth route; frontend ждёт healthy backend. +Runtime работает non-root, SQLite сохраняется вне image, ключ шифрования и JWT +secret поступают из restricted environment и сохраняются между обновлениями. + +Ограничение SQLite расширено в [ADR 015](../adr/015-single-server-sqlite-deployment.md). +[Runbook](deployment.md) описывает установку, independent +updates, backup, rollback и explicit restore. Он также перечисляет внешние +prerequisites: локальный smoke не подтверждает registry/server rollout. + ## Ответственность частей | Часть | Ответственность | @@ -106,10 +145,10 @@ credentials не являются сохраняемым представлен ## Сохранение и восстановление данных -Резервная копия SQLite должна быть согласованной: для работающей базы -используется SQLite backup API с учётом WAL. Несогласованное копирование -файлов базы и WAL не сохраняет целостный снимок. Восстановление проверяется -на отдельной копии. Ключ шифрования подключения хранится отдельно от базы; +Поддерживаемая процедура останавливает backend и копирует весь data directory +с возможными WAL/SHM files до migration. Копирование отдельных файлов +работающей базы не сохраняет целостный снимок; для live backup потребовался бы +SQLite backup API. Восстановление проверяется на отдельной копии. Ключ шифрования подключения хранится отдельно от базы; его потеря делает сохранённый T-Bank token нечитаемым. Откат приложения требует совместимости предыдущей версии с сохранённой @@ -150,12 +189,12 @@ access token и вращаемый refresh token. [Политика query-кэш отмечены в [аутентификации](../domains/authentication/overview.md#регистрация-и-вход) и [HTTP API](http-api.md#error-contract). -Подтверждённая топология — локальная SPA и локальный API с файлом SQLite. +Поддерживается локальный Compose и ограниченная single-server topology с SQLite. Origin браузера, адрес API и cookie policy должны быть согласованы. `localhost` и `127.0.0.1` задают разные browser-границы для cookie и CORS. Локальный HTTP поддерживается в development-режиме backend; в остальных режимах refresh-cookie имеет Secure flag и требует защищённого соединения. -Наличие локального запуска не задаёт production-топологию или гарантии SLA. +Single-server deployment не задаёт гарантий HA или zero-downtime. Кэши и лимитер T-Bank действуют на экземпляр backend. Ротация ключа шифрования подключений и согласование изменений подключения между распределёнными diff --git a/architecture/deployment.md b/architecture/deployment.md new file mode 100644 index 0000000..5097112 --- /dev/null +++ b/architecture/deployment.md @@ -0,0 +1,227 @@ +# Контейнерная поставка + +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 последовательность.