diff --git a/deploy/docker-compose.cdn-mm.yml b/deploy/docker-compose.cdn-mm.yml new file mode 100644 index 0000000..357b529 --- /dev/null +++ b/deploy/docker-compose.cdn-mm.yml @@ -0,0 +1,237 @@ +# 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 — from MikrotikManager: +# # https://git.shx.one/denozord/MikrotikManager/raw/branch/main/deploy/updater/targets.json.example +# 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/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/docs/Home.md b/docs/Home.md index 5d0d57e..9666479 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -9,6 +9,7 @@ Self-hosted панель управления DNS флота (ноды A/AAAA + - Не замена CFDM: там домены/сервисы/LB; здесь флот CHR и failover CNAME - SSO: [`integrate-auth-portal.md`](./integrate-auth-portal.md) - Docker + Traefik (prod): [`deploy-traefik.md`](./deploy-traefik.md) +- Docker CDN + MikrotikManager (один Traefik): [`../deploy/docker-compose.cdn-mm.yml`](../deploy/docker-compose.cdn-mm.yml) - Docker без Traefik: [`deploy-docker.md`](./deploy-docker.md) См. [README](../README.md) и [AGENTS.md](../AGENTS.md). diff --git a/docs/deploy-traefik.md b/docs/deploy-traefik.md index 7f77e08..5e0014b 100644 --- a/docs/deploy-traefik.md +++ b/docs/deploy-traefik.md @@ -12,6 +12,8 @@ | SSO | [`integrate-auth-portal.md`](./integrate-auth-portal.md) | | Релизы | [`releasing.md`](./releasing.md) | +**CDN + MikrotikManager на одном Traefik:** [`deploy/docker-compose.cdn-mm.yml`](../deploy/docker-compose.cdn-mm.yml) + [`deploy/env.cdn-mm.example`](../deploy/env.cdn-mm.example) — `cdn.shnt.top` и `mm.shnt.top`, каталог `/opt/cdn-mm`. + Документация Traefik: [Expose Docker](https://doc.traefik.io/traefik/expose/docker/basic/), [ACME DNS challenge](https://doc.traefik.io/traefik/https/acme/). ```