feat(db): перевести хранилище с SQLite на PostgreSQL
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s

При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-08 01:36:48 +07:00
co-authored by Cursor
parent 0e5fb065e2
commit ec43591a99
128 changed files with 4304 additions and 3639 deletions
+30 -21
View File
@@ -19,7 +19,7 @@
| Компонент | Путь | Стек | Порт (runtime) | Docker-образ |
|-----------|------|------|----------------|--------------|
| Frontend | корень (`app/`, `components/`, `lib/`, …) | Next.js 16.2.4 (App Router), React 19 | 3000 | `…-frontend` |
| Backend | `backend/` | Fastify 5, TypeScript ESM, SQLite, Drizzle | 8000 | `…-backend` |
| Backend | `backend/` | Fastify 5, TypeScript ESM, PostgreSQL 18, Drizzle | 8000 | `…-backend` |
| Контракты | `packages/contracts/` | Zod 4, `@mmapp/contracts` | — | встраиваются в frontend/backend |
| Updater | `deploy/updater/` | bash, `docker:27-cli`, curl, jq | — | `…-updater` |
| Деплой | `deploy/docker-compose.yml` | Docker Compose | — | — |
@@ -43,11 +43,11 @@ flowchart TB
Browser["Браузер"]
NextDev["Next.js :3000"]
FastifyDev["Fastify :8000"]
SqliteDev["SQLite файл"]
PostgresDev["PostgreSQL 18"]
Contracts["@mmapp/contracts"]
Browser --> NextDev
Browser -->|"fetch /api, CORS"| FastifyDev
FastifyDev --> SqliteDev
FastifyDev --> PostgresDev
Contracts --> NextDev
Contracts --> FastifyDev
end
@@ -60,7 +60,8 @@ flowchart TB
subgraph prod [Прод-сервер]
FE["mmapp-frontend :3000"]
BE["mmapp-backend :8000"]
DBVol["volume backend-data /app/data"]
PG["mmapp-postgres"]
DBVol["volume sqlite ETL /app/data"]
UPD["mmapp-updater"]
Sock["/var/run/docker.sock"]
Reg --> FE
@@ -69,6 +70,7 @@ flowchart TB
UPD --> Sock
UPD --> FE
UPD --> BE
BE --> PG
BE --> DBVol
BrowserProd["Клиент"] --> FE
BrowserProd -->|"live + backendUrl"| BE
@@ -117,7 +119,7 @@ sequenceDiagram
```
- Триггер workflow: `push` в ветку `main`, ручной `workflow_dispatch`.
- Параллельные jobs: `backend-image`, `frontend-image`, `updater-image`; опционально `notify-webhook` после backend и frontend, если задан секрет `DEPLOY_WEBHOOK_URL`.
- Параллельные jobs: `backend-test` (PostgreSQL 18), `backend-image`, `frontend-image`, `updater-image`; опционально `notify-webhook` после backend и frontend, если задан секрет `DEPLOY_WEBHOOK_URL`.
- Теги на каждый успешный push: **`:latest`** и **`:<commit-sha>`**; платформа **linux/amd64**.
- На сервере образы подтягиваются вручную (`docker compose pull`) и/или через **updater** (сравнение digest у тега из `targets.json`). Webhook CI **не заменяет** updater.
@@ -125,7 +127,8 @@ sequenceDiagram
| Зависимость | Реализация |
|-------------|------------|
| SQLite | Не отдельный контейнер. Файл `mikrotik.db` в томе `backend-data``/app/data` (`DATABASE_PATH=/app/data/mikrotik.db` в образе backend). |
| PostgreSQL | Контейнер `mmapp-postgres` (`postgres:18-alpine`). `DATABASE_URL=postgres://mmapp:…@postgres:5432/mmapp`. |
| SQLite (ETL) | Файл `mikrotik.db` на томе `/app/data` (`DATABASE_PATH=/app/data/mikrotik.db`). При первом старте, если PG пустой, backend сам импортирует данные и ставит маркер. Повторный старт не копирует заново. |
| Docker socket | Только у контейнера updater: `/var/run/docker.sock` — доступ к Docker API хоста (управление контейнерами, pull). |
## Локальная разработка
@@ -134,7 +137,8 @@ sequenceDiagram
- **Node.js 22** (как в `Dockerfile.frontend` и `backend/Dockerfile`).
- **npm** с workspaces; установка из корня: `npm ci` или `npm install`.
- Для нативной сборки `better-sqlite3` на Linux может понадобиться toolchain (`python3`, `make`, `g++`); в Docker-образе backend они уже ставятся.
- Для нативной сборки `better-sqlite3` на Linux может понадобиться toolchain (`python3`, `make`, `g++`); в Docker-образе backend они уже ставятся (нужен только для одноразового boot-ETL из `mikrotik.db`).
- Локально нужен PostgreSQL 18 (`deploy/docker-compose.postgres.yml` или `DATABASE_URL`).
- Backend Docker-образ ставит только workspaces `backend` + `contracts` (без корневых Next/React deps); в production логи — JSON без `pino-pretty`.
### Запуск
@@ -161,7 +165,8 @@ Backend — скопировать `backend/.env.example` в `backend/.env`:
| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `DATABASE_PATH` | `./mikrotik.db` | путь к файлу SQLite |
| `DATABASE_URL` | `postgres://mmapp:[email protected]:5432/mmapp` | подключение к PostgreSQL |
| `DATABASE_PATH` | `./mikrotik.db` | SQLite только для boot-ETL, если PG пустой |
| `PORT` | `8000` | порт Fastify |
| `CORS_ORIGIN` | `http://localhost:3000` | origin фронтенда для CORS |
@@ -179,12 +184,10 @@ Frontend — в репозитории нет корневого `.env.example`.
npm run build -w @mmapp/contracts
```
После изменения схем Drizzle:
Схема PostgreSQL накатывается при старте backend из [`backend/drizzle/0000_postgresql.sql`](backend/drizzle/0000_postgresql.sql). Опциональный ETL:
```bash
npm --prefix backend run db:generate
npm --prefix backend run db:migrate
npm --prefix backend run db:studio
npm --prefix backend run db:migrate-from-sqlite
```
## CI/CD (Gitea Actions)
@@ -281,7 +284,8 @@ SSO auth-portal: [`docs/integrate-auth-portal.md`](docs/integrate-auth-portal.md
| Сервис | `container_name` | Образ (пример) | Порты host:container | Тома | `restart` |
|--------|------------------|----------------|----------------------|------|-----------|
| backend | `mmapp-backend` | `git.shx.one/denozord/mikrotikmanager-backend:latest` | `8000:8000` | `backend-data` `/app/data` | `unless-stopped` |
| postgres | `mmapp-postgres` | `postgres:18-alpine` | нет (внутренняя сеть) | `mmapp-pgdata` | `unless-stopped` |
| backend | `mmapp-backend` | `git.shx.one/denozord/mikrotikmanager-backend:latest` | `8000:8000` | sqlite ETL → `/app/data` | `unless-stopped` |
| frontend | `mmapp-frontend` | `git.shx.one/denozord/mikrotikmanager-frontend:latest` | `3000:3000` | — | `unless-stopped` |
| updater | `mmapp-updater` | `git.shx.one/denozord/mikrotikmanager-updater:latest` | не публикуются | docker.sock, `updater-state``/state`, `targets.json``/etc/updater/targets.json:ro` | `unless-stopped` |
@@ -297,11 +301,14 @@ SSO auth-portal: [`docs/integrate-auth-portal.md`](docs/integrate-auth-portal.md
### Переменные окружения (прод)
**Backend** (в образе заданы `NODE_ENV=production`, `PORT=8000`, `DATABASE_PATH=/app/data/mikrotik.db`; в compose обычно переопределяют только CORS):
**Backend** (в образе заданы `NODE_ENV=production`, `PORT=8000`; в compose задают `DATABASE_URL`, `DATABASE_PATH` для ETL и CORS):
| Переменная | Источник в compose | Назначение |
|------------|-------------------|------------|
| `CORS_ORIGIN` | `${CORS_ORIGIN:-http://localhost:3000}` | origin UI, с которого браузер вызывает API |
| `DATABASE_URL` | `postgres://mmapp:${POSTGRES_PASSWORD}@postgres:5432/mmapp` | PostgreSQL |
| `DATABASE_PATH` | `/app/data/mikrotik.db` | SQLite на томе для одноразового ETL |
| `POSTGRES_PASSWORD` | обязателен | пароль пользователя `mmapp` |
**Frontend** (в образе: `PORT=3000`, `HOSTNAME=0.0.0.0`). URL API в образ **не** зашит — задаётся в браузере (режим live + URL бэкенда) вместе с `CORS_ORIGIN` на backend.
@@ -312,7 +319,7 @@ SSO auth-portal: [`docs/integrate-auth-portal.md`](docs/integrate-auth-portal.md
| `TARGETS_FILE` | `/etc/updater/targets.json` | список целей |
| `STATE_FILE` | `/state/updater-state.json` | digest и ошибки |
| `POLL_INTERVAL_SECONDS` | `300` | пауза между циклами опроса |
| `HEALTH_TIMEOUT_SECONDS` | `120` | ожидание HTTP health |
| `HEALTH_TIMEOUT_SECONDS` | `600` | ожидание HTTP health (первый boot с ETL может занять минуты) |
| `STOP_TIMEOUT_SECONDS` | `30` | `docker stop -t` |
| `REGISTRY` | `git.shx.one` | registry для `docker login` |
| `REGISTRY_USERNAME` | пусто | логин (если заданы оба с паролем) |
@@ -516,18 +523,20 @@ docker compose up -d --force-recreate
### Откат
- **Автоматически:** updater при failed health после обновления — контейнер на `previous_digest`.
- **Вручную:** остановить контейнер, запустить образ с нужным тегом или digest из registry, сохранив те же volume и labels. Откат **схемы SQLite** updater не выполняет — нужен отдельный backup тома `backend-data` / файла `mikrotik.db`.
- **Вручную:** остановить контейнер, запустить образ с нужным тегом или digest из registry, сохранив те же volume и labels. Откат **схемы PostgreSQL** updater не выполняет — нужен `pg_dump` / restore или том `mmapp-pgdata`.
### Резервное копирование SQLite
### Резервное копирование PostgreSQL
Том `backend-data` (или `mmapp-backend-data` при явном `docker volume create`). Пример остановки backend для консистентной копии:
Том `mmapp-pgdata` и (до cleanup) sqlite `mikrotik.db` на `/app/data`. Из UI: Настройки → «Скачать бэкап» (`pg_dump -Fc`). С хоста:
```bash
docker stop mmapp-backend
docker run --rm -v backend-data:/data -v $(pwd):/backup alpine tar czf /backup/mikrotik-db-backup.tar.gz -C /data .
docker start mmapp-backend
docker exec mmapp-postgres pg_dump -Fc -U mmapp mmapp > mikrotik-manager.dump
```
Восстановление: UI «Восстановить из файла» или `pg_restore --clean --if-exists -d …`.
Том sqlite после успешного ETL и smoke **не удаляйте сразу** — оставьте как fallback, затем уберите `mikrotik.db`, когда маркер `data_migration.sqlite_imported_at` подтверждён.
### Отладка
| Действие | Команда |