Files
MikrotikManager/README.md
T
Denozordec 9ccc8459d7
Docker images / prepare-release (push) Successful in 38s
Docker images / backend-image (push) Has been skipped
Docker images / frontend-image (push) Has been skipped
Docker images / updater-image (push) Has been skipped
Docker images / publish-release (push) Has been skipped
Docker images / notify-webhook (push) Has been skipped
feat: update application versioning and enhance release management
- Bumped application version to 1.0.0 across all relevant package files, ensuring consistency in versioning.
- Introduced new environment variables for application version and release URL in Dockerfiles, improving deployment transparency.
- Enhanced the health check endpoint to return the current application version, providing better visibility for monitoring.
- Updated CI/CD workflows to include steps for preparing and publishing releases, streamlining the release process.
- Added a new "Releases" section in the application sidebar for easier access to version information.
2026-05-12 17:19:20 +07:00

497 lines
25 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.
# MikrotikManager-3
Монорепозиторий веб-приложения для управления MikroTik: UI на Next.js, API на Fastify, общие Zod-контракты, сборка Docker-образов через Gitea Actions и автоматическое обновление контейнеров на сервере через sidecar updater.
## Содержание
1. [Состав монорепозитория](#состав-монорепозитория)
2. [Архитектура](#архитектура)
3. [Локальная разработка](#локальная-разработка)
4. [CI/CD (Gitea Actions)](#cicd-gitea-actions)
5. [Прод-развёртывание Docker](#прод-развёртывание-docker)
6. [Сервис updater](#сервис-updater)
7. [Первичная настройка сервера](#первичная-настройка-сервера)
8. [Эксплуатация и сопровождение](#эксплуатация-и-сопровождение)
9. [Чеклист деплоя](#чеклист-деплоя)
## Состав монорепозитория
| Компонент | Путь | Стек | Порт (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` |
| Контракты | `packages/contracts/` | Zod 4, `@mmapp/contracts` | — | встраиваются в frontend/backend |
| Updater | `deploy/updater/` | bash, `docker:27-cli`, curl, jq | — | `…-updater` |
| Деплой | `deploy/docker-compose.yml` | Docker Compose | — | — |
| CI | `.gitea/workflows/docker.yml` | Gitea Actions, Buildx | — | push в `git.shts.su` |
Имена образов в registry для репозитория `denozord/MikrotikManager-3`:
- `git.shts.su/denozord/mikrotikmanager-backend`
- `git.shts.su/denozord/mikrotikmanager-frontend`
- `git.shts.su/denozord/mikrotikmanager-updater`
Для другого owner/repo подставьте имя по правилу CI (см. [CI/CD](#cicd-gitea-actions)).
## Архитектура
### Общая схема
```mermaid
flowchart TB
subgraph dev [Локальная разработка]
Browser["Браузер"]
NextDev["Next.js :3000"]
FastifyDev["Fastify :8000"]
SqliteDev["SQLite файл"]
Contracts["@mmapp/contracts"]
Browser --> NextDev
Browser -->|"fetch /api, CORS"| FastifyDev
FastifyDev --> SqliteDev
Contracts --> NextDev
Contracts --> FastifyDev
end
subgraph ci [Gitea Actions]
WF[".gitea/workflows/docker.yml"]
WF --> Reg["git.shts.su registry"]
end
subgraph prod [Прод-сервер]
FE["mmapp-frontend :3000"]
BE["mmapp-backend :8000"]
DBVol["volume backend-data /app/data"]
UPD["mmapp-updater"]
Sock["/var/run/docker.sock"]
Reg --> FE
Reg --> BE
Reg --> UPD
UPD --> Sock
UPD --> FE
UPD --> BE
BE --> DBVol
BrowserProd["Клиент"] --> FE
BrowserProd -->|"live + backendUrl"| BE
end
```
### Frontend и backend
- Браузер загружает UI с порта **3000**.
- Запросы к API выполняются **напрямую с клиента** (`fetch`) на URL бэкенда. Next.js rewrites/proxy **не используются** (`next.config.ts` — только `output: "standalone"`).
- URL бэкенда и режим данных (`mock` / `live`) хранятся в `localStorage` (`routerlists:data-source`, `routerlists:backend-url`); по умолчанию `http://localhost:8000`. Настройка — страница **Настройки** в UI (`app/(main)/settings/page.tsx`, провайдер `lib/data-source.tsx`).
- Backend слушает **8000**, отдаёт `GET /health` и маршруты под `/api/…`. CORS разрешён для **одного** origin — переменная `CORS_ORIGIN` (`backend/src/config.ts`, `backend/src/index.ts`).
- Отдельно от UI-режима: `NEXT_PUBLIC_API_MODE` (`mock` по умолчанию, `live` для RouterOS REST) — `lib/api-mock.ts`; для работы с API приложения через бэкенд в проде достаточно режима **live** в UI и корректного `backendUrl`.
### Общие контракты (`packages/contracts`)
- npm workspaces: корень + `packages/*` + `backend` (`package.json`).
- Пакет `@mmapp/contracts`: сборка `tsc``dist/`, exports `./servers`, `./events`, `./alerts`.
- При `npm install` выполняется `postinstall``npm run build -w @mmapp/contracts`.
- Backend: валидация и типы в маршрутах; frontend: типы и разбор ответов в `shared/api/`.
### Поток образов: CI → registry → сервер
```mermaid
sequenceDiagram
participant Gitea
participant Actions
participant Registry
participant Updater
participant Backend
participant Frontend
Gitea->>Actions: push main / workflow_dispatch
Actions->>Registry: push backend/frontend/updater :latest и :sha
Actions-->>Gitea: optional DEPLOY_WEBHOOK_URL POST
loop каждые POLL_INTERVAL_SECONDS
Updater->>Registry: imagetools inspect digest
alt digest изменился
Updater->>Registry: docker pull
Updater->>Backend: stop/rm/run из snapshot
Updater->>Frontend: stop/rm/run из snapshot
Updater->>Backend: HTTP health
Updater->>Frontend: HTTP health
end
end
```
- Триггер workflow: `push` в ветку `main`, ручной `workflow_dispatch`.
- Параллельные jobs: `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.
### Зависимости в проде
| Зависимость | Реализация |
|-------------|------------|
| SQLite | Не отдельный контейнер. Файл `mikrotik.db` в томе `backend-data``/app/data` (`DATABASE_PATH=/app/data/mikrotik.db` в образе backend). |
| Docker socket | Только у контейнера updater: `/var/run/docker.sock` — доступ к Docker API хоста (управление контейнерами, pull). |
## Локальная разработка
### Требования
- **Node.js 22** (как в `Dockerfile.frontend` и `backend/Dockerfile`).
- **npm** с workspaces; установка из корня: `npm ci` или `npm install`.
- Для нативной сборки `better-sqlite3` на Linux может понадобиться toolchain (`python3`, `make`, `g++`); в Docker-образе backend они уже ставятся.
### Запуск
Два процесса (frontend и backend):
```powershell
npm install
npm run dev
```
```powershell
npm --prefix backend run dev
```
| Сервис | URL | Проверка |
|--------|-----|----------|
| Frontend | http://localhost:3000 | открыть в браузере |
| Backend | http://localhost:8000 | `curl -fsS http://localhost:8000/health` |
### Переменные окружения (разработка)
Backend — скопировать `backend/.env.example` в `backend/.env`:
| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `DATABASE_PATH` | `./mikrotik.db` | путь к файлу SQLite |
| `PORT` | `8000` | порт Fastify |
| `CORS_ORIGIN` | `http://localhost:3000` | origin фронтенда для CORS |
Frontend — в репозитории нет корневого `.env.example`. Опционально `.env.local`:
| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `NEXT_PUBLIC_API_MODE` | `mock` | `live` — вызовы RouterOS REST через `lib/api-mock.ts` |
Режим **live** для API приложения и URL бэкенда задаются в UI (не через build-time env).
### Контракты и БД в dev
```bash
npm run build -w @mmapp/contracts
```
После изменения схем Drizzle:
```bash
npm --prefix backend run db:generate
npm --prefix backend run db:migrate
npm --prefix backend run db:studio
```
## CI/CD (Gitea Actions)
Файл: `.gitea/workflows/docker.yml` (имя workflow: **Docker images**).
| Job | Build context | Dockerfile | Имя образа |
|-----|---------------|------------|------------|
| `backend-image` | `.ci/docker/backend` (staging в CI) | `backend/Dockerfile` | `git.shts.su/<owner>/<stem>-backend` |
| `frontend-image` | `.ci/docker/frontend` | `Dockerfile.frontend` | `git.shts.su/<owner>/<stem>-frontend` |
| `updater-image` | `deploy/updater` | `deploy/updater/Dockerfile` | `git.shts.su/<owner>/<stem>-updater` |
- `<owner>` — первая часть `gitea.repository`, lower case.
- `<stem>` — имя репозитория lower case без суффикса `-<цифры>` в конце (например `MikrotikManager-3``mikrotikmanager`).
Логин в registry в CI: `gitea.actor` + секрет **`ACTIONS_PAT`**.
Кэш Buildx: `type=gha`, отдельные scope `backend`, `frontend`, `updater`.
Опциональный job **`notify-webhook`**: POST JSON `{"repository","sha","ref","version","tag","releaseUrl"}` на URL из секрета **`DEPLOY_WEBHOOK_URL`** после успешной сборки backend и frontend и публикации релиза (updater в `needs` не входит).
### Версионирование и релизы
- Базовая версия: **`v1.0.0`**. Линия semver: `1.x.y` (major `2.x` вне scope).
- Job **`prepare-release`** запускает [`.ci/scripts/compute-release.mjs`](.ci/scripts/compute-release.mjs): коммиты с последнего тега `v*.*.*`, bump по **Conventional Commits**.
- **`feat` / `feat!` / `BREAKING CHANGE`** → minor (`1.x.0`); **`fix`**, `chore`, `docs`, `refactor`, `style`, `test`, `build`, `ci` → patch (`1.0.x`).
- Если после последнего тега нет новых коммитов, релиз и сборка образов **пропускаются**.
- Job **`publish-release`**: annotated tag `v1.2.3`, Gitea Release на `https://git.shts.su` (markdown notes), образы с тегами `:latest`, `:<sha>`, `:<semver>`.
- UI: версия в sidebar и страница **`/releases`**; manifest `public/release-manifest.json` (в CI подставляется из артефакта).
- Bootstrap: один раз выставить `1.0.0` в workspace `package.json` и создать тег **`v1.0.0`** на `main` перед первым автоматическим bump.
- Сообщения коммитов: см. [`.cursor/rules/release-versioning.mdc`](.cursor/rules/release-versioning.mdc).
## Прод-развёртывание Docker
Эталон: `deploy/docker-compose.yml`. Рабочий каталог для команд compose — `deploy/` (или укажите `-f deploy/docker-compose.yml` из корня репозитория).
### Прод-контейнеры
| Сервис | `container_name` | Образ (пример) | Порты host:container | Тома | `restart` |
|--------|------------------|----------------|----------------------|------|-----------|
| backend | `mmapp-backend` | `git.shts.su/denozord/mikrotikmanager-backend:latest` | `8000:8000` | `backend-data``/app/data` | `unless-stopped` |
| frontend | `mmapp-frontend` | `git.shts.su/denozord/mikrotikmanager-frontend:latest` | `3000:3000` | — | `unless-stopped` |
| updater | `mmapp-updater` | `git.shts.su/denozord/mikrotikmanager-updater:latest` | не публикуются | docker.sock, `updater-state``/state`, `targets.json``/etc/updater/targets.json:ro` | `unless-stopped` |
Метки для updater на backend и frontend:
| Метка | Пример значения |
|-------|-----------------|
| `mmapp.updater.managed` | `true` |
| `mmapp.updater.target` | `backend` / `frontend` |
| `mmapp.updater.image` | полное имя образа с тегом |
Явной пользовательской сети в compose нет — используется сеть проекта Compose по умолчанию.
### Переменные окружения (прод)
**Backend** (в образе заданы `NODE_ENV=production`, `PORT=8000`, `DATABASE_PATH=/app/data/mikrotik.db`; в compose обычно переопределяют только CORS):
| Переменная | Источник в compose | Назначение |
|------------|-------------------|------------|
| `CORS_ORIGIN` | `${CORS_ORIGIN:-http://localhost:3000}` | origin UI, с которого браузер вызывает API |
**Frontend** (в образе: `PORT=3000`, `HOSTNAME=0.0.0.0`). URL API в образ **не** зашит — задаётся в браузере (режим live + URL бэкенда) вместе с `CORS_ORIGIN` на backend.
**Updater**:
| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `TARGETS_FILE` | `/etc/updater/targets.json` | список целей |
| `STATE_FILE` | `/state/updater-state.json` | digest и ошибки |
| `POLL_INTERVAL_SECONDS` | `300` | пауза между циклами опроса |
| `HEALTH_TIMEOUT_SECONDS` | `120` | ожидание HTTP health |
| `STOP_TIMEOUT_SECONDS` | `30` | `docker stop -t` |
| `REGISTRY` | `git.shts.su` | registry для `docker login` |
| `REGISTRY_USERNAME` | пусто | логин (если заданы оба с паролем) |
| `REGISTRY_PASSWORD` | пусто | пароль registry |
### Docker Compose
```bash
docker login git.shts.su
export CORS_ORIGIN=http://<хост>:3000
export REGISTRY_USERNAME=<user>
export REGISTRY_PASSWORD=<token>
```
Скопировать и отредактировать конфиг updater (health URL должны быть достижимы **из контейнера updater**):
```bash
cp deploy/updater/targets.json.example deploy/updater/targets.json
```
В `deploy/docker-compose.yml` для updater заменить bind-mount на рабочий файл:
```yaml
- ./updater/targets.json:/etc/updater/targets.json:ro
```
Запуск:
```bash
cd deploy
docker compose pull
docker compose up -d
```
### Эквивалентные `docker run`
Имена томов можно согласовать с compose (`backend-data`, `updater-state`) или задать явно.
**Backend:**
```bash
docker volume create backend-data
docker run -d \
--name mmapp-backend \
--restart unless-stopped \
-p 8000:8000 \
-e CORS_ORIGIN=http://localhost:3000 \
-v backend-data:/app/data \
--label mmapp.updater.managed=true \
--label mmapp.updater.target=backend \
--label mmapp.updater.image=git.shts.su/denozord/mikrotikmanager-backend:latest \
git.shts.su/denozord/mikrotikmanager-backend:latest
```
**Frontend:**
```bash
docker run -d \
--name mmapp-frontend \
--restart unless-stopped \
-p 3000:3000 \
--label mmapp.updater.managed=true \
--label mmapp.updater.target=frontend \
--label mmapp.updater.image=git.shts.su/denozord/mikrotikmanager-frontend:latest \
git.shts.su/denozord/mikrotikmanager-frontend:latest
```
**Updater:**
```bash
docker volume create updater-state
docker run -d \
--name mmapp-updater \
--restart unless-stopped \
-e REGISTRY=git.shts.su \
-e REGISTRY_USERNAME=<user> \
-e REGISTRY_PASSWORD=<token> \
-e POLL_INTERVAL_SECONDS=300 \
-e HEALTH_TIMEOUT_SECONDS=120 \
-e STOP_TIMEOUT_SECONDS=30 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v updater-state:/state \
-v /path/to/targets.json:/etc/updater/targets.json:ro \
git.shts.su/denozord/mikrotikmanager-updater:latest
```
### Проверка после деплоя
| Проверка | Команда / ожидание |
|----------|-------------------|
| Backend health | `curl -fsS http://127.0.0.1:8000/health` → JSON со `status` |
| Frontend | `curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/``200` |
| Контейнеры | `docker ps --filter name=mmapp-` |
| UI | режим **live**, URL бэкенда совпадает с доступом клиента и с `CORS_ORIGIN` |
## Сервис updater
Исходники: `deploy/updater/entrypoint.sh`, образ `deploy/updater/Dockerfile`.
### Назначение
Sidecar на хосте с Docker: периодически сравнивает **digest манифеста** образа в registry с последним применённым digest, при изменении выполняет `docker pull`, пересоздаёт целевой контейнер и проверяет HTTP health. Состояние — файл `STATE_FILE` (том `updater-state`).
### Обнаружение новых образов
- `docker buildx imagetools inspect <image>` → digest манифеста (тег в `targets.json`, обычно `:latest`).
- Сравнение с `last_applied_digest` в state; при совпадении — noop.
- При первом запуске — baseline из digest работающего контейнера или remote, чтобы не пересоздавать без смены digest.
- Если remote digest недоступен — running контейнер **не** останавливается, в state пишется ошибка.
### Перезапуск backend/frontend
Только контейнеры с метками `mmapp.updater.managed=true`, совпадающими `mmapp.updater.target` и `mmapp.updater.image` с записью в `targets.json`.
Последовательность: `docker inspect` (snapshot) → `docker pull``docker stop``docker rm``docker run -d` с восстановлением env, mounts, `-p`, `--network`, labels, restart policy из snapshot.
При неуспешном health после обновления — откат на `previous_digest` (`image@digest`). Если предыдущего digest нет — откат пропускается (лог `rollback_skipped`).
### Конфигурация целей
Пример: `deploy/updater/targets.json.example`. Поля цели: `id`, `container_name`, `image`, `health.type`, `health.url`, `health.expect_status`.
**Важно:** в примере health URL — `http://127.0.0.1:8000/health` и `http://127.0.0.1:3000/`. В default bridge-сети контейнера updater `127.0.0.1` указывает на **сам updater**, а не на backend/frontend. Перед продом задайте URL, **достижимые из контейнера updater** (например опубликованные порты хоста при `network_mode: host` у updater, `host.docker.internal` с `extra_hosts`, или иной согласованный с вашей сетью вариант). Скопируйте example в `deploy/updater/targets.json` и отредактируйте.
### Docker socket и безопасность
- Монтирование `/var/run/docker.sock` даёт updater права на Docker API хоста: остановка, удаление и создание контейнеров, pull от имени хоста.
- Ограничение по меткам снижает риск случайного пересоздания чужих контейнеров, но **не** изолирует updater от остального Docker на хосте.
- Учётные данные registry передавайте через env, не коммитьте в git.
- Блокировка цели: `/state/locks/<target_id>`; при занятом lock цикл для цели пропускается.
- При ошибке цикла пауза удваивается (`2 × POLL_INTERVAL_SECONDS`) с джиттером до +10%.
### Валидация и staging
```bash
bash deploy/updater/validate.sh
```
Чеклист сценариев на staging: `deploy/updater/test-staging.sh`.
## Первичная настройка сервера
Пошагово на чистом Linux-хосте с доступом в интернет и к `git.shts.su`.
1. Установить Docker Engine и плагин Compose (официальная документация Docker для вашего дистрибутива).
2. Проверить:
```bash
docker version
docker compose version
```
3. Открыть входящие порты **3000** (frontend) и **8000** (backend) в firewall или настроить внешний доступ согласно вашей схеме (reverse proxy в репозитории не описан).
4. Войти в registry:
```bash
docker login git.shts.su
```
5. Получить файлы деплоя: клонировать репозиторий или скопировать каталог `deploy/` и подготовить `deploy/updater/targets.json`.
6. Задать переменные окружения для compose (`CORS_ORIGIN`, `REGISTRY_USERNAME`, `REGISTRY_PASSWORD` при приватных образах).
7. Подтянуть образы и запустить стек:
```bash
cd deploy
docker compose pull
docker compose up -d
```
8. Проверить health (см. таблицу выше) и логи updater:
```bash
docker logs mmapp-updater --tail 50
```
9. В UI включить режим **live** и указать URL бэкенда, доступный браузеру пользователя; убедиться, что значение совпадает с `CORS_ORIGIN` на backend.
## Эксплуатация и сопровождение
### Автообновление
Updater опрашивает registry с интервалом `POLL_INTERVAL_SECONDS` (по умолчанию 300 с). Обновление привязано к смене digest у образа из `targets.json` (часто тег `:latest` после push в `main`).
### Ручное обновление
```bash
docker pull git.shts.su/denozord/mikrotikmanager-backend:latest
docker pull git.shts.su/denozord/mikrotikmanager-frontend:latest
```
Через compose (пересоздание при смене образа):
```bash
cd deploy
docker compose pull
docker compose up -d --force-recreate
```
Пинning версии: тег `:<commit-sha>` из CI вместо `:latest` в `image`, метке `mmapp.updater.image` и в `targets.json`.
### Откат
- **Автоматически:** updater при failed health после обновления — контейнер на `previous_digest`.
- **Вручную:** остановить контейнер, запустить образ с нужным тегом или digest из registry, сохранив те же volume и labels. Откат **схемы SQLite** updater не выполняет — нужен отдельный backup тома `backend-data` / файла `mikrotik.db`.
### Резервное копирование SQLite
Том `backend-data` (или `mmapp-backend-data` при явном `docker volume create`). Пример остановки backend для консистентной копии:
```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 logs mmapp-backend`, `docker logs mmapp-frontend`, `docker logs mmapp-updater` |
| Следить в реальном времени | `docker logs -f mmapp-updater` |
| Конфигурация контейнера | `docker inspect mmapp-backend` |
| Состояние updater | том `updater-state`, файл `/state/updater-state.json` внутри контейнера |
## Чеклист деплоя
1. Установить Docker Engine и Compose plugin; проверить `docker version` и `docker compose version`.
2. Открыть порты 3000 и 8000 (или обеспечить доступ клиентов к UI и API).
3. Выполнить `docker login git.shts.su`.
4. Склонировать репозиторий или скопировать `deploy/`.
5. Задать `CORS_ORIGIN` (origin фронтенда для браузера).
6. Задать `REGISTRY_USERNAME` и `REGISTRY_PASSWORD` для updater при приватном registry.
7. Создать `deploy/updater/targets.json` из example; настроить health URL для сети хоста.
8. Обновить bind-mount targets в `deploy/docker-compose.yml` на `./updater/targets.json`.
9. Выполнить `docker compose pull` и `docker compose up -d` в каталоге `deploy`.
10. Проверить `curl` на `:8000/health` и `:3000/`.
11. В UI: режим live и URL бэкенда; сверить с `CORS_ORIGIN`.
12. Просмотреть `docker logs mmapp-updater`; настроить регулярный backup тома `backend-data`.