services: # =========================================== # Основные сервисы # =========================================== bot: image: ghcr.io/case211/remnawave-admin:latest restart: unless-stopped init: true env_file: - .env environment: PYTHONPATH: /app MAXMIND_CITY_DB: /app/geoip/GeoLite2-City.mmdb MAXMIND_ASN_DB: /app/geoip/GeoLite2-ASN.mmdb DB_POOL_MIN_SIZE: '2' DB_POOL_MAX_SIZE: '5' volumes: - ./logs:/app/logs - geoip_data:/app/geoip ports: - "${WEBHOOK_PORT:-8080}:${WEBHOOK_PORT:-8080}" networks: - remnawave-network depends_on: remnawave-admin-db: condition: service_healthy healthcheck: # HTTP-чек вебхук-сервера; если вебхук отключён — фолбэк на живость # процесса через /proc (pgrep в slim-образе отсутствует) test: ["CMD-SHELL", "python -c 'import urllib.request; urllib.request.urlopen(\"http://127.0.0.1:${WEBHOOK_PORT:-8080}/webhook/health\", timeout=5)' 2>/dev/null || grep -qa src.main /proc/1/cmdline"] interval: 30s timeout: 10s retries: 3 # Бот применяет миграции сам, до подъёма вебхук-сервера. На большой базе # это минуты — за 30 секунд контейнер успевал стать unhealthy, пока # честно ждал собственную миграцию. web-backend с теми же миграциями # получает 120 с, боту нужно не меньше. start_period: 180s remnawave-admin-db: container_name: remnawave-admin-db image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER is required} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} POSTGRES_DB: ${POSTGRES_DB:-remnawave_bot} volumes: - postgres_data:/var/lib/postgresql/data - ./logs:/app/logs # Fix /app/logs ownership for postgres user, then start with logging_collector entrypoint: - sh - -c - > mkdir -p /app/logs && chown postgres:postgres /app/logs && exec docker-entrypoint.sh postgres -c logging_collector=on -c log_directory=/app/logs -c log_filename=postgres.log -c log_file_mode=0644 -c log_truncate_on_rotation=off -c log_rotation_size=10240 -c log_min_messages=warning -c log_min_duration_statement=1000 -c "log_line_prefix=%t [%p] " -c shared_buffers=${ADMIN_DB_SHARED_BUFFERS:-256MB} -c work_mem=${ADMIN_DB_WORK_MEM:-8MB} -c maintenance_work_mem=128MB -c effective_cache_size=${ADMIN_DB_EFFECTIVE_CACHE_SIZE:-1GB} -c wal_buffers=16MB -c checkpoint_completion_target=0.9 -c autovacuum_vacuum_scale_factor=0.05 -c autovacuum_analyze_scale_factor=0.02 -c autovacuum_vacuum_cost_delay=2ms -c autovacuum_max_workers=3 networks: - remnawave-network healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB:-remnawave_bot}"] interval: 10s timeout: 5s retries: 5 # =========================================== # Веб-панель # =========================================== # APP_MODE (через .env): # full (default) — всё в одном контейнере, ничего не меняется # api — только UI API, плагины, автоматизация (используй с профилем collector) # # Для разделения: в .env добавь APP_MODE=api и запусти: # docker compose --profile collector up -d # Агенты остаются на 8081 (collector), админка на WEB_BACKEND_PORT (default 8081 в full, 8082 в api) web-backend: image: ghcr.io/case211/remnawave-admin-web-backend:latest container_name: remnawave-web-backend restart: unless-stopped init: true env_file: - .env environment: WEB_HOST: '0.0.0.0' WEB_PORT: '${WEB_BACKEND_PORT:-8081}' APP_MODE: '${APP_MODE:-full}' REDIS_URL: '${REDIS_URL:-}' # Trust X-Real-IP/X-Forwarded-For only from the reverse proxy. The admin # API shares the published collector port, so pin this to the frontend # proxy's address to stop direct-port IP spoofing (see .env.example). WEB_TRUSTED_PROXIES: '${WEB_TRUSTED_PROXIES:-}' MAXMIND_CITY_DB: /app/geoip/GeoLite2-City.mmdb MAXMIND_ASN_DB: /app/geoip/GeoLite2-ASN.mmdb BACKUP_DIR: /app/backups command: ["python", "-m", "uvicorn", "web.backend.main:app", "--host", "0.0.0.0", "--port", "${WEB_BACKEND_PORT:-8081}", "--no-access-log", "--log-level", "warning", "--ws", "websockets", "--ws-per-message-deflate", "true"] volumes: - ./logs:/app/logs - geoip_data:/app/geoip - ./backups:/app/backups - ./plugins:/app/plugins # Сертификат почтовых портов. Тут же оседает самоподписанный, если # своего не задали — иначе он выписывался бы заново на каждый рестарт, # и принимающие серверы видели бы у нас каждый раз новый ключ. - mail_certs:/app/certs ports: - "${WEB_BACKEND_PORT:-8081}:${WEB_BACKEND_PORT:-8081}" - "${MAIL_INBOUND_PORT:-2525}:${MAIL_INBOUND_PORT:-2525}" - "${MAIL_SUBMISSION_PORT:-587}:${MAIL_SUBMISSION_PORT:-587}" networks: - remnawave-network depends_on: remnawave-admin-db: condition: service_healthy healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:${WEB_BACKEND_PORT:-8081}/api/v2/health')"] interval: 10s timeout: 10s retries: 5 start_period: 120s # =========================================== # Collector (опционально — отдельный контейнер для violation detection) # Включить: docker compose --profile collector up -d # При включении web-backend автоматически переключается в API-only режим # =========================================== web-collector: image: ghcr.io/case211/remnawave-admin-web-backend:latest container_name: remnawave-web-collector restart: unless-stopped init: true profiles: [collector] env_file: - .env environment: WEB_HOST: '0.0.0.0' WEB_PORT: '8081' APP_MODE: 'collector' REDIS_URL: '${REDIS_URL:-}' WEB_TRUSTED_PROXIES: '${WEB_TRUSTED_PROXIES:-}' WEBHOOK_SECRET: '${WEBHOOK_SECRET:-}' BOT_WEBHOOK_URL: 'http://bot:${WEBHOOK_PORT:-8080}/webhook' BOT_CALLBACK_URL: 'http://bot:${WEBHOOK_PORT:-8080}/internal/panel-event' MAXMIND_CITY_DB: /app/geoip/GeoLite2-City.mmdb MAXMIND_ASN_DB: /app/geoip/GeoLite2-ASN.mmdb command: ["python", "-m", "uvicorn", "web.backend.main:app", "--host", "0.0.0.0", "--port", "8081", "--no-access-log", "--log-level", "warning"] volumes: - ./logs:/app/logs - geoip_data:/app/geoip ports: - "8081:8081" networks: - remnawave-network depends_on: remnawave-admin-db: condition: service_healthy healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8081/api/v2/health')"] interval: 10s timeout: 10s retries: 5 start_period: 60s web-frontend: image: ghcr.io/case211/remnawave-admin-web-frontend:latest container_name: remnawave-web-frontend restart: unless-stopped init: true env_file: - .env volumes: - ./logs:/app/logs ports: - "${WEB_FRONTEND_PORT:-3000}:80" networks: - remnawave-network depends_on: web-backend: condition: service_started # =========================================== # Мониторинг (--profile monitoring) # Поднимает Prometheus для сбора /metrics панели. # Grafana остаётся отдельным сервисом — подключи свою к http://server:9090 # =========================================== prometheus: image: prom/prometheus:v3.0.1 container_name: remnawave-prometheus restart: unless-stopped profiles: [monitoring] user: "65534:65534" # nobody:nobody, владелец /prometheus в образе volumes: - ./monitoring/prometheus.yml.tmpl:/etc/prometheus/prometheus.yml.tmpl:ro - ./monitoring/alerts.yml:/etc/prometheus/alerts.yml:ro - ./monitoring/entrypoint.sh:/usr/local/bin/entrypoint.sh:ro - prometheus_data:/prometheus environment: METRICS_AUTH_TOKEN: ${METRICS_AUTH_TOKEN:-} PROMETHEUS_RETENTION: ${PROMETHEUS_RETENTION:-30d} # Scrape target port: 8081 (full) or 8082 (APP_MODE=api split). Must match WEB_BACKEND_PORT. WEB_BACKEND_PORT: ${WEB_BACKEND_PORT:-8081} entrypoint: ["/bin/sh", "/usr/local/bin/entrypoint.sh"] ports: - "${PROMETHEUS_PORT:-9090}:9090" networks: - remnawave-network depends_on: web-backend: condition: service_started healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9090/-/ready"] interval: 30s timeout: 10s retries: 3 start_period: 30s # =========================================== # Redis (--profile redis) # Общий кэш аналитики + distributed rate limiting между контейнерами. # Включить: docker compose --profile redis up -d # В .env добавить: REDIS_URL=redis://redis:6379/0 # =========================================== redis: image: redis:7-alpine container_name: remnawave-redis restart: unless-stopped profiles: [redis] command: ["redis-server", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru", "--save", ""] volumes: - redis_data:/data networks: - remnawave-network healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 3 volumes: postgres_data: geoip_data: prometheus_data: redis_data: mail_certs: networks: remnawave-network: external: true