chore: streamline Docker workflow and enhance build efficiency
Docker images / backend-image (push) Successful in 40s
Docker images / frontend-image (push) Successful in 39s
Docker images / updater-image (push) Successful in 36s
Docker images / notify-webhook (push) Has been skipped

- 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.
This commit is contained in:
Denozordec
2026-05-12 14:46:52 +07:00
parent 7295545cde
commit 2697a35c0a
+485
View File
@@ -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`** и **`:<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"}` на 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=<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`.