# Production stack для /opt/evobgp/docker-compose.yaml # Скопируйте на сервер: # scp deploy/compose/docker-compose.production.example.yaml root@host:/opt/evobgp/docker-compose.yaml # scp deploy/compose/.env.production.example root@host:/opt/evobgp/.env # # Запуск: # cd /opt/evobgp # mkdir -p runtime-logs # docker login git.shx.one # docker compose pull # docker compose up -d # # Runtime logs API (/v1/runtime-logs/*): evobgp-all + stack-runtime-logs делят каталог # EVOBGP_RUNTIME_LOGS_HOST_DIR на хосте (default /opt/evobgp/runtime-logs). # # Postgres: shm_size + start_period — иначе часто «dependency postgres failed» на слабом хосте. # Web UI: WEBUI_DOMAIN, WEBUI_IP_WHITELIST, LETSENCRYPT_EMAIL, CF_DNS_API_TOKEN в .env # Cloudflare: DNS only (серый облачок) для WEBUI_DOMAIN. # # ACME: том traefik_letsencrypt.name зафиксирован — не `docker compose down -v` без бэкапа. # COMPOSE_PROJECT_NAME должен совпадать с label com.docker.compose.project стека. name: evobgp-microvps-full configs: prometheus_yml: content: | global: scrape_interval: 15s evaluation_interval: 15s rule_files: - /etc/prometheus/alerts.yml scrape_configs: - job_name: evobgp-all metrics_path: /metrics static_configs: - targets: ["evobgp-all:8080"] prometheus_alerts_yml: content: | groups: [] services: postgres: image: postgres:16-alpine restart: unless-stopped shm_size: "256mb" environment: POSTGRES_USER: evobgp POSTGRES_PASSWORD: evobgp POSTGRES_DB: evobgp volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U evobgp -d evobgp"] interval: 10s timeout: 5s retries: 12 start_period: 45s logging: driver: json-file options: max-size: "10m" max-file: "3" nats: image: nats:2.10-alpine restart: unless-stopped command: ["-js", "-m", "8222"] ports: - "4222:4222" logging: driver: json-file options: max-size: "10m" max-file: "3" bird2: image: ${EVOBGP_REGISTRY:-git.shx.one/denozord}/evobgp-bird2:${EVOBGP_IMAGE_TAG:-latest} restart: unless-stopped cap_add: - NET_ADMIN sysctls: net.ipv4.ip_forward: "1" net.ipv6.conf.all.forwarding: "1" volumes: - bird_etc:/etc/bird - bird_run:/run/bird ports: - "179:179/tcp" logging: driver: json-file options: max-size: "10m" max-file: "3" evobgp-agent: image: ${EVOBGP_REGISTRY:-git.shx.one/denozord}/evobgp-agent:${EVOBGP_IMAGE_TAG:-latest} restart: unless-stopped depends_on: bird2: condition: service_started cap_add: - NET_ADMIN volumes: - bird_etc:/etc/bird - bird_run:/run/bird entrypoint: ["/usr/local/bin/evobgp-agent"] command: ["watch", "-socket=/run/bird/bird.ctl", "-watch-interval=30s"] logging: driver: json-file options: max-size: "10m" max-file: "3" evobgp-all: image: ${EVOBGP_REGISTRY:-git.shx.one/denozord}/evobgp-all:${EVOBGP_IMAGE_TAG:-latest} restart: unless-stopped depends_on: postgres: condition: service_healthy nats: condition: service_started bird2: condition: service_started ports: - "8080:8080" environment: EVOBGP_DATABASE_URL: postgres://evobgp:evobgp@postgres:5432/evobgp?sslmode=disable # EVOBGP_BROKER_URL: nats://nats:4222 EVOBGP_NODE_DISPATCH_ENABLED: "1" EVOBGP_BUNDLE_SEED_HEX: "bd8fbcd31545aacfdd228203beca8e945ab9a752f2ce5624cf42d9f316389a9d" EVOBGP_HTTP_ADDR: ":8080" EVOBGP_SEED_DEMO: "1" EVOBGP_BIRDC_SOCKET: /run/bird/bird.ctl EVOBGP_BIRDC_INTERVAL: 30s EVOBGP_BIRD_ACTIVE_DIR: /etc/bird EVOBGP_BIRD_STAGING_DIR: /tmp/evobgp-bird-staging EVOBGP_SERVICE: evobgp-all EVOBGP_RUNTIME_LOGS_DIR: /opt/evobgp/runtime-logs AUTH_REQUIRED: ${AUTH_REQUIRED:-false} AUTH_JWT_SECRET: ${AUTH_JWT_SECRET:-} AUTH_ISSUER: ${AUTH_ISSUER:-https://auth.shnt.top} AUTH_PORTAL_URL: ${AUTH_PORTAL_URL:-} EVOBGP_PORTAL_TENANT_ID: ${EVOBGP_PORTAL_TENANT_ID:-} EVOBGP_DEV_INSECURE: "1" volumes: - bird_etc:/etc/bird - bird_run:/run/bird:ro - type: bind source: ${EVOBGP_RUNTIME_LOGS_HOST_DIR:-/opt/evobgp/runtime-logs} target: /opt/evobgp/runtime-logs bind: create_host_path: true logging: driver: json-file options: max-size: "10m" max-file: "3" evobgp-web: image: ${EVOBGP_REGISTRY:-git.shx.one/denozord}/evobgp-web-all:${EVOBGP_IMAGE_TAG:-latest} restart: unless-stopped depends_on: evobgp-all: condition: service_started labels: - traefik.enable=true # Публичные firewall-эндпоинты — без WEBUI_IP_WHITELIST (установка с произвольных серверов). - traefik.http.routers.evobgp-firewall-public.rule=Host(`${WEBUI_DOMAIN}`) && (Path(`/v1/firewall/install.sh`) || Path(`/v1/firewall/sync-script`) || PathPrefix(`/v1/firewall/enroll`)) - traefik.http.routers.evobgp-firewall-public.entrypoints=websecure - traefik.http.routers.evobgp-firewall-public.tls=true - traefik.http.routers.evobgp-firewall-public.tls.certresolver=letsencrypt - traefik.http.routers.evobgp-firewall-public.priority=100 - traefik.http.routers.evobgp-firewall-public.service=evobgp-web - traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`) - traefik.http.routers.evobgp-web.entrypoints=websecure - traefik.http.routers.evobgp-web.tls=true - traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt - traefik.http.routers.evobgp-web.priority=10 - traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker - traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST} - traefik.http.services.evobgp-web.loadbalancer.server.port=80 logging: driver: json-file options: max-size: "10m" max-file: "3" evobgp-edge: image: traefik:latest restart: unless-stopped depends_on: evobgp-web: condition: service_started ports: - "80:80" - "443:443" environment: DOCKER_API_VERSION: "1.44" CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN} command: - --log.level=INFO - --api.dashboard=false - --providers.docker=true - --providers.docker.exposedbydefault=false - --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} - --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 logging: driver: json-file options: max-size: "10m" max-file: "3" prometheus: image: prom/prometheus:v2.54.1 restart: unless-stopped depends_on: evobgp-all: condition: service_started ports: - "9090:9090" configs: - source: prometheus_yml target: /etc/prometheus/prometheus.yml - source: prometheus_alerts_yml target: /etc/prometheus/alerts.yml command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --storage.tsdb.retention.time=7d - --web.enable-lifecycle logging: driver: json-file options: max-size: "10m" max-file: "3" stack-runtime-logs: image: docker:27-cli restart: unless-stopped depends_on: evobgp-all: condition: service_started environment: COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-evobgp-microvps-full} volumes: - /var/run/docker.sock:/var/run/docker.sock - type: bind source: ${EVOBGP_RUNTIME_LOGS_HOST_DIR:-/opt/evobgp/runtime-logs} target: /logs bind: create_host_path: true entrypoint: ["/bin/sh", "-c"] command: - | set -eu mkdir -p /logs PROJECT=$$COMPOSE_PROJECT_NAME SERVICES="postgres nats bird2 evobgp-agent evobgp-all evobgp-web evobgp-edge prometheus" log_one() { svc=$$1 f="/logs/$$svc.log" while true; do cid=$$(docker ps -q \ -f "label=com.docker.compose.service=$$svc" \ -f "label=com.docker.compose.project=$$PROJECT" | head -n1) if [ -n "$$cid" ]; then echo "---- $$(date -u +"%Y-%m-%dT%H:%M:%SZ") attach $$svc $$cid ----" >> "$$f" docker logs -f --timestamps "$$cid" >> "$$f" 2>&1 || true fi sleep 3 done } for s in $$SERVICES; do log_one "$$s" & done wait volumes: pgdata: bird_etc: bird_run: traefik_letsencrypt: name: evobgp_traefik_letsencrypt