mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-21 16:01:32 +00:00
a7518a8558
* feat: rewrite the self-hosting docs against repo ground truth: turn the deployment guide into a full self-host guide (quick start with first-admin bootstrap via make grant-admin, .env secrets with exact key formats, PUBLIC_HOST derivation and HTTPS reverse-proxy vars, provider switches with build-tag caveats, mailbox OAuth, remote worker enrollment via SSH or wmenroll tokens, real CI image tags, upgrades and backups), rewrite the events page around the real NATS/Kafka bus topics and {type,body} envelopes, fix Kafka-era and make-target claims in architecture/local-development/deploy README, add API_PUBLIC_URL and drop the dead LOG_DISCORD_WEBHOOK_URL in env.example, and remove the docker-compose.kafka.yml comment pointing at a file that does not exist
* feat: make the self-hosting docs visual and skimmable by adding a Mermaid MDX component (client-rendered, theme-aware) to the docs site, condensing the self-host guide around a control-plane topology diagram, a worker enrollment sequence diagram, a dashboard screenshot, and symptom/check troubleshooting + optional-subsystem tables, and adding an execution-plane flowchart to the architecture page
* feat: stop the docs root flashing a 'Continue to the Warmbly docs' link before redirecting by navigating with an inline location.replace that runs during HTML parse, and demoting the visible link and meta refresh to no-JS fallbacks inside noscript
* feat: cut docs bulk and duplication by deleting three orphaned API pages that were stale forks of the reference section and were unreachable from the sidebar (porting their unique social sign-in, promo-code, and referral endpoints into api/reference/account-org.mdx as compact tables), condensing the deliverability and warmup guides to roughly half their length around tables instead of prose, replacing prose em dashes across the guides and MCP pages, and adding the required trailing slashes to internal links in 24 files
* feat: condense the sequences guide by about 40 percent, folding the switch-step deciders and branch conditions into tables and cutting restated prose while keeping every rule about threading, instant branches, reply matching, and stop on reply
* feat: condense the automations, unibox, advisor, and expressions guides by roughly 40 percent each, folding trigger lists, action catalogs, sending controls, and advisor checks into tables, adding a trigger-condition-action flow diagram to automations, and cutting restated prose while preserving every threshold, permission boundary, and rule
* feat: condense the mailboxes, campaigns, analytics, and team-roles guides by roughly 45 percent each, replacing prose walks through providers, rotation modes, lead statuses, counting rules, A/B confidence, and the permission matrix with compact tables and collapsing the four-way role grid into one capability table plus a one-line mapping
* feat: condense the AI-steps, security, and contacts-CRM guides by roughly 40 percent, turning sign-in methods, AI step modes, switch deciders, credit and failure behavior, import field mappings, and deal views into tables while keeping every safety boundary and dedupe rule
* feat: condense the meetings, notifications, AI-credits, and AI-assistant guides by roughly 40 percent, merging notification categories and their defaults into one table, collapsing credit costs, spend controls, and plan allowances into tables, and tightening the assistant page around its approval and permission boundaries
* feat: condense the integrations, collaboration, zapier, and make guides by roughly 35 percent, grouping the thirty-row Zapier and Make action lists into eight labelled areas, folding CRM default field mappings and presence indicators into tables, and promoting the destructive-action and unattended-delete warnings into callouts
* fix: correct three factual errors in the development docs: NOTIFICATION_EMAIL_DAILY_CAP=0 means uncapped rather than disabled (overEmailBudget returns false at limit<=0, so documenting it as a kill switch inverted the behavior), and the worker-SSH and warmup-pool migration citations in architecture.mdx pointed at pre-squash filenames that no longer exist or now belong to unrelated migrations, so both now cite the tables in 000001_baseline.up.sql
* feat: add the missing docs SEO primitives: a build-time sitemap.xml covering all 64 pages, a robots.txt that points at it and keeps the llms.mdx and og mirrors out of the index as duplicate content, and per-page canonical plus richer OpenGraph URL/title/description metadata
* fix: use the single real team@warmbly.com address everywhere a human is told to write in, replacing the invented hello/sales/legal/support inboxes across the marketing site, the transactional email footer, and the admin outreach composer default Reply-To (which pointed replies at a mailbox that does not exist), and collapse the contact page's two-inbox framing into one inbox with one published response time
303 lines
12 KiB
YAML
303 lines
12 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.
|
|
#
|
|
# Secrets have dev-friendly defaults so `docker compose up` just works. For a
|
|
# real deployment, put your own values in a .env file next to this file:
|
|
# AUTH_SECRET, KMS_LOCAL_MASTER_KEY (base64 32 bytes; `make gen-key`),
|
|
# CREDENTIALS_ENCRYPTION_KEY (64 hex), INTERNAL_API_TOKEN, and — to connect
|
|
# Gmail mailboxes — BOX_GOOGLE_CLIENT_ID / BOX_GOOGLE_CLIENT_SECRET.
|
|
#
|
|
# 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).
|
|
|
|
x-selfhost-env: &selfhost-env
|
|
APP_ENV: ${APP_ENV:-dev}
|
|
AWS_CONFIG_ENABLED: "false"
|
|
EVENTBUS_PROVIDER: nats
|
|
NATS_URL: nats://nats:4222
|
|
CODEC_PROVIDER: json
|
|
KMS_PROVIDER: local
|
|
KMS_LOCAL_MASTER_KEY: ${KMS_LOCAL_MASTER_KEY:-Xr0JA7gqF2POy29a7MRByyqddivTNt8WOyKsOXklazk=}
|
|
CREDENTIALS_ENCRYPTION_KEY: ${CREDENTIALS_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef}
|
|
BLOB_PROVIDER: filesystem
|
|
BLOB_FS_ROOT: /data/blobs
|
|
BLOB_PUBLIC_BASE_URL: ${API_PUBLIC_URL:-http://${PUBLIC_HOST:-localhost}:8080}/public
|
|
TASKS_PROVIDER: local
|
|
BILLING_PROVIDER: none
|
|
CAPTCHA_PROVIDER: none
|
|
PUBSUB_ENABLED: "false"
|
|
PRIMARY_DB: postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable
|
|
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}
|
|
|
|
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
|
|
|
|
# Local SMTP sink + web UI so outbound mail is visible without a real relay.
|
|
# Point mailbox SMTP at mailpit:1025 (or use it as the notification relay).
|
|
mailpit:
|
|
image: axllent/mailpit:latest
|
|
ports:
|
|
- "18025:8025"
|
|
- "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}
|
|
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
|
|
EMAIL_NAME: ${EMAIL_NAME:-Warmbly}
|
|
EMAIL_ADDRESS: ${EMAIL_ADDRESS:-noreply@warmbly.local}
|
|
SMTP_HOST: ${SMTP_HOST:-mailpit}
|
|
SMTP_PORT: ${SMTP_PORT:-1025}
|
|
TRACKING_DOMAIN: ${TRACKING_DOMAIN:-${PUBLIC_HOST:-localhost}:3000}
|
|
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:-}
|
|
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
|
|
MAIL_TLS_INSECURE: "true"
|
|
ENCRYPTED_KEYS_PROVIDER: http
|
|
ENCRYPTED_KEYS_BACKEND_URL: http://backend:8080
|
|
ENCRYPTED_KEYS_WORKER_TOKEN: ${INTERNAL_API_TOKEN:-local-dev-internal-token}
|
|
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: "false"
|
|
TRACKING_HOST: "0.0.0.0"
|
|
TRACKING_PORT: "3000"
|
|
EVENTBUS_PROVIDER: nats
|
|
NATS_URL: nats://nats:4222
|
|
KAFKA_TRACKING_TOPIC: tracking-events
|
|
# 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: postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable
|
|
DATABASE_SSL: "false"
|
|
DATABASE_POOL_SIZE: "10"
|
|
REDIS_URL: 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: "false"
|
|
CHECK_ORIGIN: "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}
|
|
# Captcha is off server-side (CAPTCHA_PROVIDER=none); this Cloudflare test
|
|
# key keeps the widget satisfied without a real 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: "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"
|
|
depends_on:
|
|
backend: { condition: service_healthy }
|
|
profiles: ["seed"]
|
|
|
|
volumes:
|
|
postgres_data:
|
|
redis_data:
|
|
nats_data:
|
|
blobs:
|