diff --git a/README.md b/README.md index 53002d6..a4d080f 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,56 @@ npm --prefix backend run db:studio Стек с Traefik + HTTPS (Let's Encrypt DNS-01 / Cloudflare), по аналогии с CDNManager: [`deploy/docker-compose.traefik.yml`](deploy/docker-compose.traefik.yml) + [`deploy/env.traefik.example`](deploy/env.traefik.example). На сервере публикуются только `:80`/`:443`; frontend получает HTTPS, `/api` и `/health` проксируются на backend внутри сети `mmapp`. Домен по умолчанию: `mm.shnt.top`. -Рабочий каталог для команд compose — `deploy/` (или укажите `-f deploy/docker-compose.yml` / `-f deploy/docker-compose.traefik.yml` из корня репозитория). +### CDN Manager + MikrotikManager (один Traefik) + +Полный стек: Traefik + `cdn.shnt.top` + `mm.shnt.top` в одном Compose. + +| Файл | Назначение | +|------|------------| +| [`deploy/docker-compose.cdn-mm.yml`](deploy/docker-compose.cdn-mm.yml) | Traefik + CDN Manager + MM backend/frontend/updater | +| [`deploy/env.cdn-mm.example`](deploy/env.cdn-mm.example) | общий `.env` | + +```bash +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`](deploy/docker-compose.traefik-cdn.yml) | +| Plain `docker` CLI (скрипт) | [`deploy/run-beside-cdn-traefik.sh`](deploy/run-beside-cdn-traefik.sh) | + +Frontend вешается в сеть `cdnmanager` с Traefik-labels; backend/updater остаются в `mmapp`. Сертификат для `MM_DOMAIN` выпускает уже работающий Traefik CDNManager (тот же `letsencrypt` / Cloudflare DNS-01). + +```bash +# 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`. + +Рабочий каталог для команд 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` из корня). ### Прод-контейнеры diff --git a/deploy/docker-compose.cdn-mm.yml b/deploy/docker-compose.cdn-mm.yml new file mode 100644 index 0000000..425abc0 --- /dev/null +++ b/deploy/docker-compose.cdn-mm.yml @@ -0,0 +1,238 @@ +# CDN Manager + MikrotikManager + one Traefik (production). +# +# Hosts: +# https://cdn.shnt.top → cdnmanager:8080 +# https://mm.shnt.top → mmapp-frontend:3000 → backend:8000 (internal rewrite) +# +# On server: +# 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 # fill secrets +# # targets.json: +# # cp deploy/updater/targets.json.example /opt/cdn-mm/updater/targets.json +# # (в CDNManager-репо скачайте тот же файл из MikrotikManager) +# docker login git.shx.one +# cd /opt/cdn-mm && docker compose pull && docker compose up -d +# +# DNS (Cloudflare DNS only, grey cloud): +# A/AAAA cdn.shnt.top → VPS +# A/AAAA mm.shnt.top → VPS +# +# Do not run a second Traefik (standalone CDNManager or MikrotikManager compose) +# on the same host ports while this stack is up. + +services: + traefik: + image: traefik:${TRAEFIK_IMAGE_TAG:-v3.7} + container_name: cdn-mm-traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + ports: + - "${TRAEFIK_HTTP_PORT:-80}:80" + - "${TRAEFIK_HTTPS_PORT:-443}:443" + environment: + CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN:?set CF_DNS_API_TOKEN in .env} + # Optional if DNS token lacks Zone:Read: + # CF_ZONE_API_TOKEN: ${CF_ZONE_API_TOKEN:-} + command: + - --log.level=${TRAEFIK_LOG_LEVEL:-INFO} + - --api.dashboard=false + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --providers.docker.network=edge + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL:?set LETSENCRYPT_EMAIL in .env} + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.letsencrypt.acme.dnschallenge=true + - --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare + - --certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=15 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik_letsencrypt:/letsencrypt + networks: + - edge + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + # --- CDN Manager ----------------------------------------------------------- + cdnmanager: + # cdnmanager и cdn-manager — один образ (алиас для drop-in). + image: git.shx.one/denozord/cdnmanager:${CDN_IMAGE_TAG:-latest} + pull_policy: always + container_name: cdnmanager + restart: unless-stopped + depends_on: + - traefik + env_file: + - .env + environment: + DATABASE_URL: sqlite:/data/app.db + STATIC_DIR: /app/static + SERVER_PORT: "8080" + NODE_ENV: production + LOG_LEVEL: ${LOG_LEVEL:-info} + CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN:?set CLOUDFLARE_API_TOKEN in .env} + JWT_SECRET: ${JWT_SECRET:-} + AUTH_REQUIRED: ${AUTH_REQUIRED:-true} + AUTH_JWT_SECRET: ${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET in .env} + AUTH_ISSUER: ${AUTH_ISSUER:-https://auth.shnt.top} + AUTH_PORTAL_URL: ${AUTH_PORTAL_URL:-https://auth.shnt.top} + AUTH_AUDIT_INGEST_SECRET: ${AUTH_AUDIT_INGEST_SECRET:-} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + ADMIN_PASSWORD_HASH: ${ADMIN_PASSWORD_HASH:-} + volumes: + - ./data/cdn:/data + networks: + - edge + labels: + - traefik.enable=true + - traefik.docker.network=edge + - traefik.http.routers.cdnmanager.rule=Host(`${CDN_DOMAIN:-cdn.shnt.top}`) + - traefik.http.routers.cdnmanager.entrypoints=websecure + - traefik.http.routers.cdnmanager.tls=true + - traefik.http.routers.cdnmanager.tls.certresolver=letsencrypt + - traefik.http.services.cdnmanager.loadbalancer.server.port=8080 + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + # --- MikrotikManager ------------------------------------------------------- + backend: + image: git.shx.one/denozord/mikrotikmanager-backend:${MM_BACKEND_IMAGE_TAG:-latest} + pull_policy: always + container_name: mmapp-backend + restart: unless-stopped + depends_on: + - traefik + environment: + NODE_ENV: production + PORT: "8000" + DATABASE_PATH: /app/data/mikrotik.db + CORS_ORIGIN: ${CORS_ORIGIN:-https://mm.shnt.top} + volumes: + - ./data/mm:/app/data + networks: + mmapp: + aliases: + - backend + labels: + mmapp.updater.managed: "true" + mmapp.updater.target: backend + mmapp.updater.image: git.shx.one/denozord/mikrotikmanager-backend:${MM_BACKEND_IMAGE_TAG:-latest} + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:8000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + frontend: + image: git.shx.one/denozord/mikrotikmanager-frontend:${MM_FRONTEND_IMAGE_TAG:-latest} + pull_policy: always + container_name: mmapp-frontend + restart: unless-stopped + depends_on: + - backend + environment: + BACKEND_INTERNAL_URL: http://backend:8000 + networks: + mmapp: + aliases: + - frontend + edge: {} + labels: + - mmapp.updater.managed=true + - mmapp.updater.target=frontend + - mmapp.updater.image=git.shx.one/denozord/mikrotikmanager-frontend:${MM_FRONTEND_IMAGE_TAG:-latest} + - traefik.enable=true + - traefik.docker.network=edge + - traefik.http.routers.mmapp.rule=Host(`${MM_DOMAIN:-mm.shnt.top}`) + - traefik.http.routers.mmapp.entrypoints=websecure + - traefik.http.routers.mmapp.tls=true + - traefik.http.routers.mmapp.tls.certresolver=letsencrypt + - traefik.http.services.mmapp.loadbalancer.server.port=3000 + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:3000/dashboard').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 25s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + updater: + image: git.shx.one/denozord/mikrotikmanager-updater:${MM_UPDATER_IMAGE_TAG:-latest} + pull_policy: always + container_name: mmapp-updater + restart: unless-stopped + depends_on: + - frontend + environment: + REGISTRY: git.shx.one + REGISTRY_USERNAME: ${REGISTRY_USERNAME:-} + REGISTRY_PASSWORD: ${REGISTRY_PASSWORD:-} + POLL_INTERVAL_SECONDS: ${POLL_INTERVAL_SECONDS:-300} + HEALTH_TIMEOUT_SECONDS: ${HEALTH_TIMEOUT_SECONDS:-120} + STOP_TIMEOUT_SECONDS: ${STOP_TIMEOUT_SECONDS:-30} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./state:/state + - ./updater/targets.json:/etc/updater/targets.json:ro + networks: + - mmapp + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +volumes: + traefik_letsencrypt: + name: cdn_mm_traefik_letsencrypt + +networks: + edge: + name: edge + mmapp: + name: mmapp diff --git a/deploy/docker-compose.traefik-cdn.yml b/deploy/docker-compose.traefik-cdn.yml new file mode 100644 index 0000000..1dccd40 --- /dev/null +++ b/deploy/docker-compose.traefik-cdn.yml @@ -0,0 +1,134 @@ +# MikrotikManager behind an existing CDNManager Traefik (no second Traefik). +# +# Prerequisite: CDNManager stack is up — network `cdnmanager` and container +# `cdnmanager-traefik` already publish :80/:443 and watch Docker labels. +# +# On server: +# mkdir -p /opt/mmapp/data /opt/mmapp/state /opt/mmapp/updater +# cp deploy/docker-compose.traefik-cdn.yml /opt/mmapp/docker-compose.yml +# cp deploy/env.traefik.example /opt/mmapp/.env +# # set MM_DOMAIN / CORS_ORIGIN; CF_* / LETSENCRYPT_* not required here +# cp deploy/updater/targets.json.example /opt/mmapp/updater/targets.json +# docker login git.shx.one +# cd /opt/mmapp && docker compose pull && docker compose up -d +# +# Equivalent plain CLI: deploy/run-beside-cdn-traefik.sh +# +# Traffic: +# Internet → CDNManager Traefik (:80/:443) → mmapp-frontend:3000 (network cdnmanager) +# └─ rewrite /api,/health → backend:8000 (network mmapp) + +services: + backend: + image: git.shx.one/denozord/mikrotikmanager-backend:${MM_BACKEND_IMAGE_TAG:-latest} + pull_policy: always + container_name: mmapp-backend + restart: unless-stopped + environment: + NODE_ENV: production + PORT: "8000" + DATABASE_PATH: /app/data/mikrotik.db + CORS_ORIGIN: ${CORS_ORIGIN:-https://mm.shnt.top} + volumes: + - ./data:/app/data + networks: + mmapp: + aliases: + - backend + labels: + mmapp.updater.managed: "true" + mmapp.updater.target: backend + mmapp.updater.image: git.shx.one/denozord/mikrotikmanager-backend:${MM_BACKEND_IMAGE_TAG:-latest} + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:8000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + frontend: + image: git.shx.one/denozord/mikrotikmanager-frontend:${MM_FRONTEND_IMAGE_TAG:-latest} + pull_policy: always + container_name: mmapp-frontend + restart: unless-stopped + depends_on: + - backend + environment: + BACKEND_INTERNAL_URL: http://backend:8000 + networks: + mmapp: + aliases: + - frontend + cdnmanager: {} + labels: + - mmapp.updater.managed=true + - mmapp.updater.target=frontend + - mmapp.updater.image=git.shx.one/denozord/mikrotikmanager-frontend:${MM_FRONTEND_IMAGE_TAG:-latest} + - traefik.enable=true + - traefik.docker.network=cdnmanager + - traefik.http.routers.mmapp.rule=Host(`${MM_DOMAIN:-mm.shnt.top}`) + - traefik.http.routers.mmapp.entrypoints=websecure + - traefik.http.routers.mmapp.tls=true + - traefik.http.routers.mmapp.tls.certresolver=letsencrypt + - traefik.http.services.mmapp.loadbalancer.server.port=3000 + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:3000/dashboard').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 25s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + updater: + image: git.shx.one/denozord/mikrotikmanager-updater:${MM_UPDATER_IMAGE_TAG:-latest} + pull_policy: always + container_name: mmapp-updater + restart: unless-stopped + depends_on: + - frontend + environment: + REGISTRY: git.shx.one + REGISTRY_USERNAME: ${REGISTRY_USERNAME:-} + REGISTRY_PASSWORD: ${REGISTRY_PASSWORD:-} + POLL_INTERVAL_SECONDS: ${POLL_INTERVAL_SECONDS:-300} + HEALTH_TIMEOUT_SECONDS: ${HEALTH_TIMEOUT_SECONDS:-120} + STOP_TIMEOUT_SECONDS: ${STOP_TIMEOUT_SECONDS:-30} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./state:/state + - ./updater/targets.json:/etc/updater/targets.json:ro + networks: + - mmapp + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +networks: + mmapp: + name: mmapp + cdnmanager: + external: true + name: cdnmanager diff --git a/deploy/env.cdn-mm.example b/deploy/env.cdn-mm.example new file mode 100644 index 0000000..19ba98c --- /dev/null +++ b/deploy/env.cdn-mm.example @@ -0,0 +1,52 @@ +# Production .env for deploy/docker-compose.cdn-mm.yml +# (CDN Manager + MikrotikManager + one Traefik). +# Copy to /opt/cdn-mm/.env and fill secrets. Do not commit. + +# --- Traefik / Let's Encrypt (Cloudflare DNS-01) --- +# Token for ACME only (Zone DNS Edit). Separate from CLOUDFLARE_API_TOKEN below. +CF_DNS_API_TOKEN= +LETSENCRYPT_EMAIL=admin@shnt.top +# TRAEFIK_IMAGE_TAG=v3.7 +# TRAEFIK_HTTP_PORT=80 +# TRAEFIK_HTTPS_PORT=443 +# TRAEFIK_LOG_LEVEL=INFO + +# --- Public hosts --- +CDN_DOMAIN=cdn.shnt.top +MM_DOMAIN=mm.shnt.top +# Must match MM UI origin (https:// + MM_DOMAIN). +CORS_ORIGIN=https://mm.shnt.top + +# --- Images --- +CDN_IMAGE_TAG=latest +# drop-in alias (same manifest): git.shx.one/denozord/cdn-manager +MM_BACKEND_IMAGE_TAG=latest +MM_FRONTEND_IMAGE_TAG=latest +MM_UPDATER_IMAGE_TAG=latest + +# --- CDN Manager --- +CLOUDFLARE_API_TOKEN= +LOG_LEVEL=info +NODE_ENV=production + +# Portal SSO (prod default AUTH_REQUIRED=true) +AUTH_REQUIRED=true +# Same HS256 secret as auth-portal JWT_SECRET (required) +AUTH_JWT_SECRET= +# Optional alias — CDN Manager also reads JWT_SECRET +JWT_SECRET= +AUTH_ISSUER=https://auth.shnt.top +AUTH_PORTAL_URL=https://auth.shnt.top +# Shared with auth-portal AUDIT_INGEST_SECRET (optional) +AUTH_AUDIT_INGEST_SECRET= + +# Legacy local admin — only when AUTH_REQUIRED=false +ADMIN_USERNAME=admin +ADMIN_PASSWORD_HASH= + +# --- MikrotikManager updater (optional; private registry pull) --- +REGISTRY_USERNAME= +REGISTRY_PASSWORD= +# POLL_INTERVAL_SECONDS=300 +# HEALTH_TIMEOUT_SECONDS=120 +# STOP_TIMEOUT_SECONDS=30 diff --git a/deploy/env.traefik.example b/deploy/env.traefik.example index e04cf59..857fe3f 100644 --- a/deploy/env.traefik.example +++ b/deploy/env.traefik.example @@ -1,13 +1,18 @@ -# Production .env for deploy/docker-compose.traefik.yml (MikrotikManager + Traefik). -# Copy to /opt/mmapp/.env and fill secrets. Do not commit. +# Production .env for MikrotikManager Traefik deploys. Do not commit. +# Use with: +# deploy/docker-compose.traefik.yml — own Traefik (standalone) +# deploy/docker-compose.traefik-cdn.yml — reuse CDNManager Traefik (network cdnmanager) +# deploy/run-beside-cdn-traefik.sh — plain docker CLI beside CDNManager -# --- Traefik / Let's Encrypt (Cloudflare DNS-01) --- -# Token for ACME only (Zone DNS Edit). Not used by the app itself. -CF_DNS_API_TOKEN= -LETSENCRYPT_EMAIL=admin@shnt.top +# --- Public host --- MM_DOMAIN=mm.shnt.top # Must match the public HTTPS origin of the UI (same as MM_DOMAIN with https://). CORS_ORIGIN=https://mm.shnt.top + +# --- Traefik / Let's Encrypt (only for docker-compose.traefik.yml standalone) --- +# Not required when attaching to CDNManager Traefik (traefik-cdn / run-beside script). +CF_DNS_API_TOKEN= +LETSENCRYPT_EMAIL=admin@shnt.top # TRAEFIK_IMAGE_TAG=v3.7 # TRAEFIK_HTTP_PORT=80 # TRAEFIK_HTTPS_PORT=443 diff --git a/deploy/run-beside-cdn-traefik.sh b/deploy/run-beside-cdn-traefik.sh new file mode 100644 index 0000000..ec6e26b --- /dev/null +++ b/deploy/run-beside-cdn-traefik.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# Run MikrotikManager beside an already-running CDNManager Traefik stack. +# Does NOT start a second Traefik — attaches frontend to network `cdnmanager`. +# +# Usage (on the VPS): +# 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 /tmp/run-beside-cdn-traefik.sh +# +# Or copy this file to the server and run it. +# +# Env overrides: +# MM_DOMAIN=mm.shnt.top +# CORS_ORIGIN=https://mm.shnt.top +# MM_ROOT=/opt/mmapp +# MM_BACKEND_IMAGE_TAG=latest +# MM_FRONTEND_IMAGE_TAG=latest +# MM_UPDATER_IMAGE_TAG=latest +# REGISTRY_USERNAME=… REGISTRY_PASSWORD=… # optional private pull + +set -euo pipefail + +MM_DOMAIN="${MM_DOMAIN:-mm.shnt.top}" +CORS_ORIGIN="${CORS_ORIGIN:-https://${MM_DOMAIN}}" +MM_ROOT="${MM_ROOT:-/opt/mmapp}" +MM_BACKEND_IMAGE_TAG="${MM_BACKEND_IMAGE_TAG:-latest}" +MM_FRONTEND_IMAGE_TAG="${MM_FRONTEND_IMAGE_TAG:-latest}" +MM_UPDATER_IMAGE_TAG="${MM_UPDATER_IMAGE_TAG:-latest}" +REGISTRY="${REGISTRY:-git.shx.one}" + +BACKEND_IMAGE="${REGISTRY}/denozord/mikrotikmanager-backend:${MM_BACKEND_IMAGE_TAG}" +FRONTEND_IMAGE="${REGISTRY}/denozord/mikrotikmanager-frontend:${MM_FRONTEND_IMAGE_TAG}" +UPDATER_IMAGE="${REGISTRY}/denozord/mikrotikmanager-updater:${MM_UPDATER_IMAGE_TAG}" + +echo "==> Check CDNManager Traefik network" +if ! docker network inspect cdnmanager >/dev/null 2>&1; then + echo "ERROR: Docker network 'cdnmanager' not found." >&2 + echo "Start CDNManager Traefik stack first (deploy/docker-compose.traefik.yml)." >&2 + exit 1 +fi +if ! docker inspect cdnmanager-traefik >/dev/null 2>&1; then + echo "WARN: container 'cdnmanager-traefik' not found — labels may not be routed." >&2 +fi + +echo "==> Prepare dirs under ${MM_ROOT}" +mkdir -p "${MM_ROOT}/data" "${MM_ROOT}/state" "${MM_ROOT}/updater" + +TARGETS="${MM_ROOT}/updater/targets.json" +if [[ ! -f "${TARGETS}" ]]; then + cat >"${TARGETS}" <<'EOF' +{ + "targets": [ + { + "id": "backend", + "container_name": "mmapp-backend", + "image": "git.shx.one/denozord/mikrotikmanager-backend:latest", + "health": { + "type": "http", + "url": "http://backend:8000/health", + "expect_status": 200 + } + }, + { + "id": "frontend", + "container_name": "mmapp-frontend", + "image": "git.shx.one/denozord/mikrotikmanager-frontend:latest", + "health": { + "type": "http", + "url": "http://frontend:3000/dashboard", + "expect_status": 200 + } + } + ] +} +EOF + echo " wrote ${TARGETS}" +fi + +echo "==> Ensure internal network mmapp" +docker network inspect mmapp >/dev/null 2>&1 || docker network create mmapp >/dev/null + +if [[ -n "${REGISTRY_USERNAME:-}" && -n "${REGISTRY_PASSWORD:-}" ]]; then + echo "==> docker login ${REGISTRY}" + echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY}" -u "${REGISTRY_USERNAME}" --password-stdin +fi + +echo "==> Pull images" +docker pull "${BACKEND_IMAGE}" +docker pull "${FRONTEND_IMAGE}" +docker pull "${UPDATER_IMAGE}" + +stop_rm() { + local name="$1" + if docker inspect "${name}" >/dev/null 2>&1; then + docker stop "${name}" >/dev/null || true + docker rm "${name}" >/dev/null || true + fi +} + +echo "==> Recreate mmapp-backend" +stop_rm mmapp-backend +docker run -d \ + --name mmapp-backend \ + --restart unless-stopped \ + --network mmapp \ + --network-alias backend \ + -e NODE_ENV=production \ + -e PORT=8000 \ + -e DATABASE_PATH=/app/data/mikrotik.db \ + -e "CORS_ORIGIN=${CORS_ORIGIN}" \ + -v "${MM_ROOT}/data:/app/data" \ + --label mmapp.updater.managed=true \ + --label mmapp.updater.target=backend \ + --label "mmapp.updater.image=${BACKEND_IMAGE}" \ + --health-cmd="node -e \"fetch('http://127.0.0.1:8000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"" \ + --health-interval=30s \ + --health-timeout=5s \ + --health-retries=3 \ + --health-start-period=15s \ + --log-driver json-file \ + --log-opt max-size=10m \ + --log-opt max-file=3 \ + "${BACKEND_IMAGE}" + +echo "==> Recreate mmapp-frontend (mmapp + cdnmanager)" +stop_rm mmapp-frontend +docker run -d \ + --name mmapp-frontend \ + --restart unless-stopped \ + --network mmapp \ + --network-alias frontend \ + -e BACKEND_INTERNAL_URL=http://backend:8000 \ + --label mmapp.updater.managed=true \ + --label mmapp.updater.target=frontend \ + --label "mmapp.updater.image=${FRONTEND_IMAGE}" \ + --label traefik.enable=true \ + --label traefik.docker.network=cdnmanager \ + --label "traefik.http.routers.mmapp.rule=Host(\`${MM_DOMAIN}\`)" \ + --label traefik.http.routers.mmapp.entrypoints=websecure \ + --label traefik.http.routers.mmapp.tls=true \ + --label traefik.http.routers.mmapp.tls.certresolver=letsencrypt \ + --label traefik.http.services.mmapp.loadbalancer.server.port=3000 \ + --health-cmd="node -e \"fetch('http://127.0.0.1:3000/dashboard').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"" \ + --health-interval=30s \ + --health-timeout=5s \ + --health-retries=3 \ + --health-start-period=25s \ + --log-driver json-file \ + --log-opt max-size=10m \ + --log-opt max-file=3 \ + "${FRONTEND_IMAGE}" + +docker network connect cdnmanager mmapp-frontend + +echo "==> Recreate mmapp-updater" +stop_rm mmapp-updater +docker run -d \ + --name mmapp-updater \ + --restart unless-stopped \ + --network mmapp \ + -e "REGISTRY=${REGISTRY}" \ + -e "REGISTRY_USERNAME=${REGISTRY_USERNAME:-}" \ + -e "REGISTRY_PASSWORD=${REGISTRY_PASSWORD:-}" \ + -e "POLL_INTERVAL_SECONDS=${POLL_INTERVAL_SECONDS:-300}" \ + -e "HEALTH_TIMEOUT_SECONDS=${HEALTH_TIMEOUT_SECONDS:-120}" \ + -e "STOP_TIMEOUT_SECONDS=${STOP_TIMEOUT_SECONDS:-30}" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "${MM_ROOT}/state:/state" \ + -v "${TARGETS}:/etc/updater/targets.json:ro" \ + --log-driver json-file \ + --log-opt max-size=10m \ + --log-opt max-file=3 \ + "${UPDATER_IMAGE}" + +echo +echo "OK. UI: https://${MM_DOMAIN}" +echo "DNS: A/AAAA for ${MM_DOMAIN} → this VPS, Cloudflare proxy OFF (DNS only)." +echo "Check: curl -fsS https://${MM_DOMAIN}/health" +echo " docker ps --filter name=mmapp-" +echo " docker network inspect cdnmanager --format '{{range .Containers}}{{.Name}} {{end}}'"