docker-compose.yml 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. # docker-compose.yml — single-host M0 stack.
  2. # Run: docker compose up -d
  3. # Then: see M0_VERIFICATION.md for the loadgen smoke test.
  4. #
  5. # Port conventions (project rule):
  6. # - app HTTP services: 8800–8899 (ingestd/routerd/deliverd-fcm/deliverd-telegram/telegramd/admind/loadgen/faketgmd)
  7. # - canonical ports stay (5432 postgres, 4222 nats, 6379 redis,
  8. # 1883 mqtt, 9090 prometheus, 3000 grafana, etc.)
  9. services:
  10. # ── Data tier ────────────────────────────────────────────────────
  11. postgres:
  12. image: timescale/timescaledb:latest-pg16
  13. environment:
  14. POSTGRES_USER: ba
  15. POSTGRES_PASSWORD: ba
  16. POSTGRES_DB: ba
  17. # No host port mapping: a host-local postgres is already on
  18. # :5432. The app services reach this one via the docker network
  19. # DNS name 'postgres'. Use `docker compose exec postgres psql ...`
  20. # to talk to it from the host.
  21. expose: ["5432"]
  22. volumes:
  23. - pgdata:/var/lib/postgresql/data
  24. healthcheck:
  25. test: ["CMD-SHELL", "pg_isready -U ba"]
  26. interval: 5s
  27. timeout: 3s
  28. retries: 10
  29. redis:
  30. image: redis:7-alpine
  31. # No host port mapping: a host-local redis is on :6379. App
  32. # services reach this one via docker DNS 'redis'.
  33. expose: ["6379"]
  34. healthcheck:
  35. test: ["CMD", "redis-cli", "ping"]
  36. interval: 5s
  37. timeout: 3s
  38. retries: 10
  39. nats:
  40. image: nats:2.10-alpine
  41. command: ["-js", "-sd", "/data", "-m", "8222"]
  42. ports: ["4222:4222", "8222:8222"] # 8222 is the monitoring HTTP
  43. volumes:
  44. - natsdata:/data
  45. healthcheck:
  46. test: ["CMD", "wget", "-qO-", "http://localhost:8222/healthz"]
  47. interval: 5s
  48. timeout: 3s
  49. retries: 20
  50. emqx:
  51. image: emqx/emqx:5.10.4
  52. ports: ["1883:1883", "18083:18083"] # MQTT + admin UI
  53. volumes:
  54. # M4: per-company ACL + auth bootstrap. acl.conf is read on
  55. # SIGHUP; the auth CSV is read on first boot. M11 promotes
  56. # this to a Postgres-backed authentication chain.
  57. - ./deploy/emqx/acl.conf:/opt/emqx/etc/acl.conf:ro
  58. - ./deploy/emqx/auth-built-in-db-bootstrap.csv:/opt/emqx/etc/auth-built-in-db-bootstrap.csv:ro
  59. # EMQX 5.x prefers env-var config over emqx.conf. The HOCON
  60. # path is emqx.conf → base.hocon → cluster.hocon → env vars
  61. # (highest precedence). The double-underscore separator in
  62. # env-var names maps to nested HOCON keys.
  63. environment:
  64. # Built-in-db authentication (one chain, password_based, plain)
  65. EMQX_AUTHENTICATION__1__BACKEND: "built_in_database"
  66. EMQX_AUTHENTICATION__1__MECHANISM: "password_based"
  67. EMQX_AUTHENTICATION__1__USER_ID_TYPE: "username"
  68. EMQX_AUTHENTICATION__1__PASSWORD_HASH_ALGORITHM__NAME: "plain"
  69. EMQX_AUTHENTICATION__1__PASSWORD_HASH_ALGORITHM__SALT_POSITION: "disable"
  70. # File-based authorization, default-deny
  71. EMQX_AUTHORIZATION__NO_MATCH: "deny"
  72. EMQX_AUTHORIZATION__DENY_ACTION: "disconnect"
  73. EMQX_AUTHORIZATION__SOURCES__1__TYPE: "file"
  74. EMQX_AUTHORIZATION__SOURCES__1__ENABLE: "true"
  75. EMQX_AUTHORIZATION__SOURCES__1__PATH: "/opt/emqx/etc/acl.conf"
  76. healthcheck:
  77. # The original `echo > /dev/tcp/...` ran under sh on
  78. # Debian-based EMQX and reported unhealthy even when the
  79. # broker was fine. Switch to `bash -c` and a TCP probe.
  80. test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/1883"]
  81. interval: 10s
  82. timeout: 5s
  83. retries: 20
  84. clickhouse:
  85. image: clickhouse/clickhouse-server:24-alpine
  86. ports: ["8123:8123", "9000:9000"]
  87. volumes:
  88. - chdata:/var/lib/clickhouse
  89. ulimits:
  90. nofile: { soft: 262144, hard: 262144 }
  91. # ── App tier ─────────────────────────────────────────────────────
  92. ingestd:
  93. build: .
  94. command: ["/app/ingestd"]
  95. environment:
  96. BA_ENV: dev
  97. BA_HTTP_ADDR: ":8800"
  98. BA_NATS_URL: nats://nats:4222
  99. BA_REDIS_URL: redis://redis:6379/0
  100. BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable
  101. BA_INGESTD_SOURCES: "acme-001:prom-prod:s3cret-acme,globex-002:grafana:s3cret-globex"
  102. BA_INGESTD_RATE_LIMIT_PER_SOURCE: "100"
  103. BA_INGESTD_RATE_LIMIT_PER_COMPANY: "10000"
  104. # M4: MQTT subscriber. ingestd subscribes to ba/+/+/incoming
  105. # with the dedicated `ingestd` user. The auth file lists
  106. # this user (password "ingestd-broker-only") so EMQX's
  107. # built-in-db authenticates the connection.
  108. BA_INGESTD_MQTT_BROKER: "tcp://emqx:1883"
  109. BA_INGESTD_MQTT_USERNAME: "ingestd"
  110. BA_INGESTD_MQTT_PASSWORD: "ingestd-broker-only"
  111. BA_INGESTD_MQTT_SUBSCRIBE: "ba/+/+/incoming"
  112. # M5: WebSocket ingest + live tail. TAIL_TOKEN gates the
  113. # /v1/tail/ws endpoint; a static token is fine for the
  114. # dev path; M11 swaps for JWT.
  115. BA_INGESTD_TAIL_TOKEN: "tail-dev-token-please-change-in-prod"
  116. BA_INGESTD_MAX_CONCURRENT_PER_IP: "32"
  117. BA_INGESTD_DEDUPE_TTL_SECONDS: "300"
  118. # M9 layer 6: circuit breaker (trips after 5 failures in 10s, 30s open)
  119. BA_INGESTD_CB_FAILURE_THRESHOLD: "5"
  120. BA_INGESTD_CB_FAILURE_WINDOW_SECS: "10"
  121. BA_INGESTD_CB_OPEN_DURATION_SECS: "30"
  122. BA_INGESTD_CB_MAX_HALF_OPEN: "1"
  123. # M9 layer 7: quarantine (bans source at 100 hits/5min for 10min)
  124. BA_INGESTD_QUARANTINE_HITS_THRESHOLD: "100"
  125. BA_INGESTD_QUARANTINE_WINDOW_SECONDS: "300"
  126. BA_INGESTD_QUARANTINE_DURATION_SECONDS: "600"
  127. ports: ["8800:8800"]
  128. depends_on:
  129. nats: { condition: service_healthy }
  130. redis: { condition: service_healthy }
  131. postgres: { condition: service_healthy }
  132. emqx: { condition: service_healthy }
  133. routerd:
  134. build: .
  135. command: ["/app/routerd"]
  136. environment:
  137. BA_ENV: dev
  138. BA_HTTP_ADDR: ":8801"
  139. BA_NATS_URL: nats://nats:4222
  140. BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable
  141. # M6.5: router-level dedupe collapse window. A burst of
  142. # identical alerts is held for up to this many ms, then
  143. # a single delivery is fanned out with the final
  144. # dedupe_count. A continuous stream re-flushes every
  145. # DedupeFlushMs.
  146. BA_ROUTERD_DEDUPE_FLUSH_MS: "2000"
  147. ports: ["8801:8801"]
  148. depends_on:
  149. nats: { condition: service_healthy }
  150. postgres: { condition: service_healthy }
  151. deliverd-fcm:
  152. build: .
  153. command: ["/app/deliverd-fcm"]
  154. environment:
  155. BA_ENV: dev
  156. BA_HTTP_ADDR: ":8802"
  157. BA_NATS_URL: nats://nats:4222
  158. BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable
  159. BA_FAKECMD_URL: "http://fakefcmd:8820"
  160. # M8: in-process retry. Defaults match
  161. # internal/retry.Default() — 10 attempts, base
  162. # 100ms, cap 2s, total budget 30s. Tuned to
  163. # terminate in ~12s for a fully failing target
  164. # so a single misbehaving source can't tie up a
  165. # consumer.
  166. BA_DELIVERD_MAX_ATTEMPTS: "10"
  167. BA_DELIVERD_RETRY_BASE_MS: "100"
  168. BA_DELIVERD_RETRY_MAX_MS: "2000"
  169. BA_DELIVERD_RETRY_BUDGET_MS: "30000"
  170. ports: ["8802:8802"]
  171. depends_on:
  172. nats: { condition: service_healthy }
  173. postgres: { condition: service_healthy }
  174. fakefcmd: { condition: service_started }
  175. deliverd-telegram:
  176. build: .
  177. command: ["/app/deliverd-telegram"]
  178. environment:
  179. BA_ENV: dev
  180. BA_HTTP_ADDR: ":8821"
  181. BA_NATS_URL: nats://nats:4222
  182. BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable
  183. BA_TELEGRAM_BOT_TOKEN: "fake-tg-bot-token-acme-001"
  184. BA_TELEGRAM_FAKE_URL: "http://faketgmd:8830"
  185. # M8: same retry knobs as deliverd-fcm.
  186. BA_DELIVERD_MAX_ATTEMPTS: "10"
  187. BA_DELIVERD_RETRY_BASE_MS: "100"
  188. BA_DELIVERD_RETRY_MAX_MS: "2000"
  189. BA_DELIVERD_RETRY_BUDGET_MS: "30000"
  190. ports: ["8821:8821"]
  191. depends_on:
  192. nats: { condition: service_healthy }
  193. postgres: { condition: service_healthy }
  194. faketgmd: { condition: service_started }
  195. telegramd:
  196. build: .
  197. command: ["/app/telegramd"]
  198. environment:
  199. BA_ENV: dev
  200. BA_HTTP_ADDR: ":8822"
  201. BA_NATS_URL: nats://nats:4222
  202. BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable
  203. BA_TELEGRAM_FAKE_URL: "http://faketgmd:8830"
  204. ports: ["8822:8822"]
  205. depends_on:
  206. nats: { condition: service_healthy }
  207. postgres: { condition: service_healthy }
  208. faketgmd: { condition: service_started }
  209. faketgmd:
  210. build: .
  211. command: ["/app/faketgmd", "--addr", ":8830", "--timeout", "5"]
  212. ports: ["8830:8830"]
  213. fakefcmd:
  214. build: .
  215. command: ["/app/fakefcmd", "--addr", ":8820"]
  216. ports: ["8820:8820"]
  217. healthcheck:
  218. test: ["CMD", "wget", "-qO-", "http://localhost:8820/health"]
  219. interval: 5s
  220. timeout: 3s
  221. retries: 10
  222. seed:
  223. build: .
  224. command: ["/app/seed"]
  225. environment:
  226. BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable
  227. BA_MIGRATIONS_DIR: /migrations
  228. volumes:
  229. - ./migrations:/migrations:ro
  230. depends_on:
  231. postgres: { condition: service_healthy }
  232. admind:
  233. build: .
  234. command: ["/app/admind"]
  235. environment:
  236. BA_ENV: dev
  237. BA_HTTP_ADDR: ":8803"
  238. BA_NATS_URL: nats://nats:4222
  239. BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable
  240. # M8: admind connects to NATS to publish DLQ
  241. # replays back onto the deliveries.<chan>.<co>
  242. # subject. The HTTPAddr serves /v1/ping, /v1/dlq*,
  243. # /dlq, /health, /metrics.
  244. ports: ["8803:8803"]
  245. depends_on:
  246. nats: { condition: service_healthy }
  247. postgres: { condition: service_healthy }
  248. archiverd:
  249. build: .
  250. command: ["/app/archiverd"]
  251. environment:
  252. BA_ENV: dev
  253. BA_HTTP_ADDR: ":8804"
  254. BA_POSTGRES_DSN: postgres://ba:ba@postgres:5432/ba?sslmode=disable
  255. # M7: archiver cadence + retention cutoff. The
  256. # Timescale retention policy does the same at 7d;
  257. # the archiver just runs ahead so ClickHouse has
  258. # the data before TS drops it.
  259. BA_ARCHIVERD_RUN_EVERY_SECONDS: "3600"
  260. BA_ARCHIVERD_OLDER_THAN_HOURS: "168"
  261. BA_ARCHIVERD_BATCH_SIZE: "10000"
  262. BA_ARCHIVERD_CLICKHOUSE_URL: "http://clickhouse:8123"
  263. ports: ["8804:8804"]
  264. depends_on:
  265. postgres: { condition: service_healthy }
  266. clickhouse: { condition: service_started }
  267. # ── Observability ────────────────────────────────────────────────
  268. prometheus:
  269. image: prom/prometheus:latest
  270. command:
  271. - --config.file=/etc/prometheus/prometheus.yml
  272. volumes:
  273. - ./deploy/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
  274. ports: ["9090:9090"]
  275. depends_on: [ingestd, routerd, deliverd-fcm, deliverd-telegram, telegramd, admind]
  276. grafana:
  277. image: grafana/grafana:latest
  278. # :3000 is held by gogs (git3) on this host. Map to :3001
  279. # on the host, keep :3000 internal. M0 ports convention
  280. # says canonical ports stay; the host-port override is a
  281. # one-line exception, fully isolated to grafana.
  282. ports: ["3001:3000"]
  283. environment:
  284. GF_SECURITY_ADMIN_USER: admin
  285. GF_SECURITY_ADMIN_PASSWORD: admin
  286. GF_SECURITY_DISABLE_LOGIN_FORM: "false"
  287. # M9: provisioning paths for dashboards + datasources.
  288. GF_PATHS_PROVISIONING: /etc/grafana/provisioning
  289. GF_DASHBOARDS_ENABLED: "true"
  290. volumes:
  291. - ./deploy/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
  292. - ./deploy/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
  293. - ./deploy/grafana/provisioning/dashboards/broad-announce-overview.json:/var/lib/grafana/dashboards/broad-announce-overview.json:ro
  294. depends_on: [prometheus]
  295. # ── Loadgen (one-shot smoke profile) ────────────────────
  296. loadgen-http:
  297. build: .
  298. command:
  299. - /app/loadgen-http
  300. - --target=http://ingestd:8800
  301. - --api-key=acme-001:prom-prod:s3cret-acme
  302. - --mode=normal
  303. - --rate=50
  304. - --duration=30s
  305. - --metrics=:8891
  306. - --instance=loadgen-http
  307. - --cluster-id=smoke
  308. profiles: ["loadgen"]
  309. depends_on:
  310. ingestd: { condition: service_started }
  311. # M10: 3-instance cluster hitting 5k/s. Each instance does ~1.7k/s.
  312. # Per-company cap is 10k/s, so 3 × 1.7k = 5.1k is safe.
  313. # Use `docker compose --profile loadgen-m10 up -d` to bring up all 3.
  314. loadgen-http-1:
  315. build: .
  316. command:
  317. - /app/loadgen-http
  318. - --target=http://ingestd:8800
  319. - --api-key=acme-001:prom-prod:s3cret-acme-001
  320. - --mode=normal
  321. - --rate=1700
  322. - --duration=10m
  323. - --ramp-up=30s
  324. - --metrics=:8891
  325. - --instance=loadgen-http-1
  326. - --cluster-id=m10
  327. profiles: ["loadgen-m10"]
  328. depends_on:
  329. ingestd: { condition: service_started }
  330. loadgen-http-2:
  331. build: .
  332. command:
  333. - /app/loadgen-http
  334. - --target=http://ingestd:8800
  335. - --api-key=acme-002:prom-prod:s3cret-acme-002
  336. - --mode=normal
  337. - --rate=1700
  338. - --duration=10m
  339. - --ramp-up=30s
  340. - --metrics=:8891
  341. - --instance=loadgen-http-2
  342. - --cluster-id=m10
  343. profiles: ["loadgen-m10"]
  344. depends_on:
  345. ingestd: { condition: service_started }
  346. loadgen-http-3:
  347. build: .
  348. command:
  349. - /app/loadgen-http
  350. - --target=http://ingestd:8800
  351. - --api-key=acme-003:prom-prod:s3cret-acme-003
  352. - --mode=normal
  353. - --rate=1700
  354. - --duration=10m
  355. - --ramp-up=30s
  356. - --metrics=:8891
  357. - --instance=loadgen-http-3
  358. - --cluster-id=m10
  359. profiles: ["loadgen-m10"]
  360. depends_on:
  361. ingestd: { condition: service_started }
  362. # M10-bench: delivery tier stubbed with deliverd-bench (no-op).
  363. # No FCM, no Telegram, no Postgres writes — just consume and ACK.
  364. # Allows broker+router ceiling testing at 50k/s without burning FCM credits.
  365. deliverd-bench:
  366. build: .
  367. command: ["/app/deliverd-bench"]
  368. environment:
  369. BA_ENV: dev
  370. BA_NATS_URL: nats://nats:4222
  371. profiles: ["bench"]
  372. depends_on:
  373. nats: { condition: service_healthy }
  374. # 10-instance loadgen cluster for 50k/s bench (5 × 5k instances × 2 = 50k).
  375. # Split across 2 host machines in production; on docker-compose single-host
  376. # we run 10 instances at 5k/s each for a total of 50k/s.
  377. loadgen-bench-1:
  378. build: .
  379. command:
  380. - /app/loadgen-http
  381. - --target=http://ingestd:8800
  382. - --api-key=acme-bench:prom-bench:s3cret-bench
  383. - --mode=normal
  384. - --rate=5000
  385. - --duration=5m
  386. - --ramp-up=15s
  387. - --metrics=:8891
  388. - --instance=loadgen-bench-1
  389. - --cluster-id=m10-bench
  390. profiles: ["bench"]
  391. depends_on:
  392. ingestd: { condition: service_started }
  393. loadgen-bench-2:
  394. build: .
  395. command:
  396. - /app/loadgen-http
  397. - --target=http://ingestd:8800
  398. - --api-key=acme-bench:prom-bench:s3cret-bench
  399. - --mode=normal
  400. - --rate=5000
  401. - --duration=5m
  402. - --ramp-up=15s
  403. - --metrics=:8891
  404. - --instance=loadgen-bench-2
  405. - --cluster-id=m10-bench
  406. profiles: ["bench"]
  407. depends_on:
  408. ingestd: { condition: service_started }
  409. volumes:
  410. pgdata: {}
  411. natsdata: {}
  412. chdata: {}