本地开发
在本地运行完整服务栈,使用 Docker、热重载、示例数据和原生服务。
整个服务栈可通过仓库根目录的一份 docker-compose.yml 在本地运行。需要模拟测试时,可以通过配置组启用较重的组件组合。
前置条件
- Docker 20.10+ 和 Docker Compose v2。
- Git。
- Go 1.25+。
- Node 22+ 和 pnpm,用于 Web 前端。
仅在原生运行以下两个辅助服务时,才需要可选工具:
- Rust,用于跟踪服务。
- Elixir 1.18+,用于实时服务。
一条命令启动
make dev对新克隆的仓库,这一条命令即可完成全部设置。它启动不依赖云服务的 Docker 基础设施,包括 postgres、redis、nats 和 mailpit,等待全部真正就绪,应用迁移,加载示例数据,首次运行时安装 Web 依赖,以容器启动实时和跟踪服务,然后在终端中一起运行后端、消费者、工作节点、控制台和管理面板。
打开 http://localhost:5173,使用 [email protected] / password123 登录。
make dev 与 make up 共用数据库
两者固定使用相同的 Compose 项目 -p warmbly、相同的 warmbly_postgres_data 卷和 warmbly_dev 数据库。make dev 默认填充九个示例账户,其中 [email protected] 拥有全部平台管理权限,并使用公开密码。
这些账户就是实例中的真实账户,因此填充数据会永久消耗首次运行的豁免机会:之后执行 make up 会发现实例已被认领,不再签发设置链接,注册表单也会返回 403 This server is invite only。可以通过 warmblyctl user create --email [email protected] --admin 恢复访问,或用 make reset 从头开始;后者会销毁保存加密密钥的卷。
对于准备继续用于自托管的数据库,请使用 make dev SEED=false。完整说明见首次运行。
原生开发服务栈保持邮件登录验证码开启,即 AUTH_LOGIN_CODE=always,以持续覆盖该流程;本地邮件进入 Mailpit,不进入真实收件箱。打开 http://localhost:18025,查看最新的“Your Login Code(你的登录验证码)”邮件,再将验证码粘贴到控制台。
自托管安装不同:默认关闭验证码,因此登录不依赖邮件。参见账户与访问权限。
Ctrl-C 停止应用服务,但 Docker 基础设施继续运行,因此下次 make dev 会直接启动应用。Go 服务通过 go run 原生运行;修改代码后按 Ctrl-C 并重新运行,一两秒即可重新编译。其他用法:
make dev SEED=false # skip fixture seeding
make dev AI_PROVIDER=ollama # with the AI assistant on (see the AI provider section)跟踪服务 :3000 和实时服务 :4000 在 make dev 下以容器运行,因此无需安装 Rust 或 Elixir 工具链,也能使用像素、点击重定向和控制台实时更新。原生的 make tracking / make realtime 目标用于直接迭代这两个服务自身。
Make 目标
初次使用:
make dev # one command: infra + migrations + seed + backend/forms/consumer/worker + realtime + tracking + dashboard + admin
make infra # postgres, redis, nats, mailpit (leave running)
make run # backend + forms + consumer + worker (native, after make infra)
make up # the whole no-cloud stack in Docker (docker compose up --build)
make gen-key # print a KMS_LOCAL_MASTER_KEY for a real deployment
make seed # load rich fixtures (3 orgs, 6 mailboxes, a campaign)
make sandbox # fully working demo environment (see /development/sandbox/)
make reset # nuke everything including volumes, start over这种拆分让多个 Git 工作树可以共享有状态组件。make infra 只需启动一次;每个工作树使用相同项目名 -p warmbly,因此第二个工作树的 make app 会基于其源码原地重建语言运行容器,不影响基础设施。
代码迭代:
# Hot reload is on by default under `make app`:
# - Go saves -> air rebuilds the binary in-container (~2-5s)
# - Rust saves -> cargo-watch rebuilds (~2-10s debug build)
# - Elixir saves -> Phoenix reloads modules in-process
# - Web saves -> Vite HMR (browser updates instantly)
#
# So normally you don't restart anything manually.
# Tail logs:
make app-logs # all hot-reload services
make logs # everything including infra
make logs backend # one service
make logs backend consumer # multiple如果使用生产镜像流程 make up,而不是 make app,二进制已打包进镜像,需要通过 make restart <svc>、make restart-go 或 make restart-all 应用代码变化。
所有目标最终都调用 docker compose -p warmbly。未安装 Make 时,等效命令如下:
docker compose -p warmbly up -d postgres redis nats mailpit # infra
docker compose -p warmbly -f docker-compose.yml -f docker-compose.dev.yml up -d --build \
backend consumer worker tracking realtime web admin # app
docker compose -p warmbly up -d dovecot # sandbox IMAP host
docker compose -p warmbly --profile seed run --rm --build seed # seed
docker compose -p warmbly --profile seed --profile sandbox down -v # reset正在运行的组件
不依赖云服务的服务栈没有 AWS、GCP、Stripe 或 Kafka。执行 make infra && make app 后会运行:
- postgres、redis:关系存储,以及缓存/实时桥接。
- nats:JetStream 事件总线,用一个小型二进制替代 Kafka、Zookeeper 和 Schema Registry。
- mailpit:带 Web 界面的 SMTP 邮件捕获器。
- dovecot:承载沙盒邮箱收件箱的真实 IMAP 服务器,属于
sandbox配置组,由make sandbox启动。 - backend、consumer、tracking、realtime、web、admin:应用服务。
- worker:一个发送/同步工作节点;可用
docker compose up --scale worker=N扩容,或通过 SSH 登记流程添加机器。
加密、Blob 存储和延迟任务均使用本地提供程序,即 KMS_PROVIDER=local、BLOB_PROVIDER=filesystem 和 TASKS_PROVIDER=local,不需要外部服务。各提供程序开关见自托管指南。
服务 URL
大多使用标准端口。部分默认端口容易冲突,因此做了偏移:Postgres 为 15432,Redis 为 16379,NATS 监控为 8222,Mailpit 界面为 18025、SMTP 为 11025。其余使用通常端口。如果仍有冲突,可在本地 docker-compose.override.yml 中覆盖。
| 服务 | URL |
|---|---|
| 后端 API | http://localhost:8080 |
| 跟踪 | http://localhost:3000 |
| 实时服务 | http://localhost:4000 |
| Web(Vite 开发) | http://localhost:5173 |
| 管理端(Vite 开发) | http://localhost:5174 |
| Mailpit | http://localhost:18025 |
| Dovecot IMAP / IMAPS | localhost:10143 / localhost:10993 |
| NATS | localhost:4222,监控为 http://localhost:8222 |
| Postgres | localhost:15432 |
| Redis | localhost:16379 |
数据库设置
后端在启动时自动运行迁移,逻辑见 internal/infrastructure/db/migrate.go,因此无需独立迁移步骤。迁移文件位于 internal/infrastructure/db/migrations/。
添加数据库迁移
每个迁移是一对文件,分别命名为 <version>_<name>.up.sql 和 <version>_<name>.down.sql;版本为六位数字,名称采用 snake_case。版本连续,因此新迁移使用 main 上最高版本之后的下一个编号。
应根据 main 选择编号,不要只看自己的分支。否则,同时创建的两个分支可能占用同一个下一版本;它们各自都能干净迁移,但合到 main 后,编号冲突会让 golang-migrate 拒绝构建 source driver。后端随后在启动时退出并陷入重启循环,所有等待它的服务也会受影响。
推送前运行:
make check-migrations它会报告重复版本、编号空缺、缺少对应文件的 .up.sql,即没有相应 .down.sql,以及 golang-migrate 无法解析的文件名。make lint 也会运行它;CI 中的 Migrations 作业会在每个修改此目录的拉取请求上执行。
如果重复编号确实进入 main,应重新编号尚未发布的那个迁移。线上部署已经在 schema_migrations 中记录了另一个,给已发布迁移重新编号会使它们再次尝试应用。
载入测试数据
make seed 单次运行数据填充器。它是幂等的,Schema 变化后可安全重跑。
始终加载的基础数据:
| 字段 | 值 |
|---|---|
| 邮箱 | [email protected] |
| 密码 | password123 |
开发用户的组织始终以一个运行中的工作区呈现,而不是空壳:
- 共享工作节点上有 4 个已预热邮箱,均加入付费预热池。
- 文件夹、标签和分类标记具有真实关联,包括邮箱标签、营销活动文件夹、联系人和收件箱会话标签。
- 约 30 个带职务和公司的联系人,部分已退订或被禁止发送。
- 一个活跃的三步骤营销活动,24 个潜在客户分布在已发送、已打开、已回复、已退信和排队等阶段,另有一个草稿活动。
- 14 天统计历史,包括营销活动发送、预热量和逐邮箱计数,始终包含今天的发送。
- 统一收件箱中有回复链、异议、休假自动回复、退信和预热风格的往来。
- CRM 销售管道中有交易、任务和备注,另有回复模板、通知,以及带交易历史的 AI 积分余额。
所有日期都相对当前时间计算,因此无论何时填充,工作区看起来都在活跃运行。
启用 SEED_RICH=true 时,它在 docker-compose.yml 和 make seed 中均已设置,还会加载:
- 3 个组织:Acme 免费版、Beta 专业版和 Gamma 企业版,各有自己的所有者用户,密码为
password123。 - 3 个与
docker-compose.yml主机名匹配的工作节点,分别为共享、付费和专用。 - 6 个分布在各节点上的邮箱账户,加入正确的预热池。
- 一个包含两步骤邮件序列的 Beta 营销活动。
- 10 个联系人,其中 2 个已退订,用于覆盖禁止发送行为。
加密与存储
本地服务栈不需要 AWS。信封加密使用本地 AES 主密钥,即 KMS_PROVIDER=local 和 KMS_LOCAL_MASTER_KEY,Makefile 中提供固定开发值;Blob,例如头像和标志,保存到目录中,由 BLOB_PROVIDER=filesystem 和 BLOB_FS_ROOT 控制。切换到 KMS_PROVIDER=aws / BLOB_PROVIDER=s3,即可改用 AWS KMS 和任意兼容 S3 的存储。所有提供程序开关见自托管指南。
连接 psql / Redis CLI
docker compose exec postgres psql -U warmbly -d warmbly_dev
docker compose exec redis redis-cli外部客户端可以使用:
- Postgres:
localhost:15432,用户warmbly,密码warmbly,数据库warmbly_dev。 - Redis:
localhost:16379。
以原生进程运行服务
make dev 已经让 Go 服务和控制台对接 Docker 基础设施原生运行。如果希望分别放在不同终端,每个服务都有独立目标,已预先配置完整开发环境,无需手动导出环境变量:
make infra # once: the backing services in Docker
make backend # API on :8080 (applies migrations on boot)
make forms # public forms service on :8090 (builds the forms/ app, then serves it)
make forms-web # Vite dev server for forms/ on :5175 when editing the app itself
make consumer # event consumer (NATS -> postgres)
make worker # send/sync worker (run more for parallelism)
make run # backend + forms + consumer + worker in one terminal
make web # dashboard dev server on :5173
make admin # admin app on :5174
make site # marketing site on :4321每个目标都会设置原本需要手动导出的相同环境。各变量用途见配置参考,裸二进制模板见 deploy/config/env.example。保存 Go 文件并重新运行目标后,利用已有构建缓存,一两秒即可重新编译。
跟踪服务(Rust)与实时服务(Elixir)
make dev 以容器运行这两个服务,make run 不包含它们。原生目标需要各自的工具链,即 cargo、elixir 和 mix,用于直接开发这些服务:
make tracking # tracking pixel + click redirect service on :3000
make realtime # Phoenix websocket fanout on :4000AI 服务提供方
本地开发默认关闭 AI 功能,包括控制台助手、回复草稿、联系人研究和自动化 AI 节点,因此选择服务商之前,助手会明确返回 503。向开发目标传入 AI_PROVIDER,预设会补齐基础 URL,你只需提供密钥和可选模型。
# free local model, no key: install Ollama, pull a tool-capable model, run
ollama pull llama3.1 # or qwen2.5
make backend AI_PROVIDER=ollama
make run AI_PROVIDER=ollama AI_MODEL=qwen2.5
# OpenRouter: one key fronts every vendor; switch models via AI_MODEL
make backend AI_PROVIDER=openrouter AI_KEY=sk-or-... AI_MODEL=deepseek/deepseek-chat
# Groq free tier (fast) or hosted OpenAI
make backend AI_PROVIDER=groq AI_KEY=gsk_... AI_MODEL=openai/gpt-oss-20b
make backend AI_PROVIDER=openai AI_KEY=sk-...AI_PROVIDER=ollama 无需密钥,并将模型标为免费,即 AI_FREE,因此不收取 AI 积分,助手显示“免费模型”提示。本地和免费等级模型能力较弱,但工具调用和完整智能体循环仍可工作;应使用 llama3.1(8B)或 qwen2.5,不要用 1B 至 3B 模型。保持 AI_PROVIDER 未设置即可继续关闭 AI。完整服务商列表见自托管指南。
Mailpit
所有出站邮件都会被 Mailpit 捕获。开发目标设置 MAIL_TRANSPORT=smtp 和 SMTP_SECURITY=none,因为邮件接收器不提供 STARTTLS,而平台传输会拒绝通过未加密连接发送凭据。Mailpit 接受任意凭据的 SMTP AUTH,因此沙盒邮箱的工作节点发送也会被捕获。
Mailpit 属于 sandbox Compose 配置组,因此自托管 make up 不会启动它。make dev、make infra 和 make sandbox 显式指定它,因此不受配置组是否启用影响。
- Web 界面:
http://localhost:18025。 - Docker 内 SMTP:
mailpit:1025。 - 主机侧 SMTP:
localhost:11025。
注意,Mailpit 只支持 SMTP,不支持 IMAP。工作节点的 IMAP 同步路径通过 Dovecot 服务测试;完整实时邮件环境见沙盒。
邮件模板
邮件模板位于 internal/notify/templates/。渲染测试:
go test ./internal/notify/templates/ -v要在浏览器中预览模板,可将它们输出到磁盘:
go test ./internal/notify/templates/ -run TestPreview -v
# Files land in the test temp dir, path printed in output也可以直接在运行中的应用触发身份验证流程,然后在 Mailpit 查看收到的邮件。
常见任务
重新构建单个服务
make restart backend仅重置 Postgres
docker compose stop postgres
docker volume rm warmbly_postgres_data
docker compose up -d postgres检查事件总线(NATS)
NATS 启用 JetStream,并提供 HTTP 监控端点:
curl http://localhost:8222/jsz | jq # JetStream streams + message counts
curl http://localhost:8222/varz | jq # server info and connections要实时查看主题,可使用 nats-io 的 NATS CLI nats,连接 nats://localhost:4222,例如执行 nats sub 'warmbly.>'。使用可选 Kafka 覆盖配置时,事件总线改为 Kafka;参见自托管指南。
故障排查
端口已被占用。 使用 lsof -i :5432 或对应端口,查找占用进程。Compose 端口有意做了偏移,因此本地 Postgres 使用 5432 时通常不会冲突。
后端无法访问事件总线。 NATS 一两秒即可启动,健康检查会控制依赖服务的启动,因此 docker compose up 应能处理顺序。如果手动启动时顺序不对,可运行 docker compose restart backend consumer。
计划发送始终不触发。 延迟发送通过进程内 Postgres 任务轮询器运行,即 TASKS_PROVIDER=local,因此后端必须运行。检查后端日志;任务协调器会在几分钟内取消并重新入队遗漏任务。
测试 Warmbly Cloud 连接
make poollink-dev 会并排启动两个实例,让你端到端体验自托管用户流程:一个接近生产的 Cloud,使用 DEPLOYMENT_MODE=cloud,启用带占位密钥的 Stripe 计费限制,通过 Mailpit 验证邮箱,并将 Sunrise Labs 沙盒组织填充到预热池,运行在 :18301 / :18401;另一个是没有任何账户的全新自托管实例,运行在 :18302 / :18402,其 WARMBLY_CLOUD_URL 指向前者。两者使用 make infra 容器,但各有自己的数据库,因此旁边的 make dev 仍可工作。
脚本会打印自托管实例的认领链接,scripts/dev-poollink.sh setup-link 可重新打印新的链接。在该实例连接邮箱:SMTP 使用 localhost:11025,即无身份验证的 Mailpit;IMAP 使用 localhost:10993,Dovecot 接受任意用户和密码 sandbox。然后进入“设置 > Warmbly Cloud”:认领账户就是实例管理员,链接要求使用双因素身份验证登录,因此应先在“设置 > 安全”启用 2FA 并重新登录。在 Cloud 控制台注册,在 /connect 批准代码,再登记邮箱。Cloud 工作区从免费试用开始,因此 10 邮箱额度和升级卡片与生产行为一致;真正结账需要真实 Stripe 密钥,设置后会从环境读取 STRIPE_SECRET_KEY 等值。make poollink-dev-down 停止全部服务,make poollink-dev-reset 还会删除数据库。产品行为见 Warmbly Cloud 指南。