Files
docs/architecture/deployment.md
T
ksv741 cead9dc176
Docs security / security (pull_request) Successful in 6s
Docs verify / verify (pull_request) Successful in 11s
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 10s
docs: document container deployment and recovery
2026-09-16 09:41:58 +03:00

228 lines
15 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.
# Контейнерная поставка
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 последовательность.