自托管

从一条 Docker 命令到分布式工作节点集群,分步介绍如何自行运行整个 Warmbly 平台。

自托管 Warmbly 完全不需要任何云账户:不需要 AWS、GCP、Stripe 或 Kafka。一个 docker-compose.yml 即可使用本地开源组件运行全部服务;每项外部依赖都由环境变量控制,之后随时可以切换。

使用 Coolify?请按照在 Coolify 上部署操作,了解仅使用镜像的 Compose 服务栈、准确的域名与端口映射,以及无须在主机上运行安装程序的配置方式。

只希望尽快运行起来?

curl -fsSL https://warmbly.com/install.sh | sh

该命令会拉取已发布的版本镜像,跳过本页其余步骤:无须克隆仓库或编译,耗时不到两分钟。添加 --wizard 后,程序会询问数据存放位置、保留内容和期限,以及备份方式。参见安装。

本页介绍从源码构建的方式,也列出两种安装方式所写入的全部设置。如果希望运行修改后的代码、使用尚无官方镜像的平台,或运行自行构建的版本,请采用此方式。

三个命令即可得到可用安装。之后的内容均为可选项,先确认系统运行,再进行后续配置。

开始前的准备

要求原因
Docker 20.10+ 和 Compose v2全部服务以容器运行
Git用于克隆仓库
约 10 GB 可用磁盘空间首次构建会产生约 3.7 GB 镜像及 2.5 GB 构建缓存
4 GB 内存服务栈空闲时约占 300 MB;首次构建的资源需求较高
空闲端口5173, 5174, 8080, 4000, 3000, 4222, 8222, 15432, 16379

开始前请检查 3000:许多自托管软件默认使用此端口,冲突会导致跟踪服务无法发布端口。可设置 TRACKING_PORT 更改主机侧端口,并将反向代理指向新端口。

首次构建需要编译 Go、Rust 和 Elixir 服务,并构建两个前端。在现代笔记本电脑上约需 6 分钟。后续构建会复用缓存,速度快得多。

要求仅此而已。首次运行前不需要 SMTP 中继、验证码密钥或云账户,也无须编写 .env。

有两项配置来自 Warmbly 之外,但都可以等系统运行后再设置:

  • SMTP 中继用于密码重置、邀请和摘要通知。未配置时,这些邮件会写入后端日志;登录本身不依赖它们。参见平台邮件。
  • 如需连接 Gmail、Google Workspace 或 Microsoft 365 邮箱,需要 OAuth 客户端。普通 SMTP 和 IMAP 邮箱无须此配置。参见连接邮箱。

全部配置集中在仓库根目录的一份 .env 中,环境配置一节提供说明和完整示例。

不要在磁盘接近满载时运行构建

同时构建七个镜像是首次安装失败最常见的原因。如果 Docker 在构建过程中耗尽空间,正在编译的任意服务都可能报出令人困惑的 no space left on device。参见故障排查。

安装平台

获取源码

git clone https://github.com/warmbly/warmbly && cd warmbly

启动服务

make up

实际执行的是 docker compose -p warmbly up -d --build。它会构建镜像,在后台启动十个容器,然后等待后端就绪,并输出用于认领实例的链接。首次运行会较慢。

只启动正式运行所需的组件。邮件捕获器和本地 IMAP 服务器属于演示环境,只有通过 make sandbox 明确请求时才会启动。

认领实例

make up 完成后会输出一次性链接。打开链接、设置密码,你就成为该实例的所有者和平台管理员。make claim 可再次输出该链接。

此时可直接访问 http://localhost:5173 上的控制台,以及 http://localhost:5174 上的管理面板;两者使用同一账户,因为所有者已是管理员。登录始终不依赖邮件:自托管安装默认关闭邮件登录验证码。

没有链接,或链接已丢失?

如果实例数据库中已有账户,就视为已认领,不会再生成链接。此前使用 make dev 向同一数据库填充过数据时,通常会出现这种情况。首次运行介绍了重新签发链接、无人值守创建所有者,以及已有账户时的处理方式。

加载演示数据(可选)

如需体验已有数据的工作区,而非空白工作区:

make seed-demo

这会创建账户 [email protected]/password123,以及邮箱、营销活动、联系人和两周历史数据。每次都会有意重新构建种子数据镜像,否则 compose run 会复用旧镜像,导致无法适配新版数据库结构。

演示数据会写入已公开的超级管理员凭据

它还会插入 [email protected],该账户使用公开密码,拥有全部平台管理员权限,并配有密钥固定写在此仓库中的全权限 API 密钥。绝不要在其他人能够访问的实例上运行此命令。详见首次运行。

如需邮件能够端到端流转、支持发送、回复、打开和点击的演示环境,请参见沙盒。

首次登录后的控制台

对外开放前的准备

以上步骤无须配置即可运行,因为 Compose 文件为所有密钥提供了可用默认值。这些默认值已公开在仓库中,因此不提供任何保护。当 APP_ENV 不是 dev 时,后端会拒绝使用这些值启动,并逐一指出问题,同时说明 ALLOW_INSECURE_DEFAULTS=true 是不应使用的绕过选项。

在允许其他人访问实例之前,请在 docker-compose.yml 旁的 .env 中生成真正的随机值:

cat > .env <<EOF
APP_ENV=prod
AUTH_SECRET=$(openssl rand -base64 32)
INTERNAL_API_TOKEN=$(openssl rand -hex 24)
SECRET_KEY_BASE=$(openssl rand -base64 64 | tr -d '\n')
KMS_LOCAL_MASTER_KEY=$(openssl rand -base64 32)
CREDENTIALS_ENCRYPTION_KEY=$(openssl rand -hex 32)
EOF
变量格式保护对象
AUTH_SECRET至少 32 个字符JWT 签名。实时服务通过 JWT_SECRET 读取同一值,Compose 会自动完成映射
INTERNAL_API_TOKEN任意随机字符串后端内部 API,工作节点和跟踪服务使用它进行身份验证
SECRET_KEY_BASE至少 64 个字符实时服务中的 Phoenix 会话签名
KMS_LOCAL_MASTER_KEYbase64 编码,恰好 32 字节用于加密各组织数据密钥的根密钥
CREDENTIALS_ENCRYPTION_KEY恰好 64 个十六进制字符静态存储的邮箱凭据:SMTP、IMAP 密码,以及 Gmail、Outlook 的 OAuth 访问令牌和刷新令牌

APP_ENV=prod 不需要云账户。配置后才会使用错误上报和 GeoIP 查询;未配置时会记录说明并跳过。

保存任何邮箱之前,先备份最后两项密钥

KMS_LOCAL_MASTER_KEY 和 CREDENTIALS_ENCRYPTION_KEY 用于加密所有存储的凭据。丢失后无法恢复,只有数据库备份而没有它们也无法解密。

轮换密钥后需要重新创建容器,make up 会执行此操作。但已有邮箱后,不能直接更换两把加密密钥:原有凭据是使用旧值加密的。

检查服务栈

make status

backend、forms、postgres、redis 和 nats 应显示 (healthy)。后端在启动时应用数据库迁移,因此比其他服务多需几秒。realtime 和 tracking 没有健康检查,显示为 running。

每个入口使用各自的端口响应:

curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/health   # backend
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/health   # tracking
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8090/health   # forms
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:4000/health   # realtime
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:5173          # dashboard

如果某项未返回 200,可用 make logs backend(或对应服务的日志命令)查看原因;常见原因见故障排查。

管理面板的 实例 > 设置与健康 > 服务 选项卡显示相同的探测结果,以及平台邮件传输状态,并提供实时连接检查和测试发送按钮。

正在运行的组件

服务端口用途
web / admin5173 / 5174控制台和管理面板(由 nginx 提供的静态构建,URL 在容器启动时注入)
backend8080REST API;每次启动时应用迁移;GET /health
tracking3000打开跟踪像素和点击重定向;GET /health
forms8090托管表单页面、嵌入和公开提交;GET /health
realtime4000WebSocket 消息分发;GET /health
consumer / worker无事件处理器;发送和同步执行器
postgres / redis15432 / 16379PostgreSQL 16;缓存和实时消息桥接
nats4222(监控端口 8222)事件总线(JetStream)
mailpit18025(SMTP 11025),绑定 localhost不会由 make up 启动。它是演示环境中的 SMTP 邮件接收器,位于 sandbox 配置组。其界面没有身份验证,因此绝不会向网络公开

共十一个容器,空闲时合计约占 300 MB。不会启动仅演示环境需要的组件:邮件捕获器和本地 IMAP 服务器位于 sandbox 配置组,只有 make sandbox、make dev 或 make infra 指定时才启动。

无须单独执行迁移。迁移已嵌入后端二进制文件,镜像中还提供独立的 /app/migrate 二进制文件。

环境配置

全部配置均使用环境变量。使用 Compose 时,将值写入 docker-compose.yml 旁的 .env,即可自动传递给对应服务;Compose 将共享配置块分发给后端、消费者和工作节点,并映射实时服务与跟踪服务使用的少量不同变量名。

以下内容都不是启动前的必需配置。Compose 默认值即可启动可用服务栈;本节各项用于进一步满足实际部署要求。

各服务必须保持一致的配置值

各服务分别读取自己的配置副本,因此以下值不一致时,系统可能悄然出错。Compose 会自动保持一致;手动部署时必须自行确保同步。

值读取方不一致的后果
AUTH_SECRET,实时服务以 JWT_SECRET 读取backend、realtime控制台可加载,但实时功能始终不可用:WebSocket 拒绝所有令牌
INTERNAL_API_TOKEN,工作节点通过 ENCRYPTED_KEYS_WORKER_TOKEN 读取,除非设置了 NODE_BROKER_TOKENbackend、worker、tracking、forms工作节点无法获取解密密钥,跟踪服务无法解析点击票据;两者均以 401 拒绝访问
KMS_LOCAL_MASTER_KEYbackend、consumer、worker无法解密已封装的数据密钥,邮箱凭据也无法再解密
CREDENTIALS_ENCRYPTION_KEYbackend、worker无法解密已存 SMTP、IMAP 密码及 OAuth 令牌,导致所有邮箱都无法发送或同步
EVENTBUS_PROVIDER 和 NATS_URL所有服务生产者和消费者连接到不同总线,任务会悄然丢失
CODEC_PROVIDER所有服务事件写入和读取使用不同格式
PUBSUB_ENABLEDbackend、consumer、realtime实时事件被发布到无人监听的传输渠道

各服务所需配置

服务所需配置
backend全部配置:五项密钥、PRIMARY_DB、REDIS、服务提供方切换项,以及公开 URL
consumer与后端相同的共享配置块。它需要写入 Postgres,因此需要 PRIMARY_DB 和加密密钥
worker不需要数据库。需要事件总线、REDIS、加密密钥、ENCRYPTED_KEYS_BACKEND_URL 及内部令牌,以及 BOX_* OAuth 客户端
tracking事件总线、BACKEND_INTERNAL_URL 和 INTERNAL_API_TOKEN
forms不需要数据库或事件总线:仅需 BACKEND_INTERNAL_URL 和 INTERNAL_API_TOKEN
realtimeJWT_SECRET, SECRET_KEY_BASE, DATABASE_URL, REDIS_URL, PHX_HOST
web / admin仅需 WARMBLY_* URL,容器启动时读取并写入 /config.js。同一镜像可在任意环境运行

APP_ENV 接受 dev 或 prod,其他值均不会启用生产环境行为。prod 不需要云账户:任何环境中的错误跟踪均为可选。所有环境中的后端都必须设置 GEODB_PATH,但其指向的文件可以不存在(消费者也可选读取该文件,为打开和点击事件补充地理位置)。

包含默认值及是否需要重启的完整变量列表,请参见配置参考。

完整的 .env 文件

实际部署通常需要的所有设置集中在一个文件中。删除任意一行即可回退到默认值。

# ── Where it lives ────────────────────────────────────────────────
PUBLIC_HOST=warmbly.example.com          # or set the four URLs below explicitly
# APP_URL=https://app.example.com
# API_PUBLIC_URL=https://api.example.com
# CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
# WEBSOCKET_URL=wss://ws.example.com/socket/websocket
# PHX_HOST=ws.example.com
# CHECK_ORIGIN=true
# TRACKING_DOMAIN=t.example.com

# ── Secrets (generate your own) ───────────────────────────────────
AUTH_SECRET=replace-me-32-chars-minimum
INTERNAL_API_TOKEN=replace-me
SECRET_KEY_BASE=replace-me-64-chars-minimum
KMS_LOCAL_MASTER_KEY=replace-me-base64-32-bytes
CREDENTIALS_ENCRYPTION_KEY=replace-me-exactly-64-hex-chars

# ── Connecting mailboxes ──────────────────────────────────────────
BOX_GOOGLE_CLIENT_ID=
BOX_GOOGLE_CLIENT_SECRET=
BOX_OUTLOOK_CLIENT_ID=
BOX_OUTLOOK_CLIENT_SECRET=

# ── Platform email (resets, invitations, digests) ─────────────────
# log writes messages to the backend logs and delivers nothing. Switch to
# smtp before anyone else relies on this instance.
MAIL_TRANSPORT=smtp
SMTP_HOST=smtp.example.com
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_SECURITY=starttls               # starttls (587) | tls (465) | none (25)
EMAIL_NAME=Warmbly
EMAIL_ADDRESS=[email protected]

# ── Auth ──────────────────────────────────────────────────────────
DEPLOYMENT_MODE=self_hosted
# AUTH_LOGIN_CODE=off                # always | new_device | off
# DISABLE_REGISTRATION=invite_only   # true | false | invite_only
# TRUSTED_PROXIES=10.0.0.0/8         # required when a reverse proxy is in front

# First owner, read only while the users table is empty. Omit and the backend
# prints a single-use setup link to its logs.
# [email protected]
# WARMBLY_BOOTSTRAP_PASSWORD_HASH=

# ── Optional: single sign-on ──────────────────────────────────────
# OIDC_ISSUER_URL=https://id.example.com/application/o/warmbly/
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
# OIDC_DEFAULT_ORG=<organization uuid>

# ── Optional: AI assistant ────────────────────────────────────────
# AI_PROVIDER=openai
# AI_API_KEY=sk-...
# AI_MODEL=

# ── Optional: sign in with Google / Apple ─────────────────────────
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# GOOGLE_REDIRECT_URI=

# ── Optional: captcha, billing, error tracking ────────────────────
# CAPTCHA_PROVIDER=turnstile
# TURNSTILE_SECRET=
# WARMBLY_TURNSTILE_KEY=            # the site key, used by web + admin
# BILLING_PROVIDER=stripe
# STRIPE_SECRET_KEY=
# STRIPE_WEBHOOK_SECRET=
# STRIPE_PUBLISHABLE_KEY=
# POSTHOG_KEY=                      # error tracking, and server-side product events
# POSTHOG_HOST=                     # empty means PostHog Cloud US
# POSTHOG_ERROR_TRACKING=true       # false keeps the key for analytics only
# WARMBLY_POSTHOG_KEY=              # the browser half, used by web + admin + form pages
# WARMBLY_POSTHOG_SESSION_REPLAY=true # false keeps the key and records no sessions
# SENTRY_DSN=                       # the other backend, alongside PostHog or instead
# WARMBLY_SENTRY_DSN=

# ── Optional: swap infrastructure ─────────────────────────────────
# BLOB_PROVIDER=s3
# BLOB_BUCKET=warmbly
# AWS_ENDPOINT_URL_S3=https://s3.example.com
# AWS_REGION=us-east-1
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# KMS_PROVIDER=aws
# KMS_AWS_KEY_ID=alias/warmbly
# EVENTBUS_PROVIDER=kafka           # needs the -kafka images (see below)
# KAFKA_BOOTSTRAP_SERVERS=broker:9092
# PRIMARY_DB=postgres://user:pass@host:5432/warmbly?sslmode=require
# REDIS=redis://host:6379

# ── Production ────────────────────────────────────────────────────
# APP_ENV=prod                      # refuses the published default secrets

Compose 安装应复制仓库根目录的 .env.example 模板:执行 cp .env.example .env 后即可原样启动,模板中被注释掉的每一行都有其原因。deploy/config/env.example 适用于 Kubernetes 和直接运行二进制文件的部署,这些环境不会自动替你填入默认值。逐个变量的完整说明、默认值及重启要求请参见配置参考。

通过自己的网络访问

在 .env 中设置一个变量,所有 URL(应用、CORS、WebSocket、跟踪、Blob、前端配置)都会由它派生:

PUBLIC_HOST=192.168.1.50        # your machine's LAN IP, or a domain

使用 make up 重新创建容器,然后打开 http://192.168.1.50:5173。

设置 PUBLIC_HOST 后,所有派生 URL 都会使用它,http://localhost:5173 将无法使用:浏览器发送的来源是 localhost,而它已不在 CORS_ALLOW_ORIGINS 中,因此 API 返回 403。请使用你配置的地址。如需同时保留两者,请自行列出:

CORS_ALLOW_ORIGINS=http://192.168.1.50:5173,http://192.168.1.50:5174,http://localhost:5173,http://localhost:5174

在支持 HTTPS 的反向代理后运行

Compose 只会根据 PUBLIC_HOST 派生普通 http:// URL,因此请在代理(Caddy、nginx、Traefik)上终止 TLS,并显式设置各 URL:

APP_URL=https://app.example.com                       # proxy to :5173
API_PUBLIC_URL=https://api.example.com                # proxy to :8080
CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
WEBSOCKET_URL=wss://ws.example.com/socket/websocket   # proxy to :4000
TRACKING_DOMAIN=t.example.com                         # proxy to :3000; separate, neutral domain, and the CNAME customers point at
PHX_HOST=ws.example.com

为管理面板设置独立主机名,代理到 :5174,并按上例将其加入 CORS_ALLOW_ORIGINS。管理面板从 API_PUBLIC_URL 读取 API 基础地址,无须其他配置。

所有调用 API 的来源都必须列入 CORS_ALLOW_ORIGINS。未列出的来源会在预检请求中收到 403,在浏览器中表现为应用能够加载,但随后所有请求都失败。

请确保代理通过 HTTP/1.1 提供 WebSocket 主机服务。升级握手无法通过 HTTP/2 表达,因此如果代理强制该主机名的客户端连接使用 h2,实时功能会失效,而其他功能仍可运行。

平台邮件

平台自身发送的邮件包括注册验证码、密码重置、团队邀请、通知摘要,以及启用时的登录验证码。这与营销活动邮件相互独立;后者通过你连接的邮箱发送。

MAIL_TRANSPORT 决定发送方式。

传输方式作用
log将每封邮件写入后端日志,不实际投递。这是 Compose 默认值,使新安装在尚无中继时也能运行。直接运行二进制文件时,如果既没有 MAIL_TRANSPORT 也没有 SMTP_HOST,则回退到 ses
smtp实际的邮件提交中继
sesAWS SES;需要 AWS 凭据和已验证的发信身份

这一默认设置经过有意设计。自托管安装的登录不依赖邮件,因此未配置中继只会影响密码重置和邀请,不会阻止访问。可使用以下命令从日志中读取验证码:

docker compose -p warmbly logs backend | grep -B2 -A12 "MAIL_TRANSPORT=log"

使用真实邮件中继

MAIL_TRANSPORT=smtp
SMTP_HOST=smtp.example.com
SMTP_USERNAME=apikey
SMTP_PASSWORD=...
EMAIL_NAME=Warmbly
EMAIL_ADDRESS=[email protected]

SMTP_SECURITY 决定传输安全模式,端口会随之确定:

SMTP_SECURITY端口调用时机
starttls587默认值,几乎所有服务商都要求此模式
tls465隐式 TLS
none25明文。仅适用于邮件接收器或同一主机上的中继

设置安全模式或端口中的任意一项,另一项会自动推导,因此只设置 SMTP_PORT=465 即可。

凭据绝不会通过未加密连接发送。如果中继不提供 STARTTLS,而你设置了用户名,发送会失败,不会明文传输密码。SMTP_AUTH 默认为 auto,会选择中继声明支持的最强机制;如需固定机制,可显式设置为 plain、login 或 cram-md5。SMTP_TLS_INSECURE_SKIP_VERIFY=true 用于采用私有 CA 的内部中继,不应对公共服务商使用。

检查是否正常工作

后端启动时会尝试连接中继一次并记录结果,因此配置问题会出现在 docker compose logs backend 中,而不会等到数周后密码重置失败才暴露。

管理面板的 实例 > 设置与健康 > 服务 选项卡包含平台邮件卡片,显示当前传输方式,提供会原样显示 SMTP 错误的实时连接检查,以及测试发送按钮。

邮件送达能力

自托管实例发出的邮件与其他发件人一样,受相同规则约束。请为 EMAIL_ADDRESS 中的域名发布 SPF 和 DKIM,并使用你控制的域名:From 地址使用没有相关记录的域名,是重置链接落入垃圾邮件最常见的原因。

品牌设置

事务性邮件默认包含 Warmbly 名称和法律页脚。可使用 EMAIL_BRAND_* 变量覆盖;如果实例会向你的用户发邮件,建议设置:

EMAIL_BRAND_NAME=Acme
EMAIL_BRAND_LEGAL_ENTITY=Acme Ltd
EMAIL_BRAND_WEBSITE_URL=https://acme.example.com
EMAIL_BRAND_SUPPORT_EMAIL=[email protected]

邮件中的所有链接(密码重置、邀请、欢迎邮件中的控制台按钮)都由 APP_URL 构建。请设置此值,否则这些链接可能指向你的部署之外的位置。

连接邮箱

安装后通常首先需要连接邮箱,这也是唯一需要 Warmbly 之外凭据的环节。

有多种方式可以连接发信邮箱,只有 OAuth 和整域连接需要额外设置。

邮箱类型所需设置变量
任意 SMTP + IMAP 服务商无无
Gmail / Google Workspace使用应用密码(默认方式)无须设置。如需提供正逐步停用的逐邮箱 Google 登录,则需要 Google Cloud OAuth 客户端BOX_GOOGLE_OAUTH_CONNECT, BOX_GOOGLE_CLIENT_ID, BOX_GOOGLE_CLIENT_SECRET
Outlook / Microsoft 365注册 Entra ID 应用BOX_OUTLOOK_CLIENT_ID, BOX_OUTLOOK_CLIENT_SECRET
整个 Google Workspace 域名或 Microsoft 365 组织Google 需要 Google Cloud 服务账户;Microsoft 使用上述 Outlook 应用,并添加三项应用权限。参见整域邮箱连接GOOGLE_WORKSPACE_DELEGATION_KEY_FILE(或 GOOGLE_WORKSPACE_DELEGATION_KEY)、BOX_OUTLOOK_CLIENT_ID、BOX_OUTLOOK_CLIENT_SECRET

这些不是用于登录的变量

BOX_GOOGLE_* 用于连接用于发信的邮箱。GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET(没有 BOX_ 前缀)的用途完全不同:它们启用登录 Warmbly 的“使用 Google 登录”。可以配置任意一组、两组或都不配置,两者无法相互替代。唯一的交叉点是:Google Workspace 管理员为整域连接证明域名时,如已设置 GOOGLE_CLIENT_ID 客户端,就通过该客户端登录,否则使用 BOX_GOOGLE_* 客户端。

SMTP 与 IMAP

无须预先配置。在控制台中填写主机、端口、用户名和密码即可添加邮箱。SMTP 必须使用 587 或 465 端口;IMAP 通常使用 993。

Gmail 与 Google Workspace

默认无须配置:连接对话框会引导用户通过 smtp.gmail.com 和 imap.gmail.com 使用 Google 应用密码,详见邮箱指南。对于 Google Workspace,推荐整域连接,需要服务账户而非 OAuth 客户端。逐邮箱 Google 登录正在逐步停用。只有希望为新 Gmail 邮箱提供该方式时才需要配置 Google OAuth 客户端,同时还需设置 BOX_GOOGLE_OAUTH_CONNECT=true;未开启时,客户端仍会为已通过此方式连接的邮箱刷新令牌,并在 Google 使授权失效后支持重新授权。

在 Google Cloud 控制台 中创建或选择项目,并启用 Gmail API。

配置 OAuth 同意屏幕。Workspace 域名可选择内部(Internal),跳过 Google 验证审核。个人 Gmail 账户需要选择外部(External);通过验证前仅限测试用户使用。

创建类型为 OAuth 客户端 ID、应用类型为Web 应用的凭据,将授权重定向 URI 设为 API 基础地址加回调路径:

https://api.example.com/addresses/google/callback

默认本地安装中,该地址是 http://localhost:8080/addresses/google/callback。

Warmbly 实际发送的基础地址是 API_PUBLIC_URL,因此必须与此处注册的地址一致。使用反向代理时,应填写公开 API 主机,而非容器绑定的地址。如果未设置 API_PUBLIC_URL,基础地址会回退到 http://localhost:8080;这适合本地安装,但不适合任何代理部署。

将客户端 ID 和密钥写入根目录的 .env:

BOX_GOOGLE_CLIENT_ID=1234567890-abc123.apps.googleusercontent.com
BOX_GOOGLE_CLIENT_SECRET=GOCSPX-your-secret-here
BOX_GOOGLE_OAUTH_CONNECT=true

然后运行 make up,使用新值重新创建容器。

Outlook 与 Microsoft 365

在 Entra ID 门户 中进入应用注册,创建新的注册。

添加一个指向 API 基础地址的 Web 重定向 URI:

https://api.example.com/addresses/outlook/callback

在API 权限下,仅添加邮箱所需的 Microsoft Graph 委托权限:User.Read、Mail.Send、Mail.ReadWrite,以及用于签发刷新令牌的 offline_access,不要添加其他权限。发送和同步均通过 Graph,因此不会请求旧版 IMAP.AccessAsUser.All 和 SMTP.Send 权限,也不应手动添加:它们都只能由管理员同意,添加后原本每位用户都能完成的登录将变成只有租户管理员才能完成。

在证书和密码下创建客户端密码,然后在根目录 .env 中设置两个值:

BOX_OUTLOOK_CLIENT_ID=00000000-0000-0000-0000-000000000000
BOX_OUTLOOK_CLIENT_SECRET=your-client-secret-value

非管理员用户与租户级授权

上述四项权限均不是管理员专属,但普通用户是否可以同意,由各 Microsoft 365 组织决定,不能由你决定。有两种情况会将用户带到 Microsoft 的需要管理员批准页面:

  • 应用未经验证。 对于 2020 年 11 月之后注册、没有已验证发布者的多租户应用,如果请求邮件权限,Microsoft 基于风险的升级同意机制会将其视为高风险,要求所有用户联系管理员。为应用注册验证发布者(需免费 Microsoft AI Cloud Partner Program 账户)后,使用默认同意设置的组织中的用户即可自行批准。
  • 组织不允许用户同意,或仅允许用户同意被管理员归类为低影响的权限。你这一侧的设置无法改变该限制。

无论哪种情况,管理员为整个组织批准一次后,其中各邮箱即可自行登录。登录单个邮箱面板会在首次尝试后向成员展示可发送给管理员的批准链接;管理员也可进入企业应用程序 > 你的应用 > 权限 > 授予管理员同意操作。Warmbly 的授权请求使用 prompt=select_account,从不使用 prompt=consent,因此后续所有用户都会沿用该授权。

工作节点也需要这些配置

后端发起 OAuth 流程,但令牌到期时由各工作节点刷新令牌。Compose 会自动将 BOX_* 值传给工作节点,远程工作节点也会在注册配置中收到它们。如果节点缺少这些值,邮箱连接时看似正常,但约一小时后首个访问令牌过期时就会悄然停止。

使用 Google 或 Apple 登录(可选)

该功能与邮箱相互独立,与发送无关。邮箱地址和密码登录以及通行密钥,无须以下任何配置即可使用。

GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
# Optional. Defaults to <API_PUBLIC_URL>/v1/auth/google/callback
GOOGLE_REDIRECT_URI=https://api.example.com/v1/auth/google/callback

APPLE_APP_ID=...            # the Services ID, not the app's bundle id
APPLE_TEAM_ID=...
APPLE_KEY_ID=...
APPLE_KEY_SECRET=...        # the Sign in with Apple .p8, as PEM or base64 (base64 -i AuthKey_XXXX.p8)
# Optional. Defaults to <API_PUBLIC_URL>/v1/auth/apple/callback
APPLE_REDIRECT_URI=https://api.example.com/v1/auth/apple/callback

重定向 URI 属于 API,而非控制台

服务商会将浏览器重定向到后端提供的路由,因此默认地址由 API_PUBLIC_URL 派生。如果在服务商处注册 https://app.example.com/auth/google/callback,OAuth 客户端本身可能配置成功,但登录按钮会跳转到不存在的控制台路由。Warmbly 启动时会记录需要注册的准确 URI:Sign in with Google enabled (redirect ...)。

在 Google Cloud 控制台的Web 应用客户端中,将该 URI 注册为授权重定向 URI;在 Apple 开发者控制台的 Services ID 中,将其注册为返回 URL。Apple 还要求 HTTPS,因此“使用 Apple 登录”无法用于普通 HTTP 本地安装。

在 Apple 侧,将 Services ID 和密钥归到 iOS 应用的 App ID 下,并将其设为主要 App ID。这样,同一用户在网页和应用中会获得相同的 Apple 身份,并登录同一账户。还需在 用于邮件通信的 Apple 登录(Sign in with Apple for Email Communication) 下注册发信域名和发件地址,否则选择“隐藏邮件地址”的用户将收不到登录验证码。

配置任意一种后,GET /auth/config 会公布其可用状态,登录页面就会显示对应按钮。首次登录会创建账户、所属组织和试用,并与密码注册一样遵循 DISABLE_REGISTRATION 和 SSO_AUTO_PROVISION。

身份验证

自托管默认值与托管产品不同,因为自行运行的部署不应依赖尚未配置的基础设施。DEPLOYMENT_MODE=self_hosted 会选择这些默认值;每项都可单独覆盖。

完整说明请参见账户与访问:包括三种注册模式和首次启动豁免、有无邮件中继时邀请团队成员的方式、单点登录自动创建账户、平台管理员,以及恢复被锁定实例所需的全部命令。

设置自托管默认值作用
AUTH_LOGIN_CODEoffalways、new_device 或 off。控制登录是否还需要发送到账户邮箱的验证码
REQUIRE_EMAIL_VERIFICATIONfalse注册时是否必须先确认邮件验证码,才创建账户
DISABLE_REGISTRATIONinvite_onlytrue、false 或 invite_only。控制谁可以通过注册表单创建账户。单点登录自动创建账户遵循相同设置,并受 SSO_AUTO_PROVISION 控制
DISABLE_PASSWORD_LOGINfalse完全关闭邮箱地址和密码登录,用于仅允许 SSO 的部署
SSO_AUTO_PROVISIONfalse是否允许仅凭已验证身份提供方的断言创建账户
AUTH_IP_RATE_LIMIT60每个来源 IP 在每 15 分钟内允许发起的未登录身份验证请求数

为何默认关闭登录验证码

每次登录都发送验证码,会让邮件中继成为所有身份验证的单点故障,而且邮件并非第二因素:NIST SP 800-63B 规定不得将电子邮件用于带外身份验证,OWASP ASVS 也有相同要求。验证码仍用于适合电子邮件的场景,即注册时验证地址和恢复账户。

设置 AUTH_LOGIN_CODE=new_device 后,只有从未登录过的设备才需要验证码;设置为 always 则采用托管产品的行为。TOTP 和通行密钥都更强,且均可使用。

更强的身份验证因素

TOTP 适用于所有登录方式,包括 Google、Apple 和单点登录。使用通行密钥时不会额外要求 TOTP:通过用户验证的通行密钥已经是绑定当前来源的持有因素。

通行密钥需要安全上下文,因此仅在 APP_URL 使用 HTTPS 或 localhost 地址时可用。通过普通 HTTP 访问的局域网 IP 不能作为 WebAuthn 依赖方 ID;后端会检测并在启动时记录,登录页面会隐藏该选项,避免在浏览器中失败。注册通行密钥后更改 APP_URL,会使所有已注册通行密钥失效。

在反向代理后运行

将 TRUSTED_PROXIES 设置为代理连接所使用的 CIDR 网段:

TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12

留空表示不信任任何代理头,适合直接暴露后端的情况。缺少这项保护时,客户端可以任意设置 X-Forwarded-For,伪造限流、会话记录、审计条目和 API 密钥 IP 限制所使用的地址。

单点登录

通用 OpenID Connect 支持 Authentik、Keycloak、Zitadel、Pocket ID、Dex,以及任何发布发现文档的身份提供方。它包含在标准构建中,不属于付费功能,而且登录不依赖对外邮件,因此在没有邮件中继时推荐使用。

OIDC_ISSUER_URL=https://id.example.com/application/o/warmbly/
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...
OIDC_REDIRECT_URL=https://api.example.com/v1/auth/oidc/callback
OIDC_ALLOWED_DOMAINS=example.com
OIDC_DEFAULT_ORG=<organization uuid>

重定向 URI 是 /v1/auth/oidc/callback,不是 /api/v1/auth/oidc/callback。服务商按完整字符串匹配,因此两侧必须完全一致。

与其他注册方式一样,单点登录遵循 DISABLE_REGISTRATION;另行启用 SSO_AUTO_PROVISION 后,可允许已验证断言在任何注册模式下创建账户。域名限制及账户如何绑定服务商 subject 声明等其余内容,请参见账户与访问。

配合 DISABLE_PASSWORD_LOGIN=true 可部署为仅允许 SSO 登录。

服务商开关

子系统Compose 默认值可选方案
事件总线NATS JetStream(EVENTBUS_PROVIDER=nats)Kafka(=kafka,需要带 kafka 标签的构建)
编解码器JSON(CODEC_PROVIDER=json)Avro 与 Schema Registry(=avro,需要相同构建标签)
根密钥本地 AES(KMS_PROVIDER=local)AWS KMS(=aws)
Blob 存储文件系统(BLOB_PROVIDER=filesystem)S3、MinIO、R2(=s3,并设置 BLOB_BUCKET)
任务调度器进程内轮询程序(TASKS_PROVIDER=local)GCP Cloud Tasks(=gcloud)
计费关闭(BILLING_PROVIDER=none,全部功能开放,AI 积分不限量)Stripe(=stripe)
人机验证关闭(CAPTCHA_PROVIDER=none)Cloudflare Turnstile(=turnstile)
实时传输Redis 发布/订阅(PUBSUB_ENABLED=false)Google Cloud Pub/Sub(=true,并设置 GCP_PROJECT_ID)

需要了解三点:

  • 直接运行二进制文件时默认使用云端方案(kafka、avro、s3、aws)。Compose、Makefile 和两份环境模板会替你设置本地值。自行配置环境时必须设置这些值,否则进程会在启动时退出。

  • Kafka 和 Avro 是构建时可选功能,因此 EVENTBUS_PROVIDER=kafka 需要包含它们的构建。对于四个使用事件总线的服务,每次发布都会在同一镜像上提供带 -kafka 后缀的第二组标签:

    ghcr.io/warmbly/warmbly/backend:prod-kafka
    ghcr.io/warmbly/warmbly/consumer:prod-kafka
    ghcr.io/warmbly/warmbly/worker:prod-kafka
    ghcr.io/warmbly/warmbly/tracking:prod-kafka

    forms、updater、realtime、web 和 admin 不读取事件总线,各自只有一种镜像。如需自行构建,请传入 --build-arg GO_TAGS=kafka(Go)或 --build-arg CARGO_FEATURES=kafka(tracking);cgo 无法交叉编译,因此每种架构都需要在对应架构的机器上构建。

  • 后端、消费者和实时服务的 PUBSUB_ENABLED 必须一致。

可选子系统

功能启用方式
Stripe 计费设置 BILLING_PROVIDER=stripe,以及 STRIPE_SECRET_KEY、STRIPE_WEBHOOK_SECRET、STRIPE_PUBLISHABLE_KEY。这三项均在启动时读取,缺少任意一项,后端都会退出
Turnstile 人机验证设置 CAPTCHA_PROVIDER=turnstile,以及 TURNSTILE_SECRET(后端)和 WARMBLY_TURNSTILE_KEY(Web 与管理面板)。Compose 固定关闭人机验证,因此需使用 docker-compose.override.yml 覆盖
使用 Google 登录GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET、GOOGLE_REDIRECT_URI(默认为 <API_PUBLIC_URL>/v1/auth/google/callback)
使用 Apple 登录APPLE_APP_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_KEY_SECRET
移动推送(APNs)在后端和消费者上设置 APNS_KEY_PATH(或 APNS_KEY)、APNS_KEY_ID、APNS_TEAM_ID、APNS_TOPIC。配置不完整时会禁用推送并给出警告,不会导致崩溃
通知调优NOTIFICATION_PUSH_WINDOW(默认 5h)、NOTIFICATION_EMAIL_DAILY_CAP(默认 25,0 表示不限量)
错误跟踪POSTHOG_KEY(自行运行 PostHog 时还需 POSTHOG_HOST)或 SENTRY_DSN,浏览器应用使用 WARMBLY_POSTHOG_KEY/WARMBLY_SENTRY_DSN。可以任选一种、两者都用或都不用;包括 prod 在内的任何环境中均为可选

AI 服务提供方

省略全部 AI 变量即可关闭 AI:AI 端点会明确返回 503,其他功能正常运行。在后端和消费者上设置以下变量:

AI_PROVIDER=openai       # openai | openrouter | groq | ollama | anthropic | custom
AI_API_KEY=sk-...        # not needed for ollama
AI_MODEL=                # blank uses the provider's preset
AI_BASE_URL=             # required for custom (any OpenAI-compatible endpoint)
AI_PROVIDER端点说明
openaiapi.openai.com默认使用 gpt-6-luna;也是唯一支持批量生成预热内容的提供方
openrouteropenrouter.ai一个密钥访问所有厂商,通过 AI_MODEL 切换
groqapi.groq.com速度快,提供免费额度
ollamalocalhost:11434无须密钥的免费本地模型(AI_FREE 自动为 true)
anthropicapi.anthropic.comAnthropic 连接器
custom你设置的 AI_BASE_URLvLLM、LocalAI、LM Studio

可选用 AI_MODEL_TRIAL 和 AI_MODEL_PAID 按套餐区分模型。助手网页搜索使用 SEARCH_PROVIDER=serper(配合 SEARCH_API_KEY),或 =searxng(配合 SEARCH_API_URL)。

工作节点

发出邮件通过各邮箱自身的服务商投递,因此源 IP 属于服务商,而非工作节点。工作节点是可相互替换、无需数据库的执行器:从事件总线接收命令,通过后端内部 API 获取加密密钥,并每 90 秒发送一次心跳。

工作节点一停止就会退出调度:关闭时发送告别心跳,将自身标记为不活跃。如果未发送便意外退出(强制终止、机器失联),心跳超过 10 分钟未更新后,分配程序便不再考虑该节点。无论哪种情况,都不会继续向不再响应的机器分配任务。

在同一主机上运行更多工作节点:

docker compose -p warmbly up -d --scale worker=3

工作节点身份

每个工作节点需要稳定的 UUID,启动时按以下顺序确定:

  1. 如设置了 WORKER_ID,直接使用原值
  2. 从 WORKER_BIND_IP 派生 UUID,使多 IP 主机上的各 IP 分别对应一个工作节点
  3. 如果主机名本身是 UUID,则使用主机名
  4. 如设置了 WORKER_STATE_DIR,从该目录领取持久化 ID
  5. 否则使用新生成的 UUID

Compose 工作节点使用第 4 种情况:随附 Compose 文件在 /data/state 挂载 worker_state 卷,并将 WORKER_STATE_DIR 指向该目录。启动时,每个副本以独占锁领取卷中的一个 ID 文件,并在进程整个生命周期内持有锁,因此重建的容器能重新获得前一个容器的 UUID。--scale worker=3 也仍可使用,因为各副本锁定不同文件,ID 池会增长到副本数量。只要不删除卷,ID 就会一直保留。

如果删除状态卷或取消设置 WORKER_STATE_DIR,每次重建都会注册新工作节点,并留下仍持有原邮箱分配的旧记录。发送会自行恢复:心跳过期后分配程序会跳过旧节点,协调器会在其运行间隔内释放仍指向旧节点的邮箱,并将其分配给活跃节点。在此之前,这些邮箱发送时会报 email account not found in worker。

也可以显式固定身份:

WORKER_ID=<a uuid you generate once>     # uuidgen

仅在每台主机运行一个工作节点时设置。扩容副本共享同一环境,因此固定 WORKER_ID 会导致身份冲突;使用 --scale 时请保持未设置,让状态卷管理身份。

加入脚本会自动处理身份:它记录控制平面分配的节点 ID,并在重新运行命令时复用,因此同一机器重建后仍保留相同身份及其邮箱。

添加主机

在你拥有的机器上运行的每个 Warmbly 进程都属于节点。添加节点只需两步:签发加入令牌,然后在机器上运行一个命令。

# On the instance
warmblyctl fleet join-token

# On the new machine (needs Docker, systemd and root)
curl -fsSL https://api.example.com/join.sh | sh -s -- \
  --url https://api.example.com \
  --token <join-token> \
  --role worker \
  --region eu-central

--role 可以是 worker(收发及同步邮件)或 consumer(处理事件)。--region 可选,只影响工作节点分配:节点越接近服务商预期的邮箱登录位置,该邮箱在该节点上的分配评分就越高。

无论加入前后,都不会有任何组件反向连接该机器。它不需要入站端口、SSH 密钥或云账户;唯一涉及的凭据是加入令牌,机器只使用一次且不保存。一个令牌默认可在七天内加入任意数量机器(--ttl 最多设为 30 天);过期或签发新令牌后,旧令牌就无法再加入机器。在同一机器上重新运行相同命令,会以原有身份重新加入,并保留历史和邮箱。

脚本会注册节点,将控制平面返回的配置写入 /etc/warmbly/node.env,安装 systemd 服务及更新定时器,并启动服务。添加 --dry-run 可预览将写入的内容,不做任何更改。

节点会继承后端的地址

节点配置由后端自身环境生成。默认本地安装会生成 ENCRYPTED_KEYS_BACKEND_URL=http://localhost:8080、NATS_URL=nats://nats:4222 和 REDIS=redis://redis:6379,这些地址均无法在另一台机器上解析。添加远程节点前,请在后端将 API_PUBLIC_URL、NATS_URL 和 REDIS 设置为目标机器实际可访问的地址,并开放相应端口。

该配置携带节点所需的解密材料:内部 API 令牌、KMS_LOCAL_MASTER_KEY 和 CREDENTIALS_ENCRYPTION_KEY。如果节点通过不受你控制的网络加入,请先为 API 启用 HTTPS。

PRIMARY_DB 只会传递给直接访问 Postgres 的消费者,绝不会传递给工作节点;工作节点只能通过内部 API 访问关系数据。

配置中的其他部分也无须云凭据。运行 KMS_PROVIDER=aws 或 BLOB_PROVIDER=s3 的实例会向节点提供相应的代理访问形式,因此集群机器通过内部 API 解密密钥和签署 Blob 操作,而不自行持有云凭据。Blob 数据本身仍直接在节点和对象存储之间传输。完整架构见分拆部署。

生成配置旁的 /etc/warmbly/node.local.env 只创建一次,之后绝不重写,容器会在第二步读取它。写入其中的内容会在重新加入后保留,并覆盖自动生成的值。

BLOB_PROVIDER=filesystem 不适用于跨主机节点集群。工作节点要读取后端写入的正文,因此两者需要能够共同访问的相同存储及权限,另一台机器上的节点并不具备这些条件。加入脚本会创建并挂载 BLOB_FS_ROOT,使节点可以启动,同时给出警告;但节点读取不到正文时,发送仍会失败。在其他主机上运行节点前,请使用 BLOB_PROVIDER=s3,并配置双方均可访问的存储桶。

保持节点更新

节点自行更新。每次心跳都会询问控制平面应运行的版本;如果与当前版本不同,主机侧定时器会拉取对应镜像并重启服务。整个过程不向节点推送。

warmblyctl fleet version            # what the fleet should be on
warmblyctl fleet version v1.4.2     # move the whole fleet
warmblyctl fleet channel stable     # follow releases again
warmblyctl fleet pin <node> v1.4.1  # hold or canary one machine

设置标签也会固定更新渠道,因此后续发布不会悄然撤销有意执行的回滚。fleet channel stable 可恢复跟随正式发布。

下发给节点的版本同时决定镜像构建变体。使用 EVENTBUS_PROVIDER=kafka 的实例会下发 v1.4.2-kafka,因为默认镜像不含 CGO 和 librdkafka;节点如果运行默认镜像,却从配置读取到 EVENTBUS_PROVIDER=kafka,会在启动时失败。无论版本如何解析,包括固定版本,都会添加该后缀,因此 fleet pin <node> v1.4.1 最终仍会下发节点能够运行的镜像。如果自行发布的镜像采用不同命名方式,可在后端设置 FLEET_IMAGE_VARIANT 更改或禁用此行为;设为空值即可关闭。

后端被有意排除在自动更新之外:它负责告诉所有节点应使用哪个版本,因此应与其他基础设施一样手动升级。后端升级后,节点集群会随之跟进。

监控节点集群

warmblyctl fleet list
ROLE      NAME      STATE  VERSION           REGION      MEM    SEEN     ID
worker    box-1     live   v1.4.2            eu-central  128MB  12s ago  de434ce4-...
consumer  events-1  live   v1.0.0 -> v1.4.2  -           96MB   30s ago  2b334317-...

STATE 可以是 live(正在发送心跳)、unreachable(已注册但无响应)或 stopped(已报告关闭)。版本显示为 a -> b 表示节点尚未切换到目标版本。管理面板的节点集群页面提供相同视图。

节点一停止就会退出调度:关闭时发送告别心跳,将自身标记为不活跃。如果未发送便意外退出(强制终止、机器失联),心跳过期后分配程序便不再考虑该节点。无论哪种情况,都不会继续向不再响应的机器分配任务。

镜像与发布版本

CI 将多架构镜像发布到 ghcr.io/<owner>/warmbly/;任何具备 packages: write 权限的分叉仓库都可使用:

触发条件镜像标签
推送到 mainbackend、consumer、worker、forms、updater、tracking、realtime:<sha>, :dev
创建 vX.Y.Z 标签上述全部镜像,以及 web 和 admin:vX.Y.Z, :vX.Y, :vX, :prod

运行分叉仓库时请注意:GHCR 创建的软件包默认私有,不继承仓库可见性。因此即使工作流成功,其他人仍无法读取发布的镜像,直到你逐个手动将软件包设为公开。个人分叉可从个人资料的 Packages 选项卡进入各软件包设置;组织分叉可从组织的 Packages 进入同一页面,并且必须先在组织软件包设置中允许公开软件包,否则控件会不可用。这两项均无对应 API。make images-check 可在不使用凭据的情况下检查指定标签的状态,发布流程在正式发布前也会执行相同检查。

docker-compose.yml 中每个应用服务都同时包含 image: 和 build:,因此同一文件支持两种方式。docker compose pull && docker compose up -d 运行已发布镜像,不执行编译;docker compose up --build 仍会构建当前检出的源码。可在 .env 中设置 WARMBLY_TAG=v1.4.2 固定版本,或将 WARMBLY_IMAGE_PREFIX 指向你自己的镜像仓库。

要让节点集群升级到新版本,只需通过 warmblyctl fleet version vX.Y.Z 设置一次目标版本。不会主动推送:各节点在下次心跳时询问目标版本,再由自身更新定时器拉取并重启。控制平面迁移只能向前执行,因此优先通过后续修复版本解决问题,而非回滚。

升级与备份

管理面板会提示新版本:侧栏底部的版本标记亮起,其中的 更新并重启 会拉取源码、重新构建、重启发生变化的服务,并在后端恢复后重新连接。make up 会启动使该按钮生效的更新器。完整流程、执行内容及无 Docker 时的运行方式,见更新。

手动执行时同样只有两步:

make upgrade                 # git pull --ff-only, then make up; migrations apply on backend boot

保留现有数据进行升级是安全的:迁移只向前执行,并在后端启动时应用。

更新时保留进行中的工作

只要平台给予足够时间,被替换的服务就会完成已开始的工作:

  • 后端。 收到停止信号后,不再领取新的导入行;对于尚未开始处理的行,会交还任务,让下一个实例立即接手;对于正在执行的邮箱连接和导入行,最多等待 45 秒。用户已发起的连接不会因刷新或关闭页面而停止。仍被中断的工作会在下一实例恢复:导入行在两分钟租约到期后继续,已保存但尚未分配的邮箱由工作节点协调器恢复。
  • 工作节点和消费者。 收到停止信号时正在处理的命令会在进程退出前完成,只停止读取下一条命令。原工作节点已离线的凭据检查会转发给其他节点;重启的工作节点在首次心跳时会重新收到其邮箱。
  • 进行中的供应商授权和登录状态保存在数据库和 Redis 中,而不是进程内,因此重启不会影响它们。

从停止信号到强制终止,至少为每个服务预留 60 秒。仓库中的 docker-compose.yml 和安装程序生成的服务栈均设置 stop_grace_period: 60s;通过 warmbly join 加入的节点使用 45 秒的 docker stop(在此版本之前加入的节点仍使用 10 秒停止等待,需重新加入才会更新)。在 Railway 上,为后端、消费者和所有工作节点服务设置 RAILWAY_DEPLOYMENT_DRAINING_SECONDS=60;在 Kubernetes 上设置 terminationGracePeriodSeconds: 60。

创建备份

一个命令即可写出必须一起恢复的三项内容:

docker compose -p warmbly exec backend warmblyctl backup --out /data/blobs/warmbly.tar.gz
docker compose -p warmbly cp backend:/data/blobs/warmbly.tar.gz ./warmbly.tar.gz \
  && docker compose -p warmbly exec -T backend rm -f /data/blobs/warmbly.tar.gz

备份包包含数据库、Blob 根目录和加密密钥。warmblyctl restore 可在另一台主机上恢复;如果该主机密钥与备份包使用的密钥不同,命令会拒绝执行。备份调度和实例迁移见数据控制。

如果希望自行手动组装备份,以下三项缺一不可:

docker compose -p warmbly exec -T postgres pg_dump -U warmbly warmbly_dev > backup.sql
  1. Postgres:使用上述命令备份
  2. blobs 卷:使用文件系统存储时,它保存上传文件和邮件正文
  3. 你的 .env:尤其是其中的两把加密密钥

缺少密钥的数据库备份无法读取

所有邮箱凭据都使用 KMS_LOCAL_MASTER_KEY 和 CREDENTIALS_ENCRYPTION_KEY 加密。如果使用不同密钥恢复数据库,记录虽然都在,却全部无法解密。

恢复备份

请在后端启动之前恢复到空数据库。转储自带数据库结构及其 schema_migrations 记录,如果后端已经执行迁移,就会与转储冲突。

# 1. Put your .env back first, with the original encryption keys.

# 2. Start only Postgres, so nothing migrates underneath you.
docker compose -p warmbly up -d postgres

# 3. Load the dump.
docker compose -p warmbly exec -T postgres psql -U warmbly -d warmbly_dev < backup.sql

# 4. Bring up the rest. The backend sees the restored schema version
#    and applies only migrations newer than the backup.
make up

恢复到已有表的数据库时,会在途中报出“already exists”错误,留下只恢复了一部分的数据库结构。遇到这种情况,请执行 make reset,然后从第 2 步重新开始。

Compose 以外的部署方式

Compose 文件是模板,并非强制要求。deploy/docker/ 中的 Dockerfile,以及 tracking/、web/ 和 admin/,构成各部署单元;配合上述环境变量参考,任何容器平台均可运行。前端在容器启动时读取 WARMBLY_* 变量,因此同一镜像可在任意环境使用。

也支持完全不用容器:每项服务都是静态二进制文件或静态构建。不使用 Docker 部署介绍从源码构建并在 systemd 下运行、由 nginx 提供前置代理的方式,使用 deploy/systemd/ 中的服务单元和 deploy/nginx/ 中的站点配置。

安装完成后

日常运维问题由以下三页说明,本页不再重复:

  • 首次运行:认领实例、重新签发设置链接,以及无浏览器创建所有者
  • 账户与访问权限:注册模式、邀请团队成员、单点登录及恢复被锁定的实例
  • 实例健康状态:管理面板对当前部署执行的检查,以及 make doctor

故障排查

自托管用户实际遇到的常见症状和修复命令都在 故障排查 中:构建失败、首次运行出现 403、登录问题、工作节点始终无法注册,以及实时功能始终不可用。

先运行 make doctor,它会执行完整健康检查,并在发现错误级别问题时返回非零退出码。make logs 跟踪全部日志,make logs backend 跟踪单个服务。

快速参考

make up          # build and start the platform, then print the claim link
make claim       # check instance state and print the claim link or the next command
make doctor      # run every instance health check (non-zero exit on an error)
make cli status  # any warmblyctl command inside the backend container
make seed-demo   # load the showcase workspace

make status      # docker compose ps
make logs        # follow logs (make logs backend for one service)
make stop        # stop everything, keep data
make down        # stop and remove containers, keep data
make reset       # tear down including volumes (destroys data and the encryption keys)

make gen-key     # print a fresh KMS_LOCAL_MASTER_KEY
make grant-admin [email protected]   # promote an existing account to platform admin

make dev         # local development stack instead (native Go, hot reload)
make sandbox     # live end-to-end demo environment

在后端容器内执行:

docker compose -p warmbly exec backend warmblyctl status
docker compose -p warmbly exec backend warmblyctl setup-link
docker compose -p warmbly exec backend warmblyctl user create --email [email protected] --admin
docker compose -p warmbly exec backend warmblyctl user reset-password --email [email protected]
docker compose -p warmbly exec backend warmblyctl org export --org [email protected] --out /tmp/workspace.warmbly.zip

将工作区迁出此实例

warmblyctl org export 将整个工作区写入一个文件,org import 则在另一实例读回。自托管安装与托管服务之间的双向迁移,以及两个自托管实例之间的迁移,都支持此方式。

邮箱凭据使用实例本地密钥加密,因此只有同时传入 --with-credentials 和口令才会迁移;否则导入目标中的邮箱需要重新连接。归档内容见导出与导入,全部选项见 warmblyctl 参考。

另请参见:配置参考、本地开发、架构、事件系统。

本页目录