# 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`** и **`:`**; платформа **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//-backend` | | `frontend-image` | `.ci/docker/frontend` | `Dockerfile.frontend` | `git.shts.su//-frontend` | | `updater-image` | `deploy/updater` | `deploy/updater/Dockerfile` | `git.shts.su//-updater` | - `` — первая часть `gitea.repository`, lower case. - `` — имя репозитория 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`). - Если после последнего тега нет новых коммитов, **релиз** пропускается; Docker-образы при этом всё равно собираются и публикуются с `:latest` и `:`. - Первый релиз без тега `v1.0.0` возможен автоматически: CI берёт историю `HEAD`, считает bump от `1.0.0` и создаёт тег (например `v1.1.0` при `feat:`). - Job **`publish-release`**: annotated tag `v1.2.3`, Gitea Release на `https://git.shts.su` (markdown notes), образы с тегами `:latest`, `:`, `:`. - 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); subject и body — **на русском**, префикс Conventional Commits — на английском. ## Прод-развёртывание 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= export REGISTRY_PASSWORD= ``` Скопировать и отредактировать конфиг 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= \ -e REGISTRY_PASSWORD= \ -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 ` → 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/`; при занятом 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 версии: тег `:` из 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`.