Files
specs/openspec/changes/archive/2026-10-09-containerized-application-delivery/design.md
T
ksv741 329d66e767
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
spec: finalize wealth-ledger SDD migration
2026-10-09 21:14:22 +03:00

248 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
Frontend и backend выпускаются из независимых Git-репозиториев. Frontend —
Vite SPA, backend — NestJS API с embedded SQLite, Prisma migrations, native
Node dependencies и runtime `.proto` contracts. Браузерная сессия использует
Bearer access token и Secure httpOnly refresh-cookie с path `/api/v1/auth`.
На production-сервере уже работает Caddy. Он должен знать только домен и один
loopback port WealthLedger; TLS и server-wide routing остаются вне приложения.
Краткий простой обновляемого компонента допустим. Текущие кэши, rate limiter и
connection generation допускают только один активный backend instance.
Root workspace связывает независимые checkout, но его CI по действующему
контракту выполняет только commitlint. Поэтому orchestration принадлежит root,
а сборка и публикация каждого image — репозиторию компонента.
## Goals / Non-Goals
**Goals:**
- запускать весь application runtime локально через Docker с Linux, общей
container network, единым browser origin и быстрым frontend/backend watch;
- поставлять отдельные immutable frontend и backend images;
- публиковать наружу только frontend port, через который доступны SPA и API;
- хранить SQLite и backend secrets вне images и сохранять их при recreation;
- применять Prisma migrations до готовности новой версии backend;
- обновлять frontend или backend отдельно с явной парой установленных версий;
- дать оператору воспроизводимые backup, rollback и restore boundaries.
**Non-Goals:**
- zero-downtime, blue-green, несколько backend replicas и высокая доступность;
- PostgreSQL, Kubernetes, container orchestration вне одного сервера;
- управление Caddy, DNS, TLS, host firewall или provisioning сервера;
- изменение HTTP API, session semantics или пользовательского интерфейса;
- общий release номер для независимых репозиториев.
## Decisions
Уточнение Change Owner от 2026-09-16: обязательная настройка digests отменена.
Production принимает `FRONTEND_IMAGE`/`BACKEND_IMAGE` с release version tags.
Публикация сохраняет запрет перезаписи release tags; `latest` не используется.
### 1. Docker Compose владеет application topology
Root workspace получает production Compose и development override. Production
Compose использует опубликованные image references; development override
заменяет их local development targets, подключает исходники и отдельное local
data storage. Новые custom orchestration scripts не создаются: lifecycle
выражается штатными Compose commands и документированной последовательностью.
Production содержит два long-running services:
- `frontend`: nginx со статической SPA и proxy `/api/` в `backend:3000`;
- `backend`: один NestJS process с доступом к persistent SQLite storage.
Одноразовый migration service использует тот же backend image и data storage,
завершается до старта новой версии backend. Наружу публикуется только nginx на
настраиваемый loopback host port. Caddy проксирует весь домен на этот port и не
маршрутизирует `/api/` отдельно.
Development сохраняет те же component и network boundaries, но frontend target
запускает Vite с proxy `/api/` в `backend:3000`, а backend target запускает Nest
watch. Browser видит только frontend port. Source mounts не включают host
`node_modules`; Linux dependencies принадлежат images/volumes. Изменение
manifest или lockfile требует rebuild соответствующего development image.
Отдельный nginx gateway в development не добавляется: он создал бы третий
постоянный service, но всё равно использовал бы отличающуюся proxy-конфигурацию
для Vite HMR. Production image и его nginx routing проверяются отдельным
production Compose smoke scenario.
### 2. Frontend и backend имеют собственные multi-stage images
Frontend image собирает SPA точной project Node version и переносит только
build output и nginx configuration в version-pinned nginx runtime. Production
SPA использует same-origin API base. nginx поддерживает SPA history fallback,
передаёт `/api/` без переписывания prefix и повторно разрешает Docker DNS имя
backend после recreation.
Backend image собирается под Linux точной project Node version. Runtime содержит
compiled application, production dependencies, generated Prisma client,
migrations и T-Bank `.proto` contracts по пути, который использует production
process. Prisma CLI становится явным runtime tool backend image; test, lint и
compiler tooling в runtime не переносится. Process работает не от root и пишет
только в выделенный data path.
Оба images получают OCI metadata с repository revision и version. Base images
и package installs закрепляются воспроизводимыми версиями; новая platform или
architecture не обещается без отдельной проверки.
### 3. Gitea Container Registry хранит независимые release images
Frontend и backend workflows публикуют собственный image после push защищённого
release tag `vX.Y.Z`. Publish job повторяет канонический repository gate,
проверяет совпадение tag с package version и только затем отправляет semver tags в Gitea Container Registry. `latest` не используется.
Server deployment фиксирует полные frontend/backend image references с
semver tags; оператор выбирает опубликованную версию каждого компонента. CI использует отдельный secret с
package write permission, сервер — отдельный credential только для pull. Ни один
credential не хранится в repository или image. Возможность встроенного Gitea
workflow token публиковать packages не предполагается без проверки фактической
server version.
Root не получает image workflow: каждый repository самостоятельно доказывает и
публикует свой artifact, а Compose только фиксирует установленную пару image references.
### 4. Same-origin routing сохраняет действующую auth boundary
Production frontend nginx и development Vite направляют исходный `/api/...` в
backend без изменения path. Frontend client использует relative API base.
Backend получает `FRONTEND_ORIGIN`, равный browser-visible origin; production
работает с `NODE_ENV=production`, поэтому refresh-cookie остаётся Secure.
nginx передаёт host, forwarding headers и request ID context, не журналирует
Authorization, Cookie или request body. Backend port доступен только Compose
network. Server host публикует frontend на loopback, чтобы Caddy оставался
единственным внешним listener.
### 5. Readiness опирается на существующий runtime contract
Migration service должен завершиться успешно до запуска backend. Backend
healthcheck подтверждает завершённый HTTP-ответ от существующего auth route;
статус `401` без cookie является допустимым доказательством готового HTTP/auth
boundary. Новый публичный health endpoint не добавляется. Frontend healthcheck
получает статический root и зависит от healthy backend при полном старте.
Compose `--wait` используется как bounded readiness gate. Он не является
автоматическим rollback и не обещает отсутствие краткого `502` во время
отдельного backend update или недоступность всей точки входа во время frontend
recreation.
### 6. SQLite остаётся single-server storage в явно ограниченной роли
Production использует один backend process и один persistent host data
directory. Это расширяет прежнее локальное решение ADR 001: docs получают новый
ADR, который разрешает SQLite только для текущего single-server deployment и
фиксирует условия пересмотра — несколько replicas, иной availability target,
рост write contention или потребность в отдельном database service.
Перед migration backend останавливается и создаётся согласованная копия всего
data directory. Остановка исключает конкурентные writes и позволяет сохранить
database вместе с возможными WAL companion files без нового backup service.
Encryption key хранится отдельно от data backup; восстановление требует оба
артефакта.
Local development использует другой named volume/data directory и другие
secrets. Production data никогда не монтируется в development environment.
### 7. Обновления меняют только выбранный service
Frontend update меняет только frontend image reference, загружает image и
recreates frontend без зависимостей. Backend и data остаются запущенными, но
единая внешняя точка входа кратко недоступна во время recreation.
Backend update сначала сохраняет backup при остановленном backend, затем меняет
backend image reference. Migration service и backend используют один reference; backend
стартует только после успешной migration и становится доступен после healthcheck.
Frontend не пересоздаётся. nginx повторно разрешает service DNS после смены
backend container address.
Release compatibility остаётся обязанностью API evolution: новая сторона
ставится первой только когда прежняя сторона совместима с ней. Compose-файл
фиксирует пару image references, но не вводит второй version negotiation mechanism.
## UI/UX Applicability and Package
**Applicability and scope:** `N/A` — change меняет development, packaging и
server operations, но не экраны, состояния, interactions или responsive
поведение WealthLedger. Новый визуальный пакет не требуется.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Multi-stage development/production image, same-origin client configuration, Vite API proxy, nginx SPA/API routing и image publication workflow. |
| backend | `affected` | Multi-stage development/production image, runtime Prisma migration assets, container readiness и image publication workflow. |
| specs | `affected` | Новая capability `application-delivery` и утверждаемый SDD package. |
| docs | `affected` | Deployment/runbook, обновлённая container topology и ADR ограниченного single-server SQLite deployment. |
Root workspace дополнительно владеет Compose orchestration и кратким entrypoint
README как cross-area coordination; это не переносит component build ownership
из frontend/backend и не добавляет root CI помимо commitlint.
## Solution Assessment
| Вариант | Fit и зрелость | Security / operations | Ограничения и migration cost | Решение |
| --- | --- | --- | --- | --- |
| Docker Compose + два component images | Нативно покрывает один server, service DNS, healthchecks, volumes и targeted recreation; широко поддерживается | Малый operating surface; registry credentials и host Docker требуют обычной защиты | Краткий простой, один host, нет automatic rollback | Выбран как минимальное полное решение |
| Один объединённый application image | Простая установка одного artifact | Один lifecycle и один process boundary проще оператору | Связывает независимые релизы, пересобирает неизменённый component, усложняет watch и нарушает repository ownership | Отклонён |
| Kubernetes или Swarm | Зрелая orchestration для replicas и rolling updates | Добавляет control plane, политики и постоянную эксплуатацию | Не нужен одному server и не решает текущие SQLite/multi-instance ограничения | Отклонён до появления HA требований |
| Caddy маршрутизирует отдельно frontend и backend | Использует существующий server proxy | Убирает internal gateway layer | Server должен знать внутреннюю topology, появляются два application upstream и deployment перестаёт быть self-contained | Отклонён по выбранной внешней границе |
| Отдельный постоянный gateway service внутри Compose | Стабильный port при frontend recreation | Может уменьшить внешний простой frontend update | Третий runtime service и конфигурация без требования zero-downtime | Отклонён; пересмотреть при SLO без краткого простоя |
| SQLite на persistent directory | Уже реализован, зрел для single-process embedded workload, не требует database service | Минимальная сеть и credentials; backup требует остановки | Один writer/process topology, нет HA; новая ADR boundary обязательна | Выбран в ограниченной single-server роли |
| PostgreSQL service | Зрел для concurrency, replicas и независимого DB lifecycle | Требует отдельные credentials, backup, upgrades и monitoring | Миграция Prisma provider и существующих данных вне scope; текущая нагрузка не требует | Отклонён до появления multi-instance/availability требования |
| Собственная packaging/orchestration layer | Может точно повторить частные команды | Создаёт собственный security и maintenance surface | Дублирует Compose, registry и standard image lifecycle | Отклонён |
Gitea Container Registry и Actions используются как уже доступные mature
platform capabilities. Предварительное условие Apply — runner с Docker build/push
capability и package permissions; при отсутствии этого условия Design не
заменяется самописным registry uploader, а publish task остаётся blocked.
## Risks / Trade-offs
- Frontend recreation кратко отключает и SPA, и API ingress; это принято
текущим non-goal zero-downtime.
- Backend update может кратко возвращать `502`; healthcheck ограничивает окно,
но не устраняет его.
- Старые browser tabs могут запрашивать chunks прежнего frontend. nginx задаёт
revalidation для HTML и immutable cache для hashed assets, но прежние assets
после recreation не сохраняются. Пользователю может потребоваться reload;
автоматическое восстановление старых вкладок остаётся вне текущего scope.
- SQLite на server расширяет прежний local-only baseline. Новый ADR делает это
осознанным ограничением, а не молчаливым изменением production guarantee.
- Воспроизводимость version tags зависит от запрета их перезаписи: workflow
отклоняет существующий tag. Переназначать опубликованные tags нельзя.
- Development работает через Vite, production — через nginx. Production Compose
smoke проверяет различающийся web-server boundary до поставки.
- Полная runtime image воспроизводимость зависит от base-image availability;
digests и сохранённые release images уменьшают, но не устраняют registry risk.
## Migration Plan
1. Утвердить SDD package и новый ADR о границе SQLite на одном сервере.
2. Подготовить component images и проверить их отдельно с каноническими gates.
3. Добавить root Compose topology и development override; подтвердить watch,
same-origin API, отсутствие внешнего backend port и изоляцию local data.
4. Добавить release-tag publication в component repositories и получить version tags
проверенных images из Gitea Registry.
5. На server создать restricted environment/secrets и data directories,
сохранить исходную базу и encryption key, затем выполнить первый migration
service и production smoke через loopback port.
6. Настроить Caddy на единственный loopback port и проверить login, refresh,
чтение существующих данных и один внешний financial request.
7. Зафиксировать установленную пару image references и удалить прежний runtime
только после успешной проверки нового.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Reason | Change вводит server persistence, migrations, secrets и новую delivery boundary. |
| Trigger | Migration failure, unhealthy component, невозможность login/refresh, нечитаемое encrypted connection, повреждение данных или критическая регрессия smoke scenario. |
| Actions | Не направлять Caddy на первый deployment либо вернуть предыдущий component image reference. Старую версию backend запускать только после проверки совместимости с текущей schema. Если schema несовместима, остановить приложение и отдельно предложить восстановление согласованного backup; не выполнять restore автоматически. |
| Data safety | Backup создаётся при остановленном backend из всего data directory; encryption key сохраняется отдельно. Данные, созданные после backup, считаются более новыми и не заменяются без явного решения оператора о потере этого интервала. |
| Verification | Проверить component health, schema migration status, login/refresh, чтение существующих user/card data, decrypt/use сохранённого T-Bank connection и отсутствие повторной migration ошибки; затем записать фактически установленную пару image references. |