From 2697a35c0a719396fbe5fdaf6045cb9ca49c28fa Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 12 May 2026 14:46:52 +0700 Subject: [PATCH] chore: streamline Docker workflow and enhance build efficiency - Updated Dockerfiles for frontend and backend to include the --ignore-scripts flag in npm ci commands, further optimizing build times. - Added a step in the Docker workflow to create the public directory in the staging environment, ensuring proper setup for deployments. - Enhanced the configuration for dynamic image naming in the Docker workflow, improving consistency across builds. --- README.md | 485 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd2e3b0 --- /dev/null +++ b/README.md @@ -0,0 +1,485 @@ +# 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"}` на URL из секрета **`DEPLOY_WEBHOOK_URL`** после успешной сборки backend и frontend (updater в `needs` не входит). + +## Прод-развёртывание 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`.