# docker-compose.yml — single-host M0 stack. # Run: docker compose up -d # Then: see M0_VERIFICATION.md for the loadgen smoke test. # # Port conventions (project rule): # - app HTTP services: 8800–8899 (ingestd/routerd/deliverd/admind/loadgen) # - canonical ports stay (5432 postgres, 4222 nats, 6379 redis, # 1883 mqtt, 9090 prometheus, 3000 grafana, etc.) services: # ── Data tier ──────────────────────────────────────────────────── postgres: image: timescale/timescaledb:latest-pg16 environment: POSTGRES_USER: ba POSTGRES_PASSWORD: ba POSTGRES_DB: ba ports: ["5432:5432"] volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ba"] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine ports: ["6379:6379"] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 nats: image: nats:2.10-alpine command: ["-js", "-sd", "/data"] ports: ["4222:4222", "8222:8222"] # 8222 is the monitoring HTTP volumes: - natsdata:/data healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:8222/healthz"] interval: 5s timeout: 3s retries: 10 emqx: image: emqx/emqx:5-alpine ports: ["1883:1883", "18083:18083"] # MQTT + admin UI healthcheck: test: ["CMD", "/opt/emqx/bin/emqx", "ping"] interval: 10s timeout: 5s retries: 10 clickhouse: image: clickhouse/clickhouse-server:24-alpine ports: ["8123:8123", "9000:9000"] volumes: - chdata:/var/lib/clickhouse ulimits: nofile: { soft: 262144, hard: 262144 } # ── App tier ───────────────────────────────────────────────────── ingestd: build: . command: ["/app/ingestd"] environment: BA_ENV: dev BA_HTTP_ADDR: ":8800" BA_NATS_URL: nats://nats:4222 BA_REDIS_URL: redis://redis:6379/0 BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable BA_INGESTD_SOURCES: "acme-001:prom-prod:s3cret-acme,globex-002:grafana:s3cret-globex" BA_INGESTD_RATE_LIMIT_PER_SOURCE: "100" BA_INGESTD_RATE_LIMIT_PER_COMPANY: "10000" ports: ["8800:8800"] depends_on: nats: { condition: service_healthy } redis: { condition: service_healthy } postgres: { condition: service_healthy } routerd: build: . command: ["/app/routerd"] environment: BA_ENV: dev BA_HTTP_ADDR: ":8801" BA_NATS_URL: nats://nats:4222 BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable ports: ["8801:8801"] depends_on: nats: { condition: service_healthy } postgres: { condition: service_healthy } deliverd: build: . command: ["/app/deliverd"] environment: BA_ENV: dev BA_HTTP_ADDR: ":8802" BA_NATS_URL: nats://nats:4222 BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable ports: ["8802:8802"] depends_on: nats: { condition: service_healthy } postgres: { condition: service_healthy } admind: build: . command: ["/app/admind"] environment: BA_ENV: dev BA_HTTP_ADDR: ":8803" BA_NATS_URL: nats://nats:4222 BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable ports: ["8803:8803"] depends_on: nats: { condition: service_healthy } postgres: { condition: service_healthy } # ── Observability ──────────────────────────────────────────────── prometheus: image: prom/prometheus:latest command: - --config.file=/etc/prometheus/prometheus.yml volumes: - ./deploy/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro ports: ["9090:9090"] depends_on: [ingestd, routerd, deliverd, admind] grafana: image: grafana/grafana:latest ports: ["3000:3000"] environment: GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: admin depends_on: [prometheus] # ── Loadgen (one-shot smoke profile) ───────────────────────────── loadgen-http: build: . command: ["/app/loadgen-http"] environment: BA_TARGET: http://ingestd:8800 BA_API_KEY: "acme-001:prom-prod:s3cret-acme" BA_RATE: "50" BA_DURATION: "30s" profiles: ["loadgen"] depends_on: ingestd: { condition: service_started } volumes: pgdata: {} natsdata: {} chdata: {}