mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-18 16:01:18 +00:00
482 lines
21 KiB
YAML
482 lines
21 KiB
YAML
# Warmbly — truly local, no-cloud self-host stack.
|
|
#
|
|
# docker compose up --build
|
|
#
|
|
# Brings up the whole platform with NO cloud account of any kind: no AWS, no
|
|
# GCP, no Stripe, no Kafka, no Astra. One command, everything local:
|
|
#
|
|
# Postgres — relational store
|
|
# Redis — cache + realtime pub/sub bridge
|
|
# NATS — event bus (JetStream); a single ~15MB binary, not Kafka+ZK+SR
|
|
# backend — API (:8080), applies migrations on boot
|
|
# consumer — event consumer
|
|
# worker — send/sync executor (add more for scale; see docs)
|
|
# tracking — open/click pixels (:3000)
|
|
# realtime — websocket fanout (:4000)
|
|
# web — dashboard (:5173) admin — control panel (:5174)
|
|
# mailpit — local SMTP sink + web UI (:18025) for testing outbound mail
|
|
#
|
|
# Providers (all local): EVENTBUS_PROVIDER=nats, KMS_PROVIDER=local,
|
|
# BLOB_PROVIDER=filesystem, TASKS_PROVIDER=local, CODEC_PROVIDER=json,
|
|
# BILLING_PROVIDER=none, CAPTCHA_PROVIDER=none, PUBSUB_ENABLED=false.
|
|
#
|
|
# ONE .env NEXT TO THIS FILE CONFIGURES EVERYTHING. Every variable below is
|
|
# ${VAR:-default}, so an absent or empty .env still boots the full no-cloud
|
|
# stack and you set only what you want to change. Copy deploy/config/env.example
|
|
# to .env as a starting point.
|
|
#
|
|
# For a real deployment set at least:
|
|
# AUTH_SECRET, KMS_LOCAL_MASTER_KEY (base64 32 bytes; `make gen-key`),
|
|
# CREDENTIALS_ENCRYPTION_KEY (64 hex), INTERNAL_API_TOKEN, SECRET_KEY_BASE,
|
|
# and — to connect Gmail/Microsoft mailboxes — BOX_GOOGLE_* / BOX_OUTLOOK_*.
|
|
#
|
|
# LOSING KMS_LOCAL_MASTER_KEY OR CREDENTIALS_ENCRYPTION_KEY IS UNRECOVERABLE:
|
|
# every stored mailbox credential is sealed with them. Back them up.
|
|
#
|
|
# To run on Kafka + S3 instead, set the provider vars above and rebuild the Go
|
|
# images with --build-arg GO_TAGS=kafka (tracking: --build-arg CARGO_FEATURES=kafka).
|
|
|
|
# Everything below is overridable from a single .env next to this file. The
|
|
# defaults are the no-cloud stack, so an empty .env still boots; set only what
|
|
# you want to change. See docs.warmbly.com/development/deployment-guide/.
|
|
x-selfhost-env: &selfhost-env
|
|
APP_ENV: ${APP_ENV:-dev}
|
|
AWS_CONFIG_ENABLED: ${AWS_CONFIG_ENABLED:-false}
|
|
|
|
# Event bus + serialization. Kafka needs images built with GO_TAGS=kafka.
|
|
EVENTBUS_PROVIDER: ${EVENTBUS_PROVIDER:-nats}
|
|
NATS_URL: ${NATS_URL:-nats://nats:4222}
|
|
CODEC_PROVIDER: ${CODEC_PROVIDER:-json}
|
|
KAFKA_BOOTSTRAP_SERVERS: ${KAFKA_BOOTSTRAP_SERVERS:-}
|
|
KAFKA_SASL_USERNAME: ${KAFKA_SASL_USERNAME:-}
|
|
KAFKA_SASL_PASSWORD: ${KAFKA_SASL_PASSWORD:-}
|
|
SCHEMA_REGISTRY_URL: ${SCHEMA_REGISTRY_URL:-}
|
|
|
|
# Encryption. Back up both keys; losing them is unrecoverable.
|
|
KMS_PROVIDER: ${KMS_PROVIDER:-local}
|
|
KMS_LOCAL_MASTER_KEY: ${KMS_LOCAL_MASTER_KEY:-Xr0JA7gqF2POy29a7MRByyqddivTNt8WOyKsOXklazk=}
|
|
KMS_AWS_KEY_ID: ${KMS_AWS_KEY_ID:-}
|
|
CREDENTIALS_ENCRYPTION_KEY: ${CREDENTIALS_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef}
|
|
|
|
# Blob storage. s3 also needs BLOB_BUCKET + the AWS_* values below.
|
|
BLOB_PROVIDER: ${BLOB_PROVIDER:-filesystem}
|
|
BLOB_FS_ROOT: ${BLOB_FS_ROOT:-/data/blobs}
|
|
BLOB_BUCKET: ${BLOB_BUCKET:-}
|
|
BLOB_PUBLIC_BASE_URL: ${BLOB_PUBLIC_BASE_URL:-${API_PUBLIC_URL:-http://${PUBLIC_HOST:-localhost}:8080}/public}
|
|
AWS_REGION: ${AWS_REGION:-}
|
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
|
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
|
|
AWS_ENDPOINT_URL_S3: ${AWS_ENDPOINT_URL_S3:-}
|
|
|
|
TASKS_PROVIDER: ${TASKS_PROVIDER:-local}
|
|
PUBSUB_ENABLED: ${PUBSUB_ENABLED:-false}
|
|
|
|
# Billing. stripe requires all three keys or the backend exits at boot.
|
|
BILLING_PROVIDER: ${BILLING_PROVIDER:-none}
|
|
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
|
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
|
|
STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUBLISHABLE_KEY:-}
|
|
|
|
# Captcha. Set both to turn Turnstile on.
|
|
CAPTCHA_PROVIDER: ${CAPTCHA_PROVIDER:-none}
|
|
TURNSTILE_SECRET: ${TURNSTILE_SECRET:-}
|
|
|
|
# Deployment mode. Picks the auth defaults below; every one stays
|
|
# individually overridable.
|
|
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self_hosted}
|
|
|
|
# ── Platform email (login codes, resets, invitations, digests) ──────────
|
|
#
|
|
# log is the default so a fresh install can complete first login with no
|
|
# relay: the code is printed to `docker compose logs backend`. Switch to smtp
|
|
# before anyone else uses this instance.
|
|
#
|
|
# MAIL_TRANSPORT=smtp
|
|
# SMTP_HOST=smtp.example.com SMTP_USERNAME=... SMTP_PASSWORD=...
|
|
#
|
|
# SMTP_SECURITY is starttls (587), tls (implicit, 465) or none (a local sink
|
|
# only). The port defaults per mode, and credentials are never sent over an
|
|
# unencrypted connection.
|
|
MAIL_TRANSPORT: ${MAIL_TRANSPORT:-log}
|
|
EMAIL_NAME: ${EMAIL_NAME:-Warmbly}
|
|
EMAIL_ADDRESS: ${EMAIL_ADDRESS:-noreply@localhost}
|
|
SMTP_HOST: ${SMTP_HOST:-}
|
|
SMTP_PORT: ${SMTP_PORT:-}
|
|
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
|
SMTP_SECURITY: ${SMTP_SECURITY:-}
|
|
SMTP_AUTH: ${SMTP_AUTH:-auto}
|
|
SMTP_EHLO_NAME: ${SMTP_EHLO_NAME:-}
|
|
SMTP_TLS_INSECURE_SKIP_VERIFY: ${SMTP_TLS_INSECURE_SKIP_VERIFY:-false}
|
|
|
|
# ── Auth policy ────────────────────────────────────────────────────────
|
|
#
|
|
# AUTH_LOGIN_CODE is always, new_device or off. Self-host defaults to off:
|
|
# emailing a code on every login makes the relay a single point of failure
|
|
# for all authentication, and neither NIST SP 800-63B nor OWASP ASVS counts
|
|
# email as a second factor. TOTP and passkeys both remain available.
|
|
AUTH_LOGIN_CODE: ${AUTH_LOGIN_CODE:-}
|
|
REQUIRE_EMAIL_VERIFICATION: ${REQUIRE_EMAIL_VERIFICATION:-}
|
|
# true | false | invite_only. Self-host defaults to invite_only, with a
|
|
# first-launch exemption so the very first signup always works.
|
|
DISABLE_REGISTRATION: ${DISABLE_REGISTRATION:-}
|
|
DISABLE_PASSWORD_LOGIN: ${DISABLE_PASSWORD_LOGIN:-false}
|
|
AUTH_IP_RATE_LIMIT: ${AUTH_IP_RATE_LIMIT:-60}
|
|
# CIDRs allowed to set X-Forwarded-For. Empty trusts nothing, which is
|
|
# correct for a directly exposed backend; set it when you run a proxy.
|
|
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-}
|
|
|
|
# First owner, read only while the users table is empty. Leave unset and the
|
|
# backend prints a single-use setup link to its logs instead.
|
|
WARMBLY_BOOTSTRAP_EMAIL: ${WARMBLY_BOOTSTRAP_EMAIL:-}
|
|
WARMBLY_BOOTSTRAP_PASSWORD_HASH: ${WARMBLY_BOOTSTRAP_PASSWORD_HASH:-}
|
|
WARMBLY_BOOTSTRAP_PASSWORD: ${WARMBLY_BOOTSTRAP_PASSWORD:-}
|
|
WARMBLY_BOOTSTRAP_ORG: ${WARMBLY_BOOTSTRAP_ORG:-}
|
|
|
|
# Generic OpenID Connect. The one sign-in path with no mail dependency, so
|
|
# it is the recommended posture for a deployment with no relay.
|
|
OIDC_ISSUER_URL: ${OIDC_ISSUER_URL:-}
|
|
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
|
|
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
|
|
OIDC_REDIRECT_URL: ${OIDC_REDIRECT_URL:-}
|
|
OIDC_SCOPES: ${OIDC_SCOPES:-}
|
|
OIDC_ALLOWED_DOMAINS: ${OIDC_ALLOWED_DOMAINS:-}
|
|
OIDC_DEFAULT_ORG: ${OIDC_DEFAULT_ORG:-}
|
|
OIDC_PROVIDER_NAME: ${OIDC_PROVIDER_NAME:-}
|
|
|
|
# Transactional email branding. A self-hosted install should not send mail
|
|
# attributed to someone else's company.
|
|
EMAIL_BRAND_NAME: ${EMAIL_BRAND_NAME:-}
|
|
EMAIL_BRAND_LEGAL_ENTITY: ${EMAIL_BRAND_LEGAL_ENTITY:-}
|
|
EMAIL_BRAND_COMPANY_NUMBER: ${EMAIL_BRAND_COMPANY_NUMBER:-}
|
|
EMAIL_BRAND_PLACE_OF_REG: ${EMAIL_BRAND_PLACE_OF_REG:-}
|
|
EMAIL_BRAND_ADDRESS: ${EMAIL_BRAND_ADDRESS:-}
|
|
EMAIL_BRAND_WEBSITE_URL: ${EMAIL_BRAND_WEBSITE_URL:-}
|
|
EMAIL_BRAND_SUPPORT_EMAIL: ${EMAIL_BRAND_SUPPORT_EMAIL:-}
|
|
EMAIL_BRAND_TERMS_URL: ${EMAIL_BRAND_TERMS_URL:-}
|
|
EMAIL_BRAND_PRIVACY_URL: ${EMAIL_BRAND_PRIVACY_URL:-}
|
|
|
|
PRIMARY_DB: ${PRIMARY_DB:-postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable}
|
|
REDIS: ${REDIS:-redis://redis:6379}
|
|
AUTH_SECRET: ${AUTH_SECRET:-local-dev-auth-secret-minimum-32-characters-long}
|
|
INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-local-dev-internal-token}
|
|
|
|
# AI assistant. Omit AI_PROVIDER to run with AI off (endpoints return 503).
|
|
AI_PROVIDER: ${AI_PROVIDER:-}
|
|
AI_API_KEY: ${AI_API_KEY:-}
|
|
AI_MODEL: ${AI_MODEL:-}
|
|
AI_MODEL_TRIAL: ${AI_MODEL_TRIAL:-}
|
|
AI_MODEL_PAID: ${AI_MODEL_PAID:-}
|
|
AI_BASE_URL: ${AI_BASE_URL:-}
|
|
AI_FREE: ${AI_FREE:-}
|
|
SEARCH_PROVIDER: ${SEARCH_PROVIDER:-}
|
|
SEARCH_API_URL: ${SEARCH_API_URL:-}
|
|
SEARCH_API_KEY: ${SEARCH_API_KEY:-}
|
|
|
|
# Mobile push. Partial config disables push with a warning, never a crash.
|
|
APNS_KEY: ${APNS_KEY:-}
|
|
APNS_KEY_ID: ${APNS_KEY_ID:-}
|
|
APNS_TEAM_ID: ${APNS_TEAM_ID:-}
|
|
APNS_TOPIC: ${APNS_TOPIC:-}
|
|
NOTIFICATION_PUSH_WINDOW: ${NOTIFICATION_PUSH_WINDOW:-}
|
|
NOTIFICATION_EMAIL_DAILY_CAP: ${NOTIFICATION_EMAIL_DAILY_CAP:-}
|
|
|
|
SENTRY_DSN: ${SENTRY_DSN:-}
|
|
|
|
services:
|
|
|
|
# ─── infrastructure ───────────────────────────────────────────────────
|
|
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_USER: warmbly
|
|
POSTGRES_PASSWORD: warmbly
|
|
POSTGRES_DB: warmbly_dev
|
|
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
|
|
ports: ["16379:6379"]
|
|
volumes:
|
|
- redis_data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
nats:
|
|
image: nats:2.10-alpine
|
|
# -js: JetStream (durable streams). -m 8222: HTTP monitoring for healthcheck.
|
|
command: ["-js", "-sd", "/data", "-m", "8222"]
|
|
ports: ["4222:4222", "8222:8222"]
|
|
volumes:
|
|
- nats_data:/data
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8222/healthz"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
# SMTP sink for testing campaign sends without a real mailbox.
|
|
#
|
|
# In the `sandbox` profile, so `make up` never starts it: platform mail goes
|
|
# to the logs (MAIL_TRANSPORT=log) or to your relay, and a real install sends
|
|
# campaign mail through the mailboxes you connect. `make sandbox`, `make dev`
|
|
# and `make infra` start it by naming it explicitly.
|
|
#
|
|
# Bound to loopback when it does run. Its web UI has no authentication, so
|
|
# publishing it would hand every captured message to anyone who can reach the
|
|
# host. Set MAILPIT_BIND=0.0.0.0 if you deliberately want it reachable.
|
|
mailpit:
|
|
image: axllent/mailpit:latest
|
|
profiles: ["sandbox"]
|
|
ports:
|
|
- "${MAILPIT_BIND:-127.0.0.1}:18025:8025"
|
|
- "${MAILPIT_BIND:-127.0.0.1}:11025:1025"
|
|
environment:
|
|
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
|
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
|
|
|
# Local IMAP host for the sandbox demo (`make sandbox`): the mailbox that
|
|
# every seeded sender syncs from, so replies and warmup mail actually land in
|
|
# the unified inbox. Any username logs in with the static password "sandbox";
|
|
# the self-signed cert is fine because sandbox clients set MAIL_TLS_INSECURE.
|
|
# In the `sandbox` profile so the lean `make up` self-host never pulls it —
|
|
# `make sandbox` / `make infra` start it by naming it explicitly.
|
|
dovecot:
|
|
image: dovecot/dovecot:latest
|
|
profiles: ["sandbox"]
|
|
environment:
|
|
USER_PASSWORD: "{PLAIN}sandbox"
|
|
ports:
|
|
- "10143:31143" # IMAP (cleartext; debugging only)
|
|
- "10993:31993" # IMAPS (self-signed cert; the sandbox worker + simulator dial this)
|
|
healthcheck:
|
|
test: ["CMD", "doveadm", "service", "status", "imap-login"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
# ─── application ──────────────────────────────────────────────────────
|
|
|
|
backend:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/backend.Dockerfile
|
|
ports: ["8080:8080"]
|
|
environment:
|
|
<<: *selfhost-env
|
|
API_HOST: "0.0.0.0:8080"
|
|
GIN_MODE: release
|
|
# To reach Warmbly from another machine, set PUBLIC_HOST=<ip-or-domain> in
|
|
# .env — every URL below derives from it. Or set each URL explicitly.
|
|
APP_URL: ${APP_URL:-http://${PUBLIC_HOST:-localhost}:5173}
|
|
# The backend's own public base. Remote workers enrolled from here get it
|
|
# as ENCRYPTED_KEYS_BACKEND_URL, so it must be reachable from the worker.
|
|
API_PUBLIC_URL: ${API_PUBLIC_URL:-http://${PUBLIC_HOST:-localhost}:8080}
|
|
CORS_ALLOW_ORIGINS: ${CORS_ALLOW_ORIGINS:-http://${PUBLIC_HOST:-localhost}:5173,http://${PUBLIC_HOST:-localhost}:5174}
|
|
WEBSOCKET_URL: ${WEBSOCKET_URL:-ws://${PUBLIC_HOST:-localhost}:4000/socket/websocket}
|
|
ENCRYPTED_KEYS_PROVIDER: postgres
|
|
# Mail and auth policy live in the shared anchor above, so the consumer
|
|
# gets them too. It previously received neither, which silently disabled
|
|
# every notification and digest email in this stack.
|
|
TRACKING_DOMAIN: ${TRACKING_DOMAIN:-${PUBLIC_HOST:-localhost}:3000}
|
|
GEODB_PATH: ${GEODB_PATH:-/app/data/GeoLite2-City.mmdb}
|
|
# Gmail mailbox OAuth (leave unset to connect only SMTP/IMAP + Outlook
|
|
# mailboxes). See the self-hosting docs for the Google Cloud setup.
|
|
BOX_GOOGLE_CLIENT_ID: ${BOX_GOOGLE_CLIENT_ID:-}
|
|
BOX_GOOGLE_CLIENT_SECRET: ${BOX_GOOGLE_CLIENT_SECRET:-}
|
|
BOX_OUTLOOK_CLIENT_ID: ${BOX_OUTLOOK_CLIENT_ID:-}
|
|
BOX_OUTLOOK_CLIENT_SECRET: ${BOX_OUTLOOK_CLIENT_SECRET:-}
|
|
# Sign in with Google / Apple. Separate from the BOX_* mailbox clients
|
|
# above; email+password and passkeys work without any of these.
|
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
|
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
|
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-}
|
|
GOOGLE_IOS_CLIENT_ID: ${GOOGLE_IOS_CLIENT_ID:-}
|
|
APPLE_APP_ID: ${APPLE_APP_ID:-}
|
|
APPLE_TEAM_ID: ${APPLE_TEAM_ID:-}
|
|
APPLE_KEY_ID: ${APPLE_KEY_ID:-}
|
|
APPLE_KEY_SECRET: ${APPLE_KEY_SECRET:-}
|
|
# Passkeys derive from APP_URL when unset. Changing the RP id invalidates
|
|
# every enrolled passkey, so keep it stable per deployment.
|
|
WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-}
|
|
WEBAUTHN_RP_ORIGINS: ${WEBAUTHN_RP_ORIGINS:-}
|
|
# Worker image the orchestrator installs on remote machines.
|
|
WORKER_IMAGE: ${WORKER_IMAGE:-}
|
|
volumes:
|
|
- blobs:/data/blobs
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
redis: { condition: service_healthy }
|
|
nats: { condition: service_healthy }
|
|
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:
|
|
<<: *selfhost-env
|
|
ENCRYPTED_KEYS_PROVIDER: postgres
|
|
volumes:
|
|
- blobs:/data/blobs
|
|
depends_on:
|
|
backend: { condition: service_healthy }
|
|
nats: { condition: service_healthy }
|
|
|
|
# One worker by default. Scale out with `docker compose up --scale worker=3`
|
|
# (or run more on other machines via the SSH enrollment flow). Outbound IPs
|
|
# belong to the mail provider, so more workers = more parallelism, not more IPs.
|
|
worker:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/worker.Dockerfile
|
|
environment:
|
|
<<: *selfhost-env
|
|
# Dovecot in the sandbox profile uses a self-signed cert. Set
|
|
# MAIL_TLS_INSECURE=false once you only connect real mailboxes.
|
|
MAIL_TLS_INSECURE: ${MAIL_TLS_INSECURE:-true}
|
|
ENCRYPTED_KEYS_PROVIDER: http
|
|
ENCRYPTED_KEYS_BACKEND_URL: ${ENCRYPTED_KEYS_BACKEND_URL:-http://backend:8080}
|
|
ENCRYPTED_KEYS_WORKER_TOKEN: ${INTERNAL_API_TOKEN:-local-dev-internal-token}
|
|
# Unset by default so each replica generates its own id and --scale works.
|
|
# Pin it only when running exactly one worker on this host.
|
|
WORKER_ID: ${WORKER_ID:-}
|
|
WORKER_TIER: ${WORKER_TIER:-}
|
|
WORKER_EGRESS_KIND: ${WORKER_EGRESS_KIND:-}
|
|
BOX_GOOGLE_CLIENT_ID: ${BOX_GOOGLE_CLIENT_ID:-}
|
|
BOX_GOOGLE_CLIENT_SECRET: ${BOX_GOOGLE_CLIENT_SECRET:-}
|
|
BOX_OUTLOOK_CLIENT_ID: ${BOX_OUTLOOK_CLIENT_ID:-}
|
|
BOX_OUTLOOK_CLIENT_SECRET: ${BOX_OUTLOOK_CLIENT_SECRET:-}
|
|
volumes:
|
|
- blobs:/data/blobs
|
|
depends_on:
|
|
backend: { condition: service_healthy }
|
|
nats: { condition: service_healthy }
|
|
|
|
tracking:
|
|
build:
|
|
context: ./tracking
|
|
dockerfile: Dockerfile
|
|
ports: ["3000:3000"]
|
|
environment:
|
|
APP_ENV: ${APP_ENV:-dev}
|
|
AWS_CONFIG_ENABLED: ${AWS_CONFIG_ENABLED:-false}
|
|
TRACKING_HOST: "0.0.0.0"
|
|
TRACKING_PORT: "3000"
|
|
EVENTBUS_PROVIDER: ${EVENTBUS_PROVIDER:-nats}
|
|
NATS_URL: ${NATS_URL:-nats://nats:4222}
|
|
KAFKA_BOOTSTRAP_SERVERS: ${KAFKA_BOOTSTRAP_SERVERS:-}
|
|
KAFKA_TRACKING_TOPIC: ${KAFKA_TRACKING_TOPIC:-tracking-events}
|
|
TRACKING_RATE_LIMIT_PER_MIN: ${TRACKING_RATE_LIMIT_PER_MIN:-}
|
|
SENTRY_DSN: ${SENTRY_DSN:-}
|
|
# Overridable so `make dev` (native backend) can point at
|
|
# host.docker.internal:8080 while `docker compose up` uses the container.
|
|
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL:-http://backend:8080}
|
|
INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-local-dev-internal-token}
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
depends_on:
|
|
nats: { condition: service_healthy }
|
|
|
|
realtime:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/realtime.Dockerfile
|
|
ports: ["4000:4000"]
|
|
environment:
|
|
PHX_HOST: ${PHX_HOST:-${PUBLIC_HOST:-localhost}}
|
|
PORT: 4000
|
|
DATABASE_URL: ${PRIMARY_DB:-postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable}
|
|
DATABASE_SSL: ${DATABASE_SSL:-false}
|
|
DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE:-10}
|
|
REDIS_URL: ${REDIS:-redis://redis:6379}
|
|
# Must equal the backend AUTH_SECRET so JWTs validate across services.
|
|
JWT_SECRET: ${AUTH_SECRET:-local-dev-auth-secret-minimum-32-characters-long}
|
|
SECRET_KEY_BASE: ${SECRET_KEY_BASE:-local-development-secret-key-base-minimum-64-characters-for-phoenix}
|
|
PUBSUB_ENABLED: ${PUBSUB_ENABLED:-false}
|
|
# Behind a reverse proxy set CHECK_ORIGIN=true once PHX_HOST matches the
|
|
# public websocket hostname.
|
|
CHECK_ORIGIN: ${CHECK_ORIGIN:-false}
|
|
# SENTRY_DSN deliberately arrives via env_file, not the mapping above: the
|
|
# Elixir Sentry library reads the variable itself and refuses to start on an
|
|
# empty string, so it has to be genuinely absent rather than "".
|
|
env_file:
|
|
- path: .env
|
|
required: false
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
redis: { condition: service_healthy }
|
|
|
|
# Production build of the dashboard (static SPA served by nginx). URLs come
|
|
# from the WARMBLY_* env at container start via /config.js, so the same image
|
|
# the release publishes to GHCR is what runs here. For hot-reload development
|
|
# use `make app` / docker-compose.dev.yml instead.
|
|
web:
|
|
build:
|
|
context: ./web
|
|
dockerfile: Dockerfile
|
|
ports: ["5173:80"]
|
|
environment:
|
|
WARMBLY_API_URL: ${API_PUBLIC_URL:-http://${PUBLIC_HOST:-localhost}:8080}
|
|
WARMBLY_APP_URL: ${APP_URL:-http://${PUBLIC_HOST:-localhost}:5173}
|
|
WARMBLY_TRACKING_DOMAIN: ${TRACKING_DOMAIN:-${PUBLIC_HOST:-localhost}:3000}
|
|
# Cloudflare's always-pass test key keeps the widget satisfied while
|
|
# captcha is off server-side. Set WARMBLY_TURNSTILE_KEY to your site key
|
|
# when you set CAPTCHA_PROVIDER=turnstile.
|
|
WARMBLY_TURNSTILE_KEY: ${WARMBLY_TURNSTILE_KEY:-1x00000000000000000000AA}
|
|
depends_on:
|
|
backend: { condition: service_healthy }
|
|
|
|
admin:
|
|
build:
|
|
context: ./admin
|
|
dockerfile: Dockerfile
|
|
ports: ["5174:80"]
|
|
environment:
|
|
WARMBLY_API_URL: ${API_PUBLIC_URL:-http://${PUBLIC_HOST:-localhost}:8080}
|
|
WARMBLY_DASHBOARD_URL: ${APP_URL:-http://${PUBLIC_HOST:-localhost}:5173}
|
|
WARMBLY_ENV_LABEL: ${ENV_LABEL:-development}
|
|
WARMBLY_TURNSTILE_KEY: ${WARMBLY_TURNSTILE_KEY:-1x00000000000000000000AA}
|
|
depends_on:
|
|
backend: { condition: service_healthy }
|
|
|
|
# ─── one-shots ────────────────────────────────────────────────────────
|
|
|
|
# Optional demo seed. Run explicitly: docker compose --profile seed run --rm seed
|
|
seed:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/backend.Dockerfile
|
|
entrypoint: ["/app/seed"]
|
|
environment:
|
|
<<: *selfhost-env
|
|
SEED_RICH: "true"
|
|
# Without SEED_FULL the paid plans, durations, and subscriptions never
|
|
# load, so the seeded orgs come up with no plan attached.
|
|
SEED_FULL: "true"
|
|
depends_on:
|
|
backend: { condition: service_healthy }
|
|
profiles: ["seed"]
|
|
|
|
volumes:
|
|
postgres_data:
|
|
redis_data:
|
|
nats_data:
|
|
blobs:
|