# Standalone stack (без profiles): Portainer / `docker compose -f stack.microvps-full.yaml up -d` # Эквивалент microvps-full из docker-compose.yaml + docker-compose.microvps-full.yaml, # с встроенными configs для Prometheus (как у вас). # # Postgres: shm_size + start_period — иначе часто «dependency postgres failed» на слабом хосте. # Для защищенного Web UI задайте переменные окружения перед запуском: # WEBUI_DOMAIN, WEBUI_IP_WHITELIST, LETSENCRYPT_EMAIL, CF_DNS_API_TOKEN # Cloudflare запись для WEBUI_DOMAIN: DNS only (серый облачок). # # ACME / Let's Encrypt: путь задан в evobgp-edge (--certificatesresolvers...storage). # Имя тома traefik_letsencrypt зафиксировано (volumes.traefik_letsencrypt.name), иначе при # другом имени стека в Portainer или смене project name создаётся новый пустой том — # Traefik не находит acme.json и отдаёт дефолтный сертификат до новой выдачи LE. # Не использовать `docker compose down -v` без бэкапа. Если раньше был том с префиксом # проекта, перенесите acme.json в том evobgp_traefik_letsencrypt. # # Долгий сбор логов в файлы на хосте: сервис stack-runtime-logs пишет в каталог # ${EVOBGP_RUNTIME_LOGS_HOST_DIR:-./runtime-logs} по одному файлу на сервис. # evobgp-all монтирует тот же каталог в /opt/evobgp/runtime-logs для API /v1/runtime-logs/*. # Автообновление сервисов из registry (без перезапуска bird2): stack-auto-updater. # Настройки через .env: AUTO_UPDATE_ENABLED, AUTO_UPDATE_INTERVAL_SEC, # AUTO_UPDATE_SERVICES, AUTO_UPDATE_PROTECTED_SERVICES. # Имя проекта в Docker должно совпадать с label com.docker.compose.project: # при смене `name:` или имени стека в Portainer задайте COMPOSE_PROJECT_NAME. # Требуется доступ к docker.sock (полные права на демон — осознанно). 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.shts.su/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.shts.su/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.shts.su/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_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 # DEV ONLY — не для production (см. docs/access.md). EVOBGP_DEV_INSECURE: "1" volumes: - bird_etc:/etc/bird - bird_run:/run/bird:ro - type: bind source: ${EVOBGP_RUNTIME_LOGS_HOST_DIR:-./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.shts.su/denozord}/evobgp-web-all:${EVOBGP_IMAGE_TAG:-latest} restart: unless-stopped depends_on: evobgp-all: condition: service_started labels: - traefik.enable=true - 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.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:-./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 stack-auto-updater: image: docker:27-cli restart: unless-stopped depends_on: evobgp-all: condition: service_started environment: COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-evobgp-microvps-full} AUTO_UPDATE_ENABLED: ${AUTO_UPDATE_ENABLED:-0} AUTO_UPDATE_INTERVAL_SEC: ${AUTO_UPDATE_INTERVAL_SEC:-300} AUTO_UPDATE_SERVICES: ${AUTO_UPDATE_SERVICES:-evobgp-all,evobgp-web} AUTO_UPDATE_PROTECTED_SERVICES: ${AUTO_UPDATE_PROTECTED_SERVICES:-bird2} volumes: - /var/run/docker.sock:/var/run/docker.sock - type: bind source: ./stack.microvps-full.yaml target: /stack/stack.microvps-full.yaml read_only: true entrypoint: ["/bin/sh", "-c"] command: - | set -eu PROJECT=$$COMPOSE_PROJECT_NAME INTERVAL=$$AUTO_UPDATE_INTERVAL_SEC ENABLED=$$AUTO_UPDATE_ENABLED if [ "$$ENABLED" != "1" ]; then echo "auto-updater disabled: AUTO_UPDATE_ENABLED=$$ENABLED" while true; do sleep 3600; done fi if [ "$$INTERVAL" -lt 30 ]; then echo "auto-updater interval too low ($$INTERVAL), forcing to 30" INTERVAL=30 fi echo "auto-updater started: project=$$PROJECT interval=$$INTERVAL services=$$AUTO_UPDATE_SERVICES protected=$$AUTO_UPDATE_PROTECTED_SERVICES" while true; do TS=$$(date -u +"%Y-%m-%dT%H:%M:%SZ") SERVICES_LIST=$$(echo "$$AUTO_UPDATE_SERVICES" | tr ',' ' ') PROTECTED_LIST=$$(echo "$$AUTO_UPDATE_PROTECTED_SERVICES" | tr ',' ' ') for svc in $$SERVICES_LIST; do if [ -z "$$svc" ]; then continue fi skip=0 for protected in $$PROTECTED_LIST; do if [ "$$svc" = "$$protected" ]; then echo "$$TS skip protected service '$$svc'" skip=1 break fi done if [ "$$skip" = "1" ]; then continue fi before=$$(docker image inspect "$$(docker compose -p "$$PROJECT" -f /stack/stack.microvps-full.yaml config --images $$svc | tail -n1)" --format '{{.Id}}' 2>/dev/null || true) pull_out=$$(docker compose -p "$$PROJECT" -f /stack/stack.microvps-full.yaml pull $$svc 2>&1 || true) echo "$$TS pull $$svc: $$pull_out" after=$$(docker image inspect "$$(docker compose -p "$$PROJECT" -f /stack/stack.microvps-full.yaml config --images $$svc | tail -n1)" --format '{{.Id}}' 2>/dev/null || true) if [ -n "$$after" ] && [ "$$before" != "$$after" ]; then echo "$$TS update detected for $$svc, restarting container" docker compose -p "$$PROJECT" -f /stack/stack.microvps-full.yaml up -d --no-deps $$svc else echo "$$TS no update for $$svc" fi done sleep "$$INTERVAL" done logging: driver: json-file options: max-size: "10m" max-file: "3" volumes: pgdata: bird_etc: bird_run: traefik_letsencrypt: name: evobgp_traefik_letsencrypt