## 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. |