故障排查
介绍自托管用户实际遇到的错误、各错误的原因,以及可用于修复的命令。
请在本页搜索你看到的精确错误字符串。每一行都给出具体原因和命令,不只列问题类别。
点击注册后收到 403
403 此服务器不接受新账户。请联系管理员获取邀请。
这并不是故障,也不需要为了登录而修改设置。
自托管实例默认使用 DISABLE_REGISTRATION=invite_only,关闭面向公众的注册表单。只有一个例外:当 users 表为空时,允许任何注册,这让全新实例可以被认领。只要有一个账户存在,该例外就永久结束。
出现这个 403,通常经历了以下过程:
- 你曾运行
make dev,它默认创建九个示例账户。 make dev和make up固定使用同一 Compose 项目、同一个warmbly_postgres_data卷和warmbly_dev数据库,因此那些示例账户就是实例账户。- 所以
make up发现实例已认领,不签发也不打印设置链接。 - 剩下的入口只有注册表单,而
invite_only会拒绝它。
按你的目的选择修复方式:
| 目的 | 操作 |
|---|---|
| 立即为自己创建账户 | 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],再打开打印的 URL |
| 让团队成员加入 | 从设置 > 成员邀请并分享链接。参见邀请团队成员 |
| 完全重新开始 | make reset && make up。这会销毁卷,包括按组织加密密钥 |
| 开放公众注册 | 设置 DISABLE_REGISTRATION=false 并重启。先阅读影响:开放公众注册 |
不确定当前处于什么状态?
make doctor较新构建为仅邀请拒绝提供独立机器代码 registration_invite_only,文字说明为“此服务器仅接受邀请。请联系管理员邀请你,再打开邀请中的链接创建账户。”原因和修复方式相同。
首次运行与登录
| 现象 | 原因与修复 |
|---|---|
注册时出现 403 This server is invite only. | 自托管的 DISABLE_REGISTRATION 默认为 invite_only。打开携带令牌的邀请链接,执行 warmblyctl user create --email [email protected] --admin,或设置 DISABLE_REGISTRATION=false 并重启。参见账户与访问权限 |
403 This server is not accepting new accounts. | 设置了 DISABLE_REGISTRATION=true。任何邀请都不能覆盖,只能通过 warmblyctl user create 添加账户 |
| 注册成功一次后就停止 | 首次启动例外仅在 users 表为空时适用。参见首次启动例外 |
make up 没有打印认领链接 | 数据库已有账户,通常来自之前的 make dev。使用 warmblyctl user create --email [email protected] --admin,或用 make reset 从头开始,后者会销毁加密卷 |
| 认领链接提示无效或过期 | 有效期 24 小时、仅限一次使用,Redis 清空后也会失效。用 warmblyctl setup-link 打印新链接 |
/setup 提示实例已经设置 | 已有账户,因此不会再签发认领链接。使用 warmblyctl user reset-password --email ...,或 warmblyctl user create --email [email protected] --admin |
被邀请者收到 invitation_invalid | 邀请已过期,默认七天;或已取消、已使用,或输入地址与邀请地址不同。重新邀请即可 |
| 单点登录在服务商侧成功,之后返回 404 | 重定向 URI 必须为 <API_PUBLIC_URL>/v1/auth/oidc/callback,不能是 /api/v1/...,两侧必须精确一致。参见单点登录 |
| 单点登录不再创建新用户 | 现在创建用户遵守 DISABLE_REGISTRATION。请邀请该用户,或设置 SSO_AUTO_PROVISION=true 并重启 |
| 收不到登录验证码 | 自托管不发送登录验证码,默认 AUTH_LOGIN_CODE=off。注册和重置验证码发送到 MAIL_TRANSPORT 指定的目标;Compose 下为后端日志 |
| 收不到邀请邮件 | 原因相同。从待接受邀请复制链接并自行发送。参见没有邮件中继时 |
| 重置密码提示“尝试次数过多” | 每个地址每四小时最多请求两次密码重置。无需等待,可使用 warmblyctl user reset-password |
| 重置表单提示已发送,却收不到邮件 | 此地址在实例中没有账户。为避免泄露账户是否存在,表单有意使用相同提示。后端会记录 password reset requested for an address with no account 及收到的地址。如果没有这条日志,说明确实尝试发信,应检查传输配置 |
| 通行密钥按钮缺失 | 通行密钥需要安全上下文,因此 APP_URL 必须为 HTTPS 或 localhost 地址。通过普通 HTTP 访问的局域网 IP 不能作为依赖方 |
| Google、Apple 或通行密钥按钮出现却无法使用 | 登录界面遵循 GET /auth/config。如果仍显示这些按钮,说明请求失败,界面会说明尝试的 API URL |
| 唯一管理员账户无法登录 | 使用 warmblyctl user create --email [email protected] --admin。所有恢复命令见恢复访问 |
构建失败
| 现象 | 原因与修复 |
|---|---|
no space left on device,常在某个服务编译中途出现 | Docker 磁盘不足。用 docker builder prune -af 和 docker image prune -af 释放空间,确认主机约有 10 GB 空闲,再运行 make up |
拉取基础镜像时出现 failed to authorize: ... EOF | 镜像仓库临时异常。重新运行 make up,已完成层会使用缓存 |
出现 ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY,发生在 forms 镜像构建期间 | forms/node_modules 来自原生 make forms 或 make dev,被带入 Docker 构建上下文,覆盖了镜像自身安装;没有终端时 pnpm 不会重建。当前检出会排除所有 node_modules 并以 CI 模式运行 pnpm;旧检出可先执行 rm -rf forms/node_modules,再运行 make up |
macOS 上出现 failed to xattr /path/._something: operation not permitted | 检出目录位于不原生支持扩展属性的文件系统,如 exFAT、NTFS 或网络共享,macOS 因而写入 BuildKit 无法读取的 ._* 旁文件。先运行 dot_clean -m .,再运行 find . -name '._*' -delete,然后重试。克隆到 APFS 卷可避免此问题 |
服务栈已经启动,但功能异常
| 现象 | 原因与修复 |
|---|---|
| 后端启动时退出 | 阅读最前面的日志。常见原因包括:APP_ENV=prod 使用公开默认秘密值,消息会提到 ALLOW_INSECURE_DEFAULTS;Compose 之外使用了云服务商默认值;CREDENTIALS_ENCRYPTION_KEY 不是精确的 64 个十六进制字符;或缺少 EMAIL_ADDRESS、EMAIL_NAME、GEODB_PATH |
| 控制台能加载,但每个请求失败 | 检查 http://localhost:5173/config.js 是否返回正确 API_URL。它在容器启动时根据 WARMBLY_API_URL 生成。URL 正确时,可能是 CORS_ALLOW_ORIGINS 缺少对应来源 |
| 没有实时更新,在线状态为空 | AUTH_SECRET 必须与实时服务的 JWT_SECRET 相同,后端、消费者和实时服务的 PUBSUB_ENABLED 必须一致。在实例 > 配置 > 环境比较指纹 |
| 工作节点或跟踪服务收到 401 | 后端和跟踪服务的 INTERNAL_API_TOKEN 必须一致。后端设置了 NODE_BROKER_TOKEN 时,工作节点提交该值,否则使用 INTERNAL_API_TOKEN,以 ENCRYPTED_KEYS_WORKER_TOKEN 传递,也必须匹配。未设置时保守拒绝 |
| 连接邮箱时提示“没有可用邮箱工作节点” | 最近十分钟没有任何节点心跳。检查 make status 是否显示 worker 运行,make logs worker 是否正常。ENCRYPTED_KEYS_BACKEND_URL 或节点令牌为空,会让节点启动却始终不登记,且没有明显提示 |
主机可达,连接邮箱却报 SERVER_UNREACHABLE | 安全设置可能与服务器不匹配。要求 STARTTLS 的服务器,对尝试隐式 TLS 的客户端会表现为不可达,反之亦然。允许任何 1 至 65535 端口,因此端口本身不再决定模式:从第一个字节就加密的服务器,应将安全设为 SSL / TLS,通常 SMTP 为 465、IMAP 为 993;原地升级加密的服务器应设为 STARTTLS,通常 SMTP 为 587 或 2525、IMAP 为 143。错误会说明失败步骤和原因。dial 被拒绝或超时表示从未到达服务器。tls: first record does not look like a TLS handshake 指向协议不匹配。其他 tls: 连接失败描述证书或握手问题,本身不代表安全模式错误。从 mail from 开始的错误则是服务器自身答复 |
发送报 SERVER_UNREACHABLE,使用的是 465 端口,而 587 可用 | 发送机器到邮件服务器的网络不允许隐式 TLS 提交;部分托管服务商封锁出站 465,但开放 587。邮箱错误会显示 dial <host>:465: connect: connection refused 或 i/o timeout。将邮箱改为 587 加 STARTTLS,或申请解除封锁 |
邮箱一两轮显示 SERVER_UNREACHABLE,工作节点日志为 imap: server sent a body structure that could not be read | 文件夹中某封邮件的结构描述无法由 IMAP 客户端读取,通常是 RFC 6533 退信,即 message/global。无需操作:工作节点从下一轮开始不请求这些邮件的结构,先整封读取,再逐封读取,最终只跳过任何方式都无法读取的邮件,并记录其文件夹和 UID。每个阶段最多导致一次失败同步 |
邮箱错误提示发送网络封锁出站 465,工作节点详情页也显示相同提示 | 工作节点观察到多个服务器在 465 无响应、却在 587 响应,因此封锁来自自身网络,而不是邮件服务器。服务商支持 587 的邮箱会通过自动回退继续发送;只接受 465 提交的服务器无法从该机器访问。许多云服务商默认封锁新服务器出站 25 和 465,需申请开放。请为工作节点地址申请开放出站 465。任意一次 465 连接成功后,警告自动清除 |
| Proton Bridge 等本地中继无法连接,也没有无安全选项 | 此选项仅在自托管实例上、主机为回环字面值 localhost、127.0.0.0/8 或 ::1 时出现,因为只有凭据不离开机器才安全。解析为 127.0.0.1 的主机名也会被拒绝,请直接输入地址。托管 Warmbly 的节点不在你的机器上,无法访问 Bridge。参见本地邮件中继 |
邮箱报告 INSECURE_REMOTE_HOST | 存储的安全模式为未加密,但主机不是本机,工作节点因此拒绝连接,避免在线路上明文发送密码。使用 SSL / TLS 或 STARTTLS 重新连接,或改回本地中继 |
连接 Outlook 邮箱停在“需要管理员批准”或“要求批准”,代码为 AADSTS90095 | 组织要求管理员批准应用。默认同意设置下,原因通常是应用注册没有已验证发布者:对于请求邮件权限的新建未验证多租户应用,Microsoft 会将每个用户都转交管理员。可以验证发布者,或让管理员通过登录单个邮箱面板在首次尝试后显示的链接,为组织批准一次;也可进入 Enterprise applications > your app > Permissions > Grant admin consent(企业应用 > 应用 > 权限 > 授予管理员同意)。检查注册只列出委派权限 User.Read、Mail.Send、Mail.ReadWrite 和 offline_access。租户级授权后,非管理员仍被拒绝,说明实例早于停止发送 prompt=consent 的修复,请升级 |
整个组织返回 AADSTS90001,提示 prompt 值 admin_consent 无效 | 实例早于将 Microsoft 365 组织授权迁移到 Microsoft 管理员同意页面的修复,请升级 |
| 邮箱约一小时后停滞 | 工作节点缺少 BOX_GOOGLE_* 或 BOX_OUTLOOK_*。后端开始 OAuth 流程,但每个节点自行刷新令牌。设置这些值并重启节点 |
| 密码正确,发送仍报身份验证错误 | Warmbly 根据服务器声明协商登录方式。如果邮箱报告 AUTH_UNSUPPORTED,服务器只提供 Warmbly 未实现的机制,例如 NTLM 或 GSSAPI,或根本没有加密连接,而 Warmbly 不会明文发送密码。使用应用专用密码或服务商文档指定的 SMTP 主机,通常可以解决 |
| 发送被拒绝且不重试 | SEND_REJECTED 或 RECIPIENT_REJECTED 表示接收服务器返回永久性 5xx,重试无法投递,只会消耗邮箱每日预算。两者都保留服务器原话,以区分地址不存在和策略阻止。临时 4xx 会自动重试,并报告为连接问题 |
| IMAP 邮箱连接成功,却始终没有邮件到达 | 查看邮箱侧边面板同步卡片中的文件夹数量。服务器可达但文件夹为零,表示服务器对每个文件夹都拒绝 STATUS;节点日志会列出跳过的文件夹。已连接邮箱的收件箱确实为空则属正常 |
| 邮箱仍显示已经修好的错误 | 连接错误会自动清除:第一次重新连上服务器的同步会解决它。错误凭据或域身份验证拒绝等需要人工处理的错误,会保留到重新连接或修复原因 |
IMAP 邮箱显示 IMAP_UNKNOWN 和 Bad token (expecting date specification),不导入任何内容 | 服务器拒绝 IMAP 客户端在 SEARCH 中写入的带引号日期,Seznam.cz 就会这样。当前工作节点会识别拒绝,每个邮箱只记录一次 server refused a dated SEARCH,改为读取每封邮件的 INTERNALDATE,导入继续,错误在下一次完整同步后清除。仍显示此问题时,请更新工作节点 |
| IMAP 邮箱停止同步,日志也安静 | 服务器断开,或防火墙删除映射但未关闭连接的会话,会在下一轮重新连接;无法访问服务器的同步轮次,会以逐渐增长、最多五分钟的间隔重试。如果仍卡住,make logs worker 可查看文件夹游标;若因同步预算暂停,邮箱侧边面板会明确说明 |
| 统一收件箱缺少某个文件夹 | 每个邮箱最多同步 100 个文件夹,可在实例 > 配置 > 实际生效限额查看。超出时保留收件箱和特殊文件夹,其余按服务器顺序选择,侧边面板同步卡片说明跳过数量。Gmail 的“所有邮件”“星标”和“重要”只是其他文件夹的标签视图,有意不同步。服务器重复列出同一文件夹名时,也可能跳过,同步卡片会说明。文件夹与另一个文件夹共用 UIDVALIDITY 已不再是跳过理由:IMAP 保证唯一的是名称,因此按名称识别文件夹。原因消失后,两类提示都会在下一轮自动清除 |
| 计划发送始终不触发 | 延迟发送通过进程内 Postgres 任务轮询器 TASKS_PROVIDER=local 运行,因此后端必须运行 |
每次发送都因 permission denied 进入死信,错误路径为 /data/blobs | blobs 卷创建于镜像取得该路径所有权之前,因此仍为 root:root,而服务以 uid 1000 运行。执行一次 docker compose -p warmbly exec -u root backend chown -R warmbly:warmbly /data/blobs 修复。blob_fs_root 健康检查会报告,当前镜像新建的卷已正确配置 |
email account <id> not found in worker | 邮箱分配给了不存在的工作节点,通常是节点重建后使用新 UUID。协调器会在其周期内释放邮箱并重新分配给活跃节点。Compose 工作节点现在将 ID 保存在 worker_state 卷中,由 WORKER_STATE_DIR 控制;卷存在后就不会反复发生。移除卷或取消 WORKER_STATE_DIR 会再次引入变化。参见工作节点身份 |
| 打开和点击从不记录 | 先用 docker compose -p warmbly ps -a 确认容器确实运行;tracking 停止不会影响其他功能,因为发送不等待它。再检查 TRACKING_DOMAIN 是否解析,服务是否在 /health 响应。如果覆盖了 KAFKA_TRACKING_TOPIC,必须同时覆盖 Rust 发布者和 Go 订阅者 |
tracking 立即退出,错误为 Bind for 0.0.0.0:3000 failed: port is already allocated | 主机上的其他进程占用常见默认端口 3000。将 TRACKING_PORT=3001 写入 .env,重新运行 make up,并将反向代理的跟踪主机指向新端口 |
后端因 duplicate migration file 不断重启 | 分支上两个迁移共用版本号,golang-migrate 在执行任何迁移前就停止。将尚未发布的迁移改为下一个可用版本,再部署。make check-migrations 会报告此问题,CI 对每个拉取请求执行相同检查。参见添加迁移 |
填充数据报 no migration found for version N | seed 镜像早于当前 Schema。带 --build 重新运行 |
| 整个工作区停止发送,横幅显示已暂停 | 滥用风险状态达到 suspended。在管理面板打开组织,阅读滥用风险状态面板发现,再撤回错误发现或直接设置状态。参见工作区滥用风险状态 |
| 工作节点心跳离线 | 没有服务主动连接节点,因此总要在机器本身诊断:先执行 systemctl status warmbly-worker,再检查能否访问后端 URL、NATS 或 Kafka,以及 Redis。warmblyctl fleet show <id> 显示最后出现时间 |
恢复
全部四种运行环境的恢复命令都在恢复访问:实例状态、列出管理员、通过打印的一次性链接重置密码、创建新所有者和管理员、清除丢失的验证器,以及重新签发首次认领链接。
破坏性的替代方案是 make reset,它删除卷并从头开始。
make reset 会销毁加密卷
卷中保存按组织数据密钥。所有密封邮箱凭据和已存储邮件正文都会永久无法解密,即使之后恢复数据库备份也一样。
不要为清除速率限制而清空 Redis
redis-cli FLUSHDB 还会销毁 bootstrap:setup_token、所有待完成身份验证会话和登录尝试计数。对于尚未认领的实例,这会丢掉唯一入口。请删除具体键,或等待窗口结束。
将错误集中到便于查看的位置
make logs 显示服务打印了什么,却不会主动告诉你凌晨三点哪个构建、哪个组织的处理器抛出了异常。Warmbly 的每种运行时都能将这些信息发送到错误跟踪器;指定目标前,没有任何运行时会这样做。
支持两种后端,默认是 PostHog:
# backend, consumer, worker, forms, tracking, realtime
POSTHOG_KEY=phc_...
# the dashboard, the admin panel, and public form pages (browser errors)
WARMBLY_POSTHOG_KEY=phc_...
# both, when your project is not on PostHog Cloud US
POSTHOG_HOST=https://eu.i.posthog.com
WARMBLY_POSTHOG_HOST=https://eu.i.posthog.com仍支持 Sentry,可单独使用,也可与 PostHog 并用:
SENTRY_DSN=https://<key>@<host>/<project>
WARMBLY_SENTRY_DSN=https://<key>@<host>/<project>同时配置两者,每个错误就发送到两者,适合在保留原服务时试用另一个。反过来,POSTHOG_ERROR_TRACKING=false 可保留 PostHog 产品分析密钥,但不向它报告异常。
使用安装器生成的 Compose 文件时,将这些值写入 .env;所有服务通过 env_file 继承,因此添加几行后执行 docker compose up -d,即可覆盖整个实例。
每个事件都带有产生它的服务、环境和构建标签,因此一个项目可容纳整个实例,并仍能筛选。异常还带有所属工作区和用户,以及存在时的失败请求 ID 和路由,使支持消息中的请求 ID 与错误跟踪中的问题对应同一事件。各属性含义和来源见配置。服务端从 APP_ENV 读取环境。控制台和管理面板从 WARMBLY_SENTRY_ENVIRONMENT 读取,因为提供静态包的容器不是报告错误的进程;无论事件发给哪个后端,都使用这一变量名。托管表单页面是浏览器应用中的例外:环境由表单服务写入页面,因此取该服务的 APP_ENV。实际中,各前端产生的事件量值得分别管理;希望分开噪声时,可给浏览器事件独立项目。
两种后端的服务器都由你选择。PostHog Cloud 免费层每月覆盖 10 万次异常,POSTHOG_HOST 也可指向自己运行的 PostHog;Sentry Cloud 免费 Developer 套餐每月覆盖 5,000 次错误,自托管 Sentry 或 GlitchTip 等兼容服务也使用相同 DSN 变量。
除非构建镜像时配置了源码映射上传,否则浏览器堆栈经过压缩;公开发布镜像已配置。自行构建时参见源码映射。
两个后端都不配置也是受支持的正常配置:不会联系任何主机,错误与之前一样写入日志。参见数据控制。
仍未解决时
make logs # follow everything
make logs backend # follow one service
make doctor # the full health check set, exits non-zero on an error可在 Discord 提问,或提交 GitHub issue,附上 make doctor 输出和失败服务日志的前 50 行。