mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-18 16:01:18 +00:00
2430095197
Remove DynamoDB-backed storage paths, add Postgres/HTTP repositories for mailbox state maps, wire the internal message-map API, and add provisioning runner/migration plumbing.
498 lines
16 KiB
YAML
498 lines
16 KiB
YAML
# Warmbly full dev/simulation stack.
|
|
#
|
|
# Profiles:
|
|
# (default) infrastructure + app services (postgres, redis, kafka,
|
|
# schema-registry, mailpit, cloud-tasks, localstack, backend,
|
|
# consumer, tracking, realtime, web, one worker)
|
|
# sim adds 2 more workers (premium + dedicated) so assignment,
|
|
# rebalancing, and per-pool routing have real targets
|
|
# seed one-shot seeder; safe to re-run
|
|
# tools debugging UIs (kafka-ui, mailpit already in default)
|
|
#
|
|
# Usage:
|
|
# docker compose up # default profile
|
|
# docker compose --profile sim up # full simulation
|
|
# docker compose --profile seed run --rm seed
|
|
# docker compose --profile tools up -d kafka-ui
|
|
#
|
|
# Standard ports throughout, with a few exceptions kept offset:
|
|
# - postgres 15432 (5432 commonly bound by system Postgres)
|
|
# - redis 16379 (6379 commonly bound by sibling projects)
|
|
# - mailpit ui 18025 / smtp 11025 (8025/1025 commonly bound by
|
|
# sibling projects)
|
|
# - kafka-ui 18090 (8080 is taken by backend)
|
|
#
|
|
# Standard ports for everything else:
|
|
# backend 8080, tracking 3000, realtime 4000, web 5173,
|
|
# kafka 9092, schema-registry 8081, localstack 4566,
|
|
# stripe-mock 12111, cloud-tasks 8123.
|
|
|
|
x-aws-localstack: &aws-localstack
|
|
AWS_ENDPOINT_URL: http://localstack:4566
|
|
AWS_REGION: us-east-1
|
|
AWS_ACCESS_KEY_ID: test
|
|
AWS_SECRET_ACCESS_KEY: test
|
|
|
|
x-worker-base: &worker-base
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/worker.Dockerfile
|
|
restart: unless-stopped
|
|
environment: &worker-env
|
|
<<: *aws-localstack
|
|
APP_ENV: dev
|
|
AWS_CONFIG_ENABLED: "false"
|
|
REDIS: redis://redis:6379
|
|
KAFKA_BOOTSTRAP_SERVERS: kafka:29092
|
|
SCHEMA_REGISTRY_URL: http://schema-registry:8081
|
|
depends_on:
|
|
kafka:
|
|
condition: service_healthy
|
|
kafka-init:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
schema-registry:
|
|
condition: service_started
|
|
localstack-init:
|
|
condition: service_completed_successfully
|
|
|
|
services:
|
|
|
|
# infrastructure
|
|
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_USER: warmbly
|
|
POSTGRES_PASSWORD: warmbly
|
|
POSTGRES_DB: warmbly_dev
|
|
# 5432 is more often already taken on dev machines (system Postgres,
|
|
# another docker-compose project, etc.) than any other port in this
|
|
# stack — keep the offset for it specifically.
|
|
ports: ["15432:5432"]
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U warmbly"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
# 6379 commonly conflicts with another docker-compose project's redis.
|
|
ports: ["16379:6379"]
|
|
volumes:
|
|
- redis_data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
zookeeper:
|
|
image: confluentinc/cp-zookeeper:7.5.0
|
|
environment:
|
|
ZOOKEEPER_CLIENT_PORT: 2181
|
|
ZOOKEEPER_TICK_TIME: 2000
|
|
healthcheck:
|
|
# Kafka connecting to zookeeper while zookeeper is still loading
|
|
# the snapshot opens the TCP socket but never gets a session, then
|
|
# times out after 40s and the kafka container exits. Gate kafka's
|
|
# depends_on on this healthcheck so it only fires after zookeeper
|
|
# is actually ready to serve sessions.
|
|
#
|
|
# `srvr` is whitelisted by default on cp-zookeeper; only outputs
|
|
# the "Mode:" line once the server is fully initialized. Using it
|
|
# avoids needing to tweak 4lw.commands.whitelist.
|
|
test: ["CMD-SHELL", "echo srvr | ncat -w 2 localhost 2181 | grep -q Mode"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 20
|
|
start_period: 5s
|
|
|
|
kafka:
|
|
image: confluentinc/cp-kafka:7.5.0
|
|
depends_on:
|
|
zookeeper:
|
|
condition: service_healthy
|
|
ports: ["9092:9092"]
|
|
environment:
|
|
KAFKA_BROKER_ID: 1
|
|
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
|
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,PLAINTEXT_HOST://0.0.0.0:9092
|
|
# PLAINTEXT_HOST is what host-run clients (native `make run`, or a dev
|
|
# box on another machine) connect to. It must advertise an address the
|
|
# client can actually reach: leave it `localhost` for same-box dev, or
|
|
# set KAFKA_ADVERTISED_HOST=<this machine's LAN IP/hostname> before
|
|
# `make infra` so off-box clients aren't redirected back to their own
|
|
# localhost.
|
|
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://${KAFKA_ADVERTISED_HOST:-localhost}:9092
|
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
|
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
|
healthcheck:
|
|
test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:29092"]
|
|
interval: 10s
|
|
timeout: 10s
|
|
retries: 12
|
|
start_period: 30s
|
|
|
|
kafka-init:
|
|
image: confluentinc/cp-kafka:7.5.0
|
|
depends_on:
|
|
kafka:
|
|
condition: service_healthy
|
|
entrypoint: ["/bin/bash", "-lc"]
|
|
# Pre-creates the Kafka topics used by the platform. Topic names use "."
|
|
# separators (the only separator legal in both Kafka topic names and NATS
|
|
# subjects — see internal/infrastructure/kafka/topics.go). The per-worker
|
|
# "w.<uuid>" command topics are not listed here: they are created on demand
|
|
# via auto-create (KAFKA_AUTO_CREATE_TOPICS_ENABLE) the first time the
|
|
# backend publishes to a worker, so the worker's startup subscription warns
|
|
# only briefly until then.
|
|
command:
|
|
- >-
|
|
for topic in tracking-events email-events warmup-events jobs.worker-events; do
|
|
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic "$$topic" --partitions 1 --replication-factor 1;
|
|
done
|
|
|
|
schema-registry:
|
|
image: confluentinc/cp-schema-registry:7.5.0
|
|
depends_on:
|
|
kafka:
|
|
condition: service_healthy
|
|
ports: ["8081:8081"]
|
|
environment:
|
|
SCHEMA_REGISTRY_HOST_NAME: schema-registry
|
|
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: kafka:29092
|
|
|
|
cloud-tasks-emulator:
|
|
image: ghcr.io/aertje/cloud-tasks-emulator:latest
|
|
command:
|
|
- "-host"
|
|
- "0.0.0.0"
|
|
- "-port"
|
|
- "8123"
|
|
- "-queue"
|
|
- "projects/local/locations/local/queues/default"
|
|
ports: ["8123:8123"]
|
|
|
|
mailpit:
|
|
image: axllent/mailpit:latest
|
|
# 8025 / 1025 are common defaults in other docker-compose stacks
|
|
# (any project with mailpit). Offsetting to avoid conflict.
|
|
ports:
|
|
- "18025:8025" # web UI
|
|
- "11025:1025" # SMTP
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
localstack:
|
|
image: localstack/localstack:3.7
|
|
ports:
|
|
- "4566:4566"
|
|
environment:
|
|
SERVICES: kms,s3
|
|
DEBUG: "0"
|
|
AWS_DEFAULT_REGION: us-east-1
|
|
PERSISTENCE: "0"
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
healthcheck:
|
|
# LocalStack reports each service as 'available' (initialised, never
|
|
# called) or 'running' (initialised + handled at least one request).
|
|
# Either state means KMS is up; the previous check only matched
|
|
# 'available' and flipped to unhealthy the moment anything used KMS.
|
|
test: ["CMD-SHELL", "curl -fsS http://localhost:4566/_localstack/health | grep -qE '\"kms\": \"(available|running)\"'"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 30
|
|
start_period: 10s
|
|
|
|
localstack-init:
|
|
image: amazon/aws-cli:2.15.0
|
|
depends_on:
|
|
localstack:
|
|
condition: service_healthy
|
|
environment:
|
|
<<: *aws-localstack
|
|
entrypoint: ["/bin/sh", "/init/localstack-init.sh"]
|
|
volumes:
|
|
- ./scripts/localstack-init.sh:/init/localstack-init.sh:ro
|
|
|
|
stripe-mock:
|
|
image: stripe/stripe-mock:latest
|
|
ports:
|
|
- "12111:12111"
|
|
- "12112:12112"
|
|
|
|
# app: backend
|
|
|
|
backend:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/backend.Dockerfile
|
|
ports: ["8080:8080"]
|
|
environment:
|
|
<<: *aws-localstack
|
|
APP_ENV: dev
|
|
AWS_CONFIG_ENABLED: "false"
|
|
|
|
API_HOST: "0.0.0.0:8080"
|
|
GIN_MODE: debug
|
|
# Web origin for CORS. Backend's allow-list resolution checks
|
|
# CORS_ALLOW_ORIGINS, then APP_URL, then a fallback derived from
|
|
# WEBSOCKET_URL. Without this the origin would default to the
|
|
# realtime port (4000), not the Vite dev server (5173), and
|
|
# browser preflights from the dashboard get 403'd.
|
|
APP_URL: "http://localhost:5173"
|
|
WEBSOCKET_URL: ws://localhost:4000/socket/websocket
|
|
|
|
PRIMARY_DB: postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable
|
|
REDIS: redis://redis:6379
|
|
GEODB_PATH: /app/data/GeoLite2-City.mmdb
|
|
|
|
KAFKA_BOOTSTRAP_SERVERS: kafka:29092
|
|
KAFKA_CONSUMER_GROUP: consumer-group
|
|
KAFKA_TRACKING_TOPIC: tracking-events
|
|
SCHEMA_REGISTRY_URL: http://schema-registry:8081
|
|
|
|
AUTH_SECRET: local-dev-auth-secret-minimum-32-characters-long
|
|
GOOGLE_CLIENT_ID: local-google-client-id
|
|
GOOGLE_CLIENT_SECRET: local-google-client-secret
|
|
GOOGLE_REDIRECT_URI: http://localhost:3000/auth/google/callback
|
|
APPLE_APP_ID: local-apple-app-id
|
|
APPLE_TEAM_ID: local-apple-team-id
|
|
APPLE_KEY_ID: local-apple-key-id
|
|
APPLE_KEY_SECRET: local-apple-key-secret-base64
|
|
TURNSTILE_SECRET: 1x0000000000000000000000000000000AA
|
|
TURNSTILE_BYPASS_TOKEN: warmbly-local-turnstile-bypass
|
|
|
|
STRIPE_API_BASE: http://stripe-mock:12111
|
|
STRIPE_SECRET_KEY: sk_test_local
|
|
STRIPE_WEBHOOK_SECRET: whsec_local
|
|
STRIPE_PUBLISHABLE_KEY: pk_test_local
|
|
|
|
EMAIL_NAME: Warmbly Dev
|
|
EMAIL_ADDRESS: dev@warmbly.local
|
|
TRACKING_DOMAIN: localhost:3000
|
|
|
|
SMTP_HOST: mailpit
|
|
SMTP_PORT: "1025"
|
|
|
|
GCP_PROJECT_ID: ""
|
|
GOOGLE_APPLICATION_CREDENTIALS_JSON: "dev@local.iam.gserviceaccount.com"
|
|
|
|
CLOUD_TASKS_EMULATOR_HOST: cloud-tasks-emulator:8123
|
|
CLOUD_TASKS_QUEUE_NAME: projects/local/locations/local/queues/default
|
|
CLOUD_TASKS_WEBHOOK_URL: http://backend:8080/webhook/email
|
|
|
|
ASTRA_DB_ID: local-astra-db-id
|
|
ASTRA_DB_REGION: local-region
|
|
ASTRA_KEYSPACE_NAME: warmbly_dev
|
|
ASTRA_APPLICATION_TOKEN: local-astra-token
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
kafka:
|
|
condition: service_healthy
|
|
kafka-init:
|
|
condition: service_completed_successfully
|
|
schema-registry:
|
|
condition: service_started
|
|
cloud-tasks-emulator:
|
|
condition: service_started
|
|
localstack-init:
|
|
condition: service_completed_successfully
|
|
stripe-mock:
|
|
condition: service_started
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 10
|
|
start_period: 20s
|
|
|
|
consumer:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/consumer.Dockerfile
|
|
environment:
|
|
<<: *aws-localstack
|
|
APP_ENV: dev
|
|
AWS_CONFIG_ENABLED: "false"
|
|
|
|
PRIMARY_DB: postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable
|
|
REDIS: redis://redis:6379
|
|
KAFKA_BOOTSTRAP_SERVERS: kafka:29092
|
|
KAFKA_CONSUMER_GROUP: consumer-group
|
|
SCHEMA_REGISTRY_URL: http://schema-registry:8081
|
|
|
|
ASTRA_DB_ID: local-astra-db-id
|
|
ASTRA_DB_REGION: local-region
|
|
ASTRA_KEYSPACE_NAME: warmbly_dev
|
|
ASTRA_APPLICATION_TOKEN: local-astra-token
|
|
GCP_PROJECT_ID: ""
|
|
depends_on:
|
|
backend:
|
|
condition: service_healthy
|
|
kafka:
|
|
condition: service_healthy
|
|
kafka-init:
|
|
condition: service_completed_successfully
|
|
schema-registry:
|
|
condition: service_started
|
|
|
|
# workers
|
|
# Hostnames are deterministic UUIDv5(URL, "worker-N") so the worker can
|
|
# parse os.Hostname() back into its UUID. Same value every boot.
|
|
|
|
worker-shared-1:
|
|
<<: *worker-base
|
|
hostname: "10c8f5e4-1c39-5b2a-9c8b-3d2f0a8b1a01"
|
|
environment:
|
|
<<: *worker-env
|
|
WORKER_TIER: shared
|
|
|
|
worker-premium-1:
|
|
<<: *worker-base
|
|
hostname: "10c8f5e4-1c39-5b2a-9c8b-3d2f0a8b1a02"
|
|
environment:
|
|
<<: *worker-env
|
|
WORKER_TIER: premium
|
|
profiles: ["sim"]
|
|
|
|
worker-dedicated-1:
|
|
<<: *worker-base
|
|
hostname: "10c8f5e4-1c39-5b2a-9c8b-3d2f0a8b1a03"
|
|
environment:
|
|
<<: *worker-env
|
|
WORKER_TIER: dedicated
|
|
profiles: ["sim"]
|
|
|
|
# tracking + realtime + web
|
|
|
|
tracking:
|
|
build:
|
|
context: ./tracking
|
|
dockerfile: Dockerfile
|
|
ports: ["3000:3000"]
|
|
environment:
|
|
APP_ENV: dev
|
|
AWS_CONFIG_ENABLED: "false"
|
|
TRACKING_HOST: "0.0.0.0"
|
|
TRACKING_PORT: "3000"
|
|
KAFKA_BOOTSTRAP_SERVERS: kafka:29092
|
|
KAFKA_TRACKING_TOPIC: tracking-events
|
|
SCHEMA_REGISTRY_URL: http://schema-registry:8081
|
|
depends_on:
|
|
kafka:
|
|
condition: service_healthy
|
|
kafka-init:
|
|
condition: service_completed_successfully
|
|
schema-registry:
|
|
condition: service_started
|
|
|
|
realtime:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/realtime.Dockerfile
|
|
ports: ["4000:4000"]
|
|
environment:
|
|
PHX_HOST: localhost
|
|
PORT: 4000
|
|
DATABASE_URL: postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable
|
|
DATABASE_SSL: "false"
|
|
DATABASE_POOL_SIZE: "10"
|
|
REDIS_URL: redis://redis:6379
|
|
JWT_SECRET: local-dev-auth-secret-minimum-32-characters-long
|
|
SECRET_KEY_BASE: local-development-secret-key-base-minimum-64-characters-for-phoenix
|
|
PUBSUB_ENABLED: "false"
|
|
CHECK_ORIGIN: "false"
|
|
MAX_CONNECTIONS_PER_USER: "10"
|
|
MAX_CONNECTIONS_PER_IP: "50"
|
|
MAX_CONNECTIONS_GLOBAL: "100000"
|
|
RATE_LIMIT_WS_MESSAGE: "120"
|
|
RATE_LIMIT_WS_JOIN: "30"
|
|
RATE_LIMIT_WS_EVENT: "60"
|
|
GCP_PROJECT_ID: ""
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
web:
|
|
image: node:22-alpine
|
|
working_dir: /app
|
|
volumes:
|
|
- ./web:/app
|
|
- web_node_modules:/app/node_modules
|
|
ports: ["5173:5173"]
|
|
# Retry corepack only — that one tends to fail transiently while it
|
|
# downloads the pnpm version. pnpm install failures are PERMANENT (bad
|
|
# package.json, unresolvable peer dep, etc.) so we fail fast instead of
|
|
# looping forever and burying the actual error.
|
|
command: >
|
|
sh -c '
|
|
until corepack enable; do echo "corepack retry..."; sleep 2; done;
|
|
pnpm install || { echo ""; echo "pnpm install failed — fix package.json then run make restart web"; exit 1; };
|
|
pnpm dev --host
|
|
'
|
|
environment:
|
|
VITE_APP_URL: "http://localhost:5173"
|
|
VITE_API_URL: "http://localhost:8080"
|
|
VITE_TRACKING_DOMAIN: "localhost:3000"
|
|
VITE_TURNSTILE_KEY: "1x00000000000000000000AA"
|
|
VITE_TURNSTILE_BYPASS_TOKEN: "warmbly-local-turnstile-bypass"
|
|
depends_on:
|
|
backend:
|
|
condition: service_healthy
|
|
|
|
# one-shots
|
|
|
|
seed:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/backend.Dockerfile
|
|
entrypoint: ["/app/seed"]
|
|
environment:
|
|
<<: *aws-localstack
|
|
PRIMARY_DB: postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable
|
|
# SEED_RICH: 3 orgs, 3 workers, 6 mailboxes, 1 campaign, 10 contacts.
|
|
# SEED_FULL: layers on plans + subscriptions, admin/manager/viewer users,
|
|
# CRM, API key, audit log, enterprise inquiry. See cmd/seed/main.go.
|
|
SEED_RICH: "true"
|
|
SEED_FULL: "true"
|
|
depends_on:
|
|
backend:
|
|
condition: service_healthy
|
|
profiles: ["seed"]
|
|
|
|
# tools (debugging UIs)
|
|
|
|
kafka-ui:
|
|
image: provectuslabs/kafka-ui:latest
|
|
ports: ["18090:8080"]
|
|
environment:
|
|
KAFKA_CLUSTERS_0_NAME: local
|
|
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
|
|
KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry:8081
|
|
depends_on:
|
|
kafka:
|
|
condition: service_healthy
|
|
profiles: ["tools"]
|
|
|
|
volumes:
|
|
postgres_data:
|
|
redis_data:
|
|
web_node_modules:
|