JWT на backend, handoff/callback на UI, RBAC mm:*, AUTH_* в compose. Co-authored-by: Cursor <[email protected]>
MikrotikManager-3
Монорепозиторий веб-приложения для управления MikroTik: UI на Next.js, API на Fastify, общие Zod-контракты, сборка Docker-образов через Gitea Actions и автоматическое обновление контейнеров на сервере через sidecar updater.
Содержание
- Состав монорепозитория
- Архитектура
- Локальная разработка
- CI/CD (Gitea Actions)
- Прод-развёртывание Docker
- Сервис updater
- Первичная настройка сервера
- Эксплуатация и сопровождение
- Чеклист деплоя
Состав монорепозитория
| Компонент | Путь | Стек | Порт (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.shx.one |
Имена образов в registry для репозитория denozord/MikrotikManager-3:
git.shx.one/denozord/mikrotikmanager-backendgit.shx.one/denozord/mikrotikmanager-frontendgit.shx.one/denozord/mikrotikmanager-updater
Для другого owner/repo подставьте имя по правилу CI (см. CI/CD).
Архитектура
Общая схема
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.shx.one 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 → сервер
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):
npm install
npm run dev
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
npm run build -w @mmapp/contracts
После изменения схем Drizzle:
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.shx.one/<owner>/<stem>-backend |
frontend-image |
.ci/docker/frontend |
Dockerfile.frontend |
git.shx.one/<owner>/<stem>-frontend |
updater-image |
deploy/updater |
deploy/updater/Dockerfile |
git.shx.one/<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(major2.xвне scope). - Job
prepare-releaseзапускает.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и:<sha>. - Первый релиз без тега
v1.0.0возможен автоматически: CI берёт историюHEAD, считает bump от1.0.0и создаёт тег (напримерv1.1.0приfeat:). - Job
publish-release: annotated tagv1.2.3, Gitea Release наhttps://git.shx.one(markdown notes), образы с тегами:latest,:<sha>,:<semver>. - UI: версия в sidebar и страница
/releases; manifestpublic/release-manifest.json(в CI подставляется из артефакта). - Bootstrap: один раз выставить
1.0.0в workspacepackage.jsonи создать тегv1.0.0наmainперед первым автоматическим bump. - Сообщения коммитов: см.
.cursor/rules/release-versioning.mdc; subject и body — на русском, префикс Conventional Commits — на английском.
Прод-развёртывание Docker
Эталон без reverse-proxy: deploy/docker-compose.yml (порты 3000 / 8000 на хост).
Стек с Traefik + HTTPS (Let's Encrypt DNS-01 / Cloudflare), по аналогии с CDNManager: deploy/docker-compose.traefik.yml + deploy/env.traefik.example. На сервере публикуются только :80/:443; frontend получает HTTPS, /api и /health проксируются на backend внутри сети mmapp. Домен по умолчанию: mm.shnt.top.
CDN Manager + MikrotikManager (один Traefik)
Полный стек: Traefik + cdn.shnt.top + mm.shnt.top в одном Compose.
| Файл | Назначение |
|---|---|
deploy/docker-compose.cdn-mm.yml |
Traefik + CDN Manager + MM backend/frontend/updater |
deploy/env.cdn-mm.example |
общий .env |
mkdir -p /opt/cdn-mm/{data/cdn,data/mm,state,updater}
cp deploy/docker-compose.cdn-mm.yml /opt/cdn-mm/docker-compose.yml
cp deploy/env.cdn-mm.example /opt/cdn-mm/.env
cp deploy/updater/targets.json.example /opt/cdn-mm/updater/targets.json
# заполнить CF_DNS_API_TOKEN, CLOUDFLARE_API_TOKEN, AUTH_JWT_SECRET, CORS_ORIGIN, …
docker login git.shx.one
cd /opt/cdn-mm && docker compose pull && docker compose up -d
curl -fsS https://cdn.shnt.top/health
curl -fsS https://mm.shnt.top/health
Данные: ./data/cdn (CDN), ./data/mm (MM). Сеть Traefik: edge. Не запускайте параллельно standalone docker-compose.traefik.yml CDNManager или MM на тех же 80/443.
Если на сервере уже крутится CDNManager Traefik (cdnmanager-traefik, сеть cdnmanager) — не поднимайте второй Traefik. Варианты:
| Способ | Файл |
|---|---|
| Compose без своего Traefik | deploy/docker-compose.traefik-cdn.yml |
Plain docker CLI (скрипт) |
deploy/run-beside-cdn-traefik.sh |
Frontend вешается в сеть cdnmanager с Traefik-labels; backend/updater остаются в mmapp. Сертификат для MM_DOMAIN выпускает уже работающий Traefik CDNManager (тот же letsencrypt / Cloudflare DNS-01).
# Compose (рекомендуется)
mkdir -p /opt/mmapp/{data,state,updater}
cp deploy/docker-compose.traefik-cdn.yml /opt/mmapp/docker-compose.yml
cp deploy/env.traefik.example /opt/mmapp/.env # MM_DOMAIN + CORS_ORIGIN
cp deploy/updater/targets.json.example /opt/mmapp/updater/targets.json
cd /opt/mmapp && docker compose pull && docker compose up -d
# Или одной CLI-командой (скрипт сам сделает network/pull/run/connect):
curl -fsSL -o /tmp/run-beside-cdn-traefik.sh \
https://git.shx.one/denozord/MikrotikManager/raw/branch/main/deploy/run-beside-cdn-traefik.sh
chmod +x /tmp/run-beside-cdn-traefik.sh
sudo MM_DOMAIN=mm.shnt.top CORS_ORIGIN=https://mm.shnt.top /tmp/run-beside-cdn-traefik.sh
DNS: A/AAAA для mm.shnt.top → IP VPS, Cloudflare DNS only. Проверка: curl -fsS https://mm.shnt.top/health.
SSO auth-portal: docs/integrate-auth-portal.md (app id mm).
Рабочий каталог для команд compose — deploy/ (или -f deploy/docker-compose.yml / -f deploy/docker-compose.traefik.yml / -f deploy/docker-compose.traefik-cdn.yml / -f deploy/docker-compose.cdn-mm.yml из корня).
Прод-контейнеры
| Сервис | container_name |
Образ (пример) | Порты host:container | Тома | restart |
|---|---|---|---|---|---|
| backend | mmapp-backend |
git.shx.one/denozord/mikrotikmanager-backend:latest |
8000:8000 |
backend-data → /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 |
Метки для 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.shx.one |
registry для docker login |
REGISTRY_USERNAME |
пусто | логин (если заданы оба с паролем) |
REGISTRY_PASSWORD |
пусто | пароль registry |
Docker Compose
docker login git.shx.one
export CORS_ORIGIN=http://<хост>:3000
export REGISTRY_USERNAME=<user>
export REGISTRY_PASSWORD=<token>
Скопировать и отредактировать конфиг updater (health URL должны быть достижимы из контейнера updater):
cp deploy/updater/targets.json.example deploy/updater/targets.json
В deploy/docker-compose.yml для updater заменить bind-mount на рабочий файл:
- ./updater/targets.json:/etc/updater/targets.json:ro
Запуск:
cd deploy
docker compose pull
docker compose up -d
Эквивалентные docker run
Имена томов можно согласовать с compose (backend-data, updater-state) или задать явно.
Backend:
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.shx.one/denozord/mikrotikmanager-backend:latest \
git.shx.one/denozord/mikrotikmanager-backend:latest
Frontend:
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.shx.one/denozord/mikrotikmanager-frontend:latest \
git.shx.one/denozord/mikrotikmanager-frontend:latest
Updater:
docker volume create updater-state
docker run -d \
--name mmapp-updater \
--restart unless-stopped \
-e REGISTRY=git.shx.one \
-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.shx.one/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 deploy/updater/validate.sh
Чеклист сценариев на staging: deploy/updater/test-staging.sh.
Первичная настройка сервера
Пошагово на чистом Linux-хосте с доступом в интернет и к git.shx.one.
- Установить Docker Engine и плагин Compose (официальная документация Docker для вашего дистрибутива).
- Проверить:
docker version
docker compose version
- Открыть входящие порты 3000 (frontend) и 8000 (backend) в firewall или настроить внешний доступ согласно вашей схеме (reverse proxy в репозитории не описан).
- Войти в registry:
docker login git.shx.one
- Получить файлы деплоя: клонировать репозиторий или скопировать каталог
deploy/и подготовитьdeploy/updater/targets.json. - Задать переменные окружения для compose (
CORS_ORIGIN,REGISTRY_USERNAME,REGISTRY_PASSWORDпри приватных образах). - Подтянуть образы и запустить стек:
cd deploy
docker compose pull
docker compose up -d
- Проверить health (см. таблицу выше) и логи updater:
docker logs mmapp-updater --tail 50
- В UI включить режим live и указать URL бэкенда, доступный браузеру пользователя; убедиться, что значение совпадает с
CORS_ORIGINна backend.
Эксплуатация и сопровождение
Автообновление
Updater опрашивает registry с интервалом POLL_INTERVAL_SECONDS (по умолчанию 300 с). Обновление привязано к смене digest у образа из targets.json (часто тег :latest после push в main).
Ручное обновление
docker pull git.shx.one/denozord/mikrotikmanager-backend:latest
docker pull git.shx.one/denozord/mikrotikmanager-frontend:latest
Через compose (пересоздание при смене образа):
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 для консистентной копии:
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 внутри контейнера |
Чеклист деплоя
- Установить Docker Engine и Compose plugin; проверить
docker versionиdocker compose version. - Открыть порты 3000 и 8000 (или обеспечить доступ клиентов к UI и API).
- Выполнить
docker login git.shx.one. - Склонировать репозиторий или скопировать
deploy/. - Задать
CORS_ORIGIN(origin фронтенда для браузера). - Задать
REGISTRY_USERNAMEиREGISTRY_PASSWORDдля updater при приватном registry. - Создать
deploy/updater/targets.jsonиз example; настроить health URL для сети хоста. - Обновить bind-mount targets в
deploy/docker-compose.ymlна./updater/targets.json. - Выполнить
docker compose pullиdocker compose up -dв каталогеdeploy. - Проверить
curlна:8000/healthи:3000/. - В UI: режим live и URL бэкенда; сверить с
CORS_ORIGIN. - Просмотреть
docker logs mmapp-updater; настроить регулярный backup томаbackend-data.