spec: define containerized application delivery #10
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 8s
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 8s
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
This commit was merged in pull request #11.
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
## 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. |
|
||||
Reference in New Issue
Block a user