name: woolyai

networks:
  net-br:
    name: net-br
    driver: bridge

volumes:
  nats-1-data:
  nats-2-data:
  nats-3-data:

  etcd-1-data:
  etcd-2-data:
  etcd-3-data:

  traefik-logs:

services:
  # Traefik Load Balancer
  # View logs (run from project root):
  #
  # Info/warning/error logs (real-time):
  # docker compose logs -f traefik
  #
  # Access logs (real-time):
  # docker compose exec traefik tail -f /var/log/traefik/access.log
  #
  # Access logs (view all):
  # docker compose exec traefik cat /var/log/traefik/access.log
  #
  # Search for specific API endpoints in access logs:
  # docker compose exec traefik grep "/api/v1" /var/log/traefik/access.log
  #
  # Search for errors/warnings (if any):
  # docker compose exec traefik grep -i "error\|warning" /var/log/traefik/traefik.log 2>/dev/null || echo "No errors/warnings found"
  traefik:
    image: traefik:3.5
    command:
      - "--api.insecure=true"
      - "--api.dashboard=true"
      - "--entrypoints.web.address=:9000"
      - "--entrypoints.traefik.address=:9001"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--log.level=info"
      - "--log.filepath=/var/log/traefik/traefik.log"
      - "--accesslog=true"
      - "--accesslog.filepath=/var/log/traefik/access.log"
      - "--accesslog.format=clf"
    ports:
      - "8080:9000"
      - "8081:9001"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-logs:/var/log/traefik
    networks:
      - net-br
    restart: unless-stopped
    depends_on:
      controller-1:
        condition: service_healthy
      controller-2:
        condition: service_healthy
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: [ "CMD-SHELL", "netstat -tuln | grep 9000 || ss -tuln | grep 9000" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s

  controller-1:
    image: woolyai/controller:latest
    # Uncomment the below build lines to build the controller image/tag locally instead of using the one from Docker Hub (comment out the image line above too!)
    # build:
    #   context: .
    #   dockerfile: Dockerfile.controller
    environment:
      # Controller Configuration
      CONTROLLER_ID: "controller-1"  # default: auto-generated from hostname+UUID; used by leadership services and must stay unique per controller instance
      # CONTROLLER_LEASE_TTL: "10s"  # default: 10s
      # CONTROLLER_ASSIGNED_TASK_EXPIRATION_TIMEOUT: "10s"  # default: 10s
      # CONTROLLER_NODE_LIVENESS_TIMEOUT: "60s"  # default: 60s
      # CONTROLLER_RETENTION_PERIOD: "168h"  # default: 7 days (168h)
      # CONTROLLER_RESERVATION_RECONCILIATION_TIMEOUT: "5s"  # default: 5s

      # Server Configuration
      # SERVER_ADDRESS: ":8080"  # default: :8080
      # SERVER_READ_TIMEOUT: "30s"  # default: 30s
      # SERVER_WRITE_TIMEOUT: "30s"  # default: 30s
      # SERVER_IDLE_TIMEOUT: "120s"  # default: 120s

      # Logging Configuration
      # LOG_LEVEL: 0  # -4=debug, 0=info, 4=warn, 8=error; default: 0; valid range: -4 to 8
      # LOG_FORMAT: "json"  # default: json; valid: json, text, console

      # ETCD Configuration
      ETCD_ENDPOINTS: http://etcd-1:2379,http://etcd-2:2379,http://etcd-3:2379
      # ETCD_DIAL_TIMEOUT: "5s"  # default: 5s
      # ETCD_REQUEST_TIMEOUT: "10s"  # default: 10s
      ETCD_LOG_LEVEL: 0  # default: inherits LOG_LEVEL; override to tune etcd client verbosity

      # NATS Configuration
      NATS_URL: nats://nats-1:4222,nats://nats-2:4222,nats://nats-3:4222
      # NATS_CONNECT_TIMEOUT: "5s"  # default: 5s
      # NATS_RECONNECT_WAIT: "2s"  # default: 2s
      # NATS_MAX_RECONNECTS: 10  # default: 10
      # NATS_REQUEST_TIMEOUT: "10s"
      # NATS_STREAM_NAME: "CONTROLLER_EVENTS"  # default: CONTROLLER_EVENTS
      # NATS_ACK_WAIT: "2m"  # default: 2m
      # NATS_RETENTION: "168h"  # default: 7 days (168h) to outlive long weekends; adjust as needed
      # NATS_QUEUE_GROUP: "controller-events"  # default: controller-events; controllers with same queue group share workload

      # Health Check Configuration
      # HEALTH_CACHE_TTL: "10s"  # default: 10s
      # HEALTH_ETCD_TIMEOUT: "5s"  # default: 5s
      # HEALTH_NATS_TIMEOUT: "3s"  # default: 3s
      # HEALTH_ENABLE_ETCD_CHECK: true  # default: true
      # HEALTH_ENABLE_NATS_CHECK: true  # default: true
      # HEALTH_ENABLE_SERVICES_CHECK: true  # default: true

      # Debug/Feature Flags
      # DEBUG_VALIDATE_NATS_MESSAGES: true  # default: false; enable JSON schema validation of NATS messages
      # DEBUG_DUMP_NATS_MESSAGES: true  # default: false; log all NATS message bodies at DEBUG level (requires LOG_LEVEL=-4 to be visible)
      # DEBUG_DUMP_API_REQUESTS: true  # default: false; log all /api requests at DEBUG level (requires LOG_LEVEL=-4 to be visible)
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.controller.rule=PathPrefix(`/`)"
      - "traefik.http.routers.controller.entrypoints=web"
      - "traefik.http.services.controller.loadbalancer.server.port=8080"
      - "traefik.http.services.controller.loadbalancer.healthcheck.path=/readyz"
      - "traefik.http.services.controller.loadbalancer.healthcheck.interval=10s"
    networks:
      - net-br
    healthcheck:
      test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/readyz" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    depends_on:
      etcd-1:
        condition: service_healthy
      etcd-2:
        condition: service_healthy
      etcd-3:
        condition: service_healthy
      nats-1:
        condition: service_healthy
      nats-2:
        condition: service_healthy
      nats-3:
        condition: service_healthy
    restart: unless-stopped

  controller-2:
    image: woolyai/controller:latest
    # Uncomment the below build lines to build the controller image/tag locally instead of using the one from Docker Hub (comment out the image line above too!)
    # build:
    #   context: .
    environment:
      # Controller Configuration
      CONTROLLER_ID: "controller-2"  # default: auto-generated from hostname+UUID; used by leadership services and must stay unique per controller instance
      # CONTROLLER_LEASE_TTL: "10s"  # default: 10s
      # CONTROLLER_ASSIGNED_TASK_EXPIRATION_TIMEOUT: "10s"  # default: 10s
      # CONTROLLER_NODE_LIVENESS_TIMEOUT: "60s"  # default: 60s
      # CONTROLLER_RETENTION_PERIOD: "168h"  # default: 7 days (168h)
      # CONTROLLER_RESERVATION_RECONCILIATION_TIMEOUT: "5s"  # default: 5s

      # Server Configuration
      # SERVER_ADDRESS: ":8080"  # default: :8080
      # SERVER_READ_TIMEOUT: "30s"  # default: 30s
      # SERVER_WRITE_TIMEOUT: "30s"  # default: 30s
      # SERVER_IDLE_TIMEOUT: "120s"  # default: 120s

      # Logging Configuration
      # LOG_LEVEL: 0  # -4=debug, 0=info, 4=warn, 8=error; default: 0; valid range: -4 to 8
      # LOG_FORMAT: "json"  # default: json; valid: json, text, console

      # ETCD Configuration
      ETCD_ENDPOINTS: http://etcd-1:2379,http://etcd-2:2379,http://etcd-3:2379
      # ETCD_DIAL_TIMEOUT: "5s"  # default: 5s
      # ETCD_REQUEST_TIMEOUT: "10s"  # default: 10s
      ETCD_LOG_LEVEL: 0  # default: inherits LOG_LEVEL; override to tune etcd client verbosity

      # NATS Configuration
      NATS_URL: nats://nats-1:4222,nats://nats-2:4222,nats://nats-3:4222
      # NATS_CONNECT_TIMEOUT: "5s"  # default: 5s
      # NATS_RECONNECT_WAIT: "2s"  # default: 2s
      # NATS_MAX_RECONNECTS: 10  # default: 10
      # NATS_REQUEST_TIMEOUT: "10s"
      # NATS_STREAM_NAME: "CONTROLLER_EVENTS"  # default: CONTROLLER_EVENTS
      # NATS_ACK_WAIT: "2m"  # default: 2m
      # NATS_RETENTION: "168h"  # default: 7 days (168h) to outlive long weekends; adjust as needed
      # NATS_QUEUE_GROUP: "controller-events"  # default: controller-events; controllers with same queue group share workload

      # Health Check Configuration
      # HEALTH_CACHE_TTL: "10s"  # default: 10s
      # HEALTH_ETCD_TIMEOUT: "5s"  # default: 5s
      # HEALTH_NATS_TIMEOUT: "3s"  # default: 3s
      # HEALTH_ENABLE_ETCD_CHECK: true  # default: true
      # HEALTH_ENABLE_NATS_CHECK: true  # default: true
      # HEALTH_ENABLE_SERVICES_CHECK: true  # default: true

      # Debug/Feature Flags
      # DEBUG_VALIDATE_NATS_MESSAGES: true  # default: false; enable JSON schema validation of NATS messages
      # DEBUG_DUMP_NATS_MESSAGES: true  # default: false; log all NATS message bodies at DEBUG level (requires LOG_LEVEL=-4 to be visible)
      # DEBUG_DUMP_API_REQUESTS: true  # default: false; log all /api requests at DEBUG level (requires LOG_LEVEL=-4 to be visible)
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.controller.rule=PathPrefix(`/`)"
      - "traefik.http.routers.controller.entrypoints=web"
      - "traefik.http.services.controller.loadbalancer.server.port=8080"
      - "traefik.http.services.controller.loadbalancer.healthcheck.path=/readyz"
      - "traefik.http.services.controller.loadbalancer.healthcheck.interval=10s"
    networks:
      - net-br
    healthcheck:
      test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/readyz" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    depends_on:
      etcd-1:
        condition: service_healthy
      etcd-2:
        condition: service_healthy
      etcd-3:
        condition: service_healthy
      nats-1:
        condition: service_healthy
      nats-2:
        condition: service_healthy
      nats-3:
        condition: service_healthy
    restart: unless-stopped

  # NATS Cluster (3 nodes)
  nats-1:
    image: nats:2.12.1-alpine
    command:
      [
        "--jetstream",
        "--store_dir",
        "/data/jetstream",
        "--server_name",
        "nats-1",
        "--cluster_name",
        "wooly",
        "--cluster",
        "nats://0.0.0.0:6222",
# IMPORTANT: use the publicly reachable host/IP so clients outside the Docker network can reach this server
#        "--client_advertise",
#        "<server_public_ip_here>:14222",
        "--routes",
        "nats://nats-2:6222,nats://nats-3:6222",
        "--http_port",
        "8222",
      ]
    ports:
      - "14222:4222"
      - "18222:8222"
    networks:
      - net-br
    volumes:
      - nats-1-data:/data
    healthcheck:
      test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8222/" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    restart: unless-stopped

  nats-2:
    image: nats:2.12.1-alpine
    command:
      [
        "--jetstream",
        "--store_dir",
        "/data/jetstream",
        "--server_name",
        "nats-2",
        "--cluster_name",
        "wooly",
        "--cluster",
        "nats://0.0.0.0:6222",
# IMPORTANT: use the publicly reachable host/IP so clients outside the Docker network can reach this server
#        "--client_advertise",
#        "<server_public_ip_here>:24222",
        "--routes",
        "nats://nats-1:6222,nats://nats-3:6222",
        "--http_port",
        "8222",
      ]
    ports:
      - "24222:4222"
      - "28222:8222"
    networks:
      - net-br
    volumes:
      - nats-2-data:/data
    healthcheck:
      test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8222/" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    restart: unless-stopped

  nats-3:
    image: nats:2.12.1-alpine
    command:
      [
        "--jetstream",
        "--store_dir",
        "/data/jetstream",
        "--server_name",
        "nats-3",
        "--cluster_name",
        "wooly",
        "--cluster",
        "nats://0.0.0.0:6222",
# IMPORTANT: use the publicly reachable host/IP so clients outside the Docker network can reach this server
#        "--client_advertise",
#        "<server_public_ip_here>:34222",
        "--routes",
        "nats://nats-1:6222,nats://nats-2:6222",
        "--http_port",
        "8222",
      ]
    ports:
      - "34222:4222"
      - "38222:8222"
    networks:
      - net-br
    volumes:
      - nats-3-data:/data
    healthcheck:
      test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8222/" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    restart: unless-stopped

  # NATS management CLI container for inspecting streams and consumers
  # Note: You can run nats commands directly on the host if you have the nats CLI installed
  # Or use the container with: docker compose run --rm nats-cli nats <command>
  #
  # Examples:
  #   - List subscribed controller consumers:
  #       docker compose run --rm nats-cli nats consumer ls CONTROLLER_EVENTS -s nats://nats-1:4222
  #   - Get details of a specific controller consumer:
  #       docker compose run --rm nats-cli nats consumer info CONTROLLER_EVENTS <consumer_id> -s nats://nats-1:4222
  #   - Delete a specific controller consumer:
  #       docker compose run --rm nats-cli nats consumer rm CONTROLLER_EVENTS <consumer_id> -s nats://nats-1:4222 -f
  #   - Enter interactive shell:
  #       docker compose run --rm nats-cli sh
  nats-cli:
    image: natsio/nats-box:latest
    networks:
      - net-br
    restart: "no"
    profiles: [ "cli" ]  # Only started explicitly via: docker compose --profile cli run --rm nats-cli

  # etcd Cluster (3 nodes)
  etcd-1:
    image: quay.io/coreos/etcd:v3.6.5
    environment:
      ETCD_NAME: etcd-1
      ETCD_DATA_DIR: /etcd-data
      ETCD_INITIAL_ADVERTISE_PEER_URLS: http://etcd-1:2380
      ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
      ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
      ETCD_ADVERTISE_CLIENT_URLS: http://etcd-1:2379
      ETCD_INITIAL_CLUSTER: etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
      ETCD_INITIAL_CLUSTER_STATE: new
      ETCD_INITIAL_CLUSTER_TOKEN: wooly-dev-cluster
    ports:
      - "2379:2379"
    volumes:
      - etcd-1-data:/etcd-data
    networks:
      - net-br
    healthcheck:
      test: [ "CMD", "etcdctl", "endpoint", "health" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    restart: unless-stopped

  etcd-2:
    image: quay.io/coreos/etcd:v3.6.5
    environment:
      ETCD_NAME: etcd-2
      ETCD_DATA_DIR: /etcd-data
      ETCD_INITIAL_ADVERTISE_PEER_URLS: http://etcd-2:2380
      ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
      ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
      ETCD_ADVERTISE_CLIENT_URLS: http://etcd-2:2379
      ETCD_INITIAL_CLUSTER: etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
      ETCD_INITIAL_CLUSTER_STATE: new
      ETCD_INITIAL_CLUSTER_TOKEN: wooly-dev-cluster
    volumes:
      - etcd-2-data:/etcd-data
    networks:
      - net-br
    healthcheck:
      test: [ "CMD", "etcdctl", "endpoint", "health" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    restart: unless-stopped

  etcd-3:
    image: quay.io/coreos/etcd:v3.6.5
    environment:
      ETCD_NAME: etcd-3
      ETCD_DATA_DIR: /etcd-data
      ETCD_INITIAL_ADVERTISE_PEER_URLS: http://etcd-3:2380
      ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
      ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
      ETCD_ADVERTISE_CLIENT_URLS: http://etcd-3:2379
      ETCD_INITIAL_CLUSTER: etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
      ETCD_INITIAL_CLUSTER_STATE: new
      ETCD_INITIAL_CLUSTER_TOKEN: wooly-dev-cluster
    volumes:
      - etcd-3-data:/etcd-data
    networks:
      - net-br
    healthcheck:
      test: [ "CMD", "etcdctl", "endpoint", "health" ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    restart: unless-stopped
