不使用 Docker 部署

分步介绍如何从源码构建 Warmbly,并在单台 Linux 主机上以原生 systemd 服务运行,全程不使用容器。

自托管指南默认使用 Docker Compose,但 Warmbly 并没有必须依赖容器的部分:每个服务都是读取环境变量配置的单一二进制或静态构建。本页介绍如何从源码构建这些产物,在一台 Linux 主机上通过 systemd 运行,并在前方使用 nginx。

这条路径比 Compose 更长,因为需要手动完成 Compose 文件代劳的工作:安装基础服务、生成秘密值、将同一组配置分发给五个进程,以及提供两个静态前端。开始前请完整阅读一遍;第二次操作时,页面底部的速查就足够。

开始前的准备

要求原因
带 systemd 的 Linux 主机用于下方服务单元;已知 Debian 12、Ubuntu 22.04+ 及相关发行版可用
root 或 sudo 权限安装软件包、创建服务用户,以及写入 /etc 和 /opt
PostgreSQL 16、Redis 7、NATS 2.10+基础服务。可使用发行版软件包、上游仓库,或其他位置的托管实例
Go 1.25构建后端、消费者、工作节点、migrate 和 warmblyctl
Rust 稳定版,1.93 或更新构建跟踪服务
OTP 27 上的 Elixir 1.18构建实时服务
Node 22 和 pnpm构建控制台、管理面板和表单应用
nginx提供静态前端,并为 API、WebSocket、跟踪和表单主机终止 TLS
六个 DNS 名称域名下的 app、admin、api、ws、t 和 forms,全部指向该主机

工具链只需安装在构建机器上。如果不想在生产主机安装编译器,可在同架构的另一台机器构建,再复制 /opt/warmbly;除环境文件中的配置值外,下方流程没有其他路径依赖。

本页不是开发环境配置

开发 Warmbly 本身时请使用 make dev,它已经原生运行 Go 服务,只将 Postgres、Redis 和 NATS 放入容器。本页用于向其他人提供产品服务,因此需要真实秘密值、TLS 和邮件中继。

安装基础依赖服务

PostgreSQL

安装 PostgreSQL 16,然后创建后端使用的角色和数据库:

sudo -u postgres psql <<'SQL'
CREATE ROLE warmbly WITH LOGIN PASSWORD 'choose-a-password';
CREATE DATABASE warmbly OWNER warmbly;
SQL

迁移会创建所有表和索引,因此数据库中无需其他准备。每次后端启动都会自动应用迁移。

Redis

安装 Redis 7 并保留默认配置:仅监听 localhost,不设密码。Redis 保存速率限制、已解密密钥缓存和实时桥接;这些都不是持久状态,重启不会丢失关键数据。

启用 requirepass 时,将密码写入下方每个 REDIS 和 REDIS_URL 值,形式为 redis://:[email protected]:6379。

启用 JetStream 的 NATS

NATS 是一个静态二进制。通过发行版或 nats.io 安装,然后启用 JetStream、指定存储目录,并将两个监听器都绑定到回环地址:

sudo useradd --system --home /var/lib/nats --create-home nats
sudo tee /etc/nats.conf >/dev/null <<'EOF'
listen: 127.0.0.1:4222
http: 127.0.0.1:8222
jetstream { store_dir: /var/lib/nats }
EOF
sudo tee /etc/systemd/system/nats.service >/dev/null <<'EOF'
[Unit]
Description=NATS server
After=network-online.target
Wants=network-online.target

[Service]
User=nats
ExecStart=/usr/local/bin/nats-server -c /etc/nats.conf
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now nats
curl -s http://127.0.0.1:8222/healthz

两行配置尤其关键。jetstream 必须存在;缺少它时,后端会启动,但首次发布失败并提示流不存在。listen: 127.0.0.1 保证总线只在本机可用。默认 NATS 流量没有身份验证,其中既有工作节点命令,例如发送邮件和同步邮箱,也有跟踪事件,因此能访问 4222 端口的客户端就能发出两者。在单主机上,只有本机服务需要访问,回环地址就是完整的访问控制。

只有其他机器的工作节点必须访问时才开放,并在同一次变更中添加身份验证和 TLS。配置见其他机器上的工作节点。

从源码构建

获取源码并创建目录结构

sudo useradd --system --home /var/lib/warmbly --create-home --shell /usr/sbin/nologin warmbly
sudo mkdir -p /opt/warmbly/bin /etc/warmbly /var/lib/warmbly/blobs
sudo chown -R warmbly:warmbly /var/lib/warmbly
sudo chmod 0700 /etc/warmbly
sudo install -d -o "$USER" -g "$USER" /opt/warmbly/src
git clone https://github.com/warmbly/warmbly /opt/warmbly/src

检出目录属于你,不属于 root:下方所有构建都以普通用户运行并写入 /opt/warmbly/src,只有各产物最后的 sudo install 或 sudo cp 会触碰 root 所有的 /opt/warmbly/bin、/opt/warmbly/web 等目录。使用 sudo 克隆会得到 root 所有的目录树,构建无法写入。

需要已知版本时,应检出发布标签而非 main:git -C /opt/warmbly/src checkout vX.Y.Z。下方所有操作从 /opt/warmbly/src 开始。

Go 服务

一个模块构建出六个二进制。它们采用静态构建,因此生产主机不需要 Go 运行时:

cd /opt/warmbly/src
export CGO_ENABLED=0
for cmd in backend forms consumer worker migrate warmblyctl; do
  go build -ldflags="-s -w" -o "out/$cmd" "./cmd/$cmd"
done
sudo install -m 0755 out/backend out/forms out/consumer out/worker out/migrate out/warmblyctl /opt/warmbly/bin/
sudo ln -sf /opt/warmbly/bin/warmblyctl /usr/local/bin/warmblyctl

默认构建不支持 Kafka,这正适合此处,因为事件总线是 NATS。只有连接已有 Kafka 集群时,才添加 -tags kafka,并设置 CGO_ENABLED=1、安装 librdkafka。

跟踪服务(Rust)

cd /opt/warmbly/src/tracking
cargo build --release
sudo install -m 0755 target/release/tracking /opt/warmbly/bin/tracking

static/ 中的跟踪代码片段会编译进二进制,无需再复制其他内容。

实时服务(Elixir)

Mix release 会打包 Erlang 运行时,因此构建完成后,主机无需安装 Elixir:

cd /opt/warmbly/src/realtime
mix local.hex --force && mix local.rebar --force
MIX_ENV=prod mix deps.get --only prod
MIX_ENV=prod mix compile
MIX_ENV=prod mix release --overwrite
sudo rm -rf /opt/warmbly/realtime
sudo cp -r _build/prod/rel/realtime /opt/warmbly/realtime
sudo chown -R warmbly:warmbly /opt/warmbly/realtime

构建机器应与目标主机使用相同 libc。Alpine 的 musl 构建无法在 Debian 的 glibc 上启动,反过来也一样。

控制台、管理面板与表单应用

三者都是 Vite 静态构建。构建时不会固化 URL;控制台和管理端在运行时读取 config.js,下一步会配置。表单应用由 forms 二进制自身提供,它会在工作目录下的 forms/dist 查找文件。

cd /opt/warmbly/src/web   && pnpm install --frozen-lockfile && pnpm build
cd /opt/warmbly/src/admin && pnpm install --frozen-lockfile && pnpm build
cd /opt/warmbly/src/forms && pnpm install --frozen-lockfile && pnpm build
sudo rm -rf /opt/warmbly/web /opt/warmbly/admin /opt/warmbly/forms
sudo cp -r /opt/warmbly/src/web/dist   /opt/warmbly/web
sudo cp -r /opt/warmbly/src/admin/dist /opt/warmbly/admin
sudo install -d /opt/warmbly/forms
sudo cp -r /opt/warmbly/src/forms/dist /opt/warmbly/forms/dist

前端运行时配置

容器镜像中的入口脚本会根据环境变量生成此文件。没有容器时,需要为每个前端手动写入:

sudo tee /opt/warmbly/web/config.js >/dev/null <<'EOF'
window.__WARMBLY_ENV__ = {
  API_URL: "https://api.example.com",
  APP_URL: "https://app.example.com",
  TURNSTILE_KEY: ""
};
EOF

sudo tee /opt/warmbly/admin/config.js >/dev/null <<'EOF'
window.__WARMBLY_ENV__ = {
  API_URL: "https://api.example.com",
  DASHBOARD_URL: "https://app.example.com",
  ENV_LABEL: "production",
  TURNSTILE_KEY: ""
};
EOF
sudo chmod -R a+rX /opt/warmbly/web /opt/warmbly/admin

TURNSTILE_KEY 是 Cloudflare Turnstile 站点密钥;除非后端同时设置 CAPTCHA_PROVIDER=turnstile,否则保持为空。每次页面加载都会读取该文件,下方 nginx 以 no-store 提供,因此以后修改 URL 无需重新构建。

配置设置

生成密钥

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)

连接邮箱前先备份最后两个值

KMS_LOCAL_MASTER_KEY 和 CREDENTIALS_ENCRYPTION_KEY 密封所有已存储凭据。丢失后无法恢复;数据库备份没有它们也无法解密。

编写 /etc/warmbly/warmbly.env

一个文件同时供后端、消费者、跟踪和实时服务读取。Compose 原本会在服务之间映射部分名称,例如 AUTH_SECRET 到 JWT_SECRET、PRIMARY_DB 到 DATABASE_URL、REDIS 到 REDIS_URL;这里将两种名称以相同值写入同一文件。

sudo tee /etc/warmbly/warmbly.env >/dev/null <<EOF
# ── Mode ─────────────────────────────────────────────────────────
APP_ENV=prod
DEPLOYMENT_MODE=self_hosted
GIN_MODE=release
AWS_CONFIG_ENABLED=false

# ── Secrets ──────────────────────────────────────────────────────
AUTH_SECRET=${AUTH_SECRET}
JWT_SECRET=${AUTH_SECRET}
INTERNAL_API_TOKEN=${INTERNAL_API_TOKEN}
SECRET_KEY_BASE=${SECRET_KEY_BASE}
KMS_PROVIDER=local
KMS_LOCAL_MASTER_KEY=${KMS_LOCAL_MASTER_KEY}
CREDENTIALS_ENCRYPTION_KEY=${CREDENTIALS_ENCRYPTION_KEY}

# ── Backing services ─────────────────────────────────────────────
PRIMARY_DB=postgres://warmbly:[email protected]:5432/warmbly?sslmode=disable
DATABASE_URL=postgres://warmbly:[email protected]:5432/warmbly?sslmode=disable
REDIS=redis://127.0.0.1:6379
REDIS_URL=redis://127.0.0.1:6379
EVENTBUS_PROVIDER=nats
NATS_URL=nats://127.0.0.1:4222
CODEC_PROVIDER=json
PUBSUB_ENABLED=false
ENCRYPTED_KEYS_PROVIDER=postgres
TASKS_PROVIDER=local
BILLING_PROVIDER=none
CAPTCHA_PROVIDER=none

# ── Storage ──────────────────────────────────────────────────────
BLOB_PROVIDER=filesystem
BLOB_FS_ROOT=/var/lib/warmbly/blobs
BLOB_PUBLIC_BASE_URL=https://api.example.com/public

# ── Where it lives ───────────────────────────────────────────────
API_HOST=127.0.0.1:8080
API_PUBLIC_URL=https://api.example.com
APP_URL=https://app.example.com
CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
WEBSOCKET_URL=wss://ws.example.com/socket/websocket
TRACKING_DOMAIN=t.example.com
TRUSTED_PROXIES=127.0.0.1/32

# ── Tracking service ─────────────────────────────────────────────
TRACKING_HOST=127.0.0.1
TRACKING_PORT=3000
BACKEND_INTERNAL_URL=http://127.0.0.1:8080
TRACKING_TRUSTED_PROXIES=127.0.0.1/32

# ── Realtime service ─────────────────────────────────────────────
PHX_HOST=ws.example.com
PORT=4000
CHECK_ORIGIN=true

# ── GeoIP (the variable is required by the backend; the file is optional) ──
GEODB_PATH=/var/lib/warmbly/GeoLite2-City.mmdb

# ── Platform email (resets, invitations, digests) ────────────────
MAIL_TRANSPORT=smtp
SMTP_HOST=smtp.example.com
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_SECURITY=starttls
EMAIL_NAME=Warmbly
[email protected]

# ── Connecting mailboxes (optional) ──────────────────────────────
BOX_GOOGLE_CLIENT_ID=
BOX_GOOGLE_CLIENT_SECRET=
BOX_OUTLOOK_CLIENT_ID=
BOX_OUTLOOK_CLIENT_SECRET=
EOF
sudo chmod 0600 /etc/warmbly/warmbly.env
sudo chown warmbly:warmbly /etc/warmbly/warmbly.env

有几行与 Compose 默认值有意不同:

  • API_HOST、TRACKING_HOST 和实时服务的 PORT 设在 127.0.0.1,只让 nginx 访问。不过实时服务仍会绑定所有接口,因此防火墙中应关闭 4000。
  • TRUSTED_PROXIES 和 TRACKING_TRUSTED_PROXIES 指定代理,使速率限制和审计记录看到访问者地址,而不是 127.0.0.1。
  • CHECK_ORIGIN=true 会让 WebSocket 拒绝不在 PHX_HOST 来源列表中的浏览器。
  • GEODB_PATH 指向的文件可以不存在。没有这个变量时后端拒绝启动,但数据库文件缺失只会让会话、审计行、邮件打开和点击没有城市信息。如果已有 MaxMind GeoLite2-City.mmdb,放到该路径;也可设置 GEODB_URL,让服务自行获取副本,见 GeoIP。

其他变量及默认值都在配置参考中;deploy/config/env.example 是本文件所精简自的带注释模板。邮件中继、OAuth 客户端、单点登录和 AI 服务商的配置方式,与自托管指南完全相同,只是将值传给进程的方式不同。

编写 /etc/warmbly/worker.env

工作节点有意不直接访问数据库:它通过后端内部 API 获取加密密钥,只持有发送所需内容。为它单独提供文件:

sudo tee /etc/warmbly/worker.env >/dev/null <<EOF
APP_ENV=prod
AWS_CONFIG_ENABLED=false
WORKER_ID=$(uuidgen)
WARMBLY_NODE_REGION=eu-central

EVENTBUS_PROVIDER=nats
NATS_URL=nats://127.0.0.1:4222
CODEC_PROVIDER=json
REDIS=redis://127.0.0.1:6379

ENCRYPTED_KEYS_PROVIDER=http
ENCRYPTED_KEYS_BACKEND_URL=http://127.0.0.1:8080
ENCRYPTED_KEYS_WORKER_TOKEN=${INTERNAL_API_TOKEN}
KMS_PROVIDER=local
KMS_LOCAL_MASTER_KEY=${KMS_LOCAL_MASTER_KEY}
CREDENTIALS_ENCRYPTION_KEY=${CREDENTIALS_ENCRYPTION_KEY}

BLOB_PROVIDER=filesystem
BLOB_FS_ROOT=/var/lib/warmbly/blobs

BOX_GOOGLE_CLIENT_ID=
BOX_GOOGLE_CLIENT_SECRET=
BOX_OUTLOOK_CLIENT_ID=
BOX_OUTLOOK_CLIENT_SECRET=
EOF
sudo chmod 0600 /etc/warmbly/worker.env
sudo chown warmbly:warmbly /etc/warmbly/worker.env

WORKER_ID 只生成一次,之后不再改变:它是工作节点身份,分配给它的邮箱跟随这个 UUID。如果每次启动都生成新 ID,旧节点仍会持有邮箱,发送会停滞,直到协调器发现。BOX_* 值必须与后端一致,因为过期 OAuth 令牌由工作节点刷新。

运行服务

安装 systemd 单元

仓库在 deploy/systemd/ 为每个服务提供一个单元,已指向上述路径,并限制为 warmbly 用户运行:

sudo cp /opt/warmbly/src/deploy/systemd/warmbly-*.service /etc/systemd/system/
sudo systemctl daemon-reload

先启动后端

后端启动时应用迁移,其他服务都假设 Schema 已存在:

sudo systemctl enable --now warmbly-backend
sudo journalctl -u warmbly-backend -f

等待迁移完成,并确认 curl http://127.0.0.1:8080/health 返回 200。prod 启动会拒绝任何公开开发秘密值,因此如果消息指出某个变量,说明环境文件仍含占位符。

如果要只应用迁移而不启动 API,例如在部署脚本中,可执行 sudo -u warmbly env $(grep PRIMARY_DB /etc/warmbly/warmbly.env) /opt/warmbly/bin/migrate,它运行相同的嵌入迁移后退出。

启动其余服务

sudo systemctl enable --now warmbly-forms warmbly-consumer warmbly-tracking warmbly-realtime warmbly-worker
systemctl status 'warmbly-*' --no-pager

各服务在自己的端口响应:

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

消费者和工作节点没有端口;journalctl -u warmbly-worker 可查看节点登记及心跳,能够登录后,也会在管理面板“工作节点”中看到它。

在前端配置 nginx 代理

deploy/nginx/warmbly.conf 提供两个静态前端,并代理 API、WebSocket、跟踪和表单主机。它按路径引用 Let's Encrypt 证书,因此 nginx 接受站点前,证书必须存在。先在 80 端口未被占用时用独立模式签发,再启用站点:

sudo systemctl stop nginx
sudo certbot certonly --standalone --agree-tos -m [email protected] \
  -d app.yourdomain.com -d admin.yourdomain.com -d api.yourdomain.com \
  -d ws.yourdomain.com -d t.yourdomain.com -d forms.yourdomain.com \
  --cert-name app.yourdomain.com

sudo cp /opt/warmbly/src/deploy/nginx/warmbly.conf /etc/nginx/sites-available/warmbly.conf
sudo sed -i 's/example\.com/yourdomain.com/g' /etc/nginx/sites-available/warmbly.conf
sudo ln -s /etc/nginx/sites-available/warmbly.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl start nginx

--cert-name app.yourdomain.com 将六个名称放在同一张证书中,路径为 /etc/letsencrypt/live/app.yourdomain.com/,随附配置中的每个 server 块都使用此路径。续期由 certbot 安装的定时器执行;因为证书以独立模式签发,需要一次性告知它如何重新加载 nginx:

sudo certbot renew --dry-run --pre-hook 'systemctl stop nginx' --post-hook 'systemctl start nginx'

这些钩子保存在续期配置中,每次真正续期都会复用。

自定义跟踪域名和表单域名

实例中的工作区可以把自己的子域指向你的跟踪或表单主机,例如将 t.acme.com CNAME 指向 t.yourdomain.com,然后验证。验证只检查 DNS,因此 nginx 尚不知道该名称也会通过;但此域名下每个跟踪和退订链接都会因 nginx 没有证书而 TLS 失败。

nginx 不提供按需签发,因此需要逐域名处理。将名称加入证书,并为它添加 server 块:跟踪代理到 :3000,表单代理到 :8090:

sudo certbot certonly --standalone --cert-name app.yourdomain.com \
  --expand -d t.acme.com

后端在 GET /tls/authorize?domain=<host> 提供 nginx 自己无法查询的同一判断:实例已验证名称时返回 204,否则返回 403。内置 Caddy 安装正是利用它,无需逐域名步骤即可签发证书;自动化上述过程的脚本也应在申请证书前检查该端点。

如果不想逐客户执行这一步,就不要验证自定义域名:链接会继续使用已有证书的共享跟踪主机,不会出错。

根域名重定向也需要为裸域名及其 www 执行同样步骤,例如 acme.io 和 www.acme.io,代理到 :3000 并设置 proxy_set_header Host $host;。在 nginx 正确转发前,“发送域名”页面会将重定向显示为访问者无法到达,并说明所需改动。已连接 Warmbly Cloud 的实例也可改由 Cloud 提供重定向,这样无需修改 nginx。

前端是 /opt/warmbly/web 和 /opt/warmbly/admin 下的普通文件,历史路由回退到 index.html;index.html 和 config.js 以 Cache-Control: no-store 提供,让下一次加载即可获取重新部署。其他 Web 服务器也能实现相同效果,配置文件列出了精确响应头。

认领实例

首次启动且 users 表为空时,后端会在日志打印一次性设置链接:

sudo journalctl -u warmbly-backend | grep -o 'http[^ ]*/setup?token=[a-f0-9]*' | tail -1

在 https://app.yourdomain.com 打开它,设置密码,即成为所有者和平台管理员。如果日志行已滚走,warmblyctl setup-link 可打印新的链接:

sudo -u warmbly env $(grep -E '^(PRIMARY_DB|REDIS|AUTH_SECRET|APP_URL)=' /etc/warmbly/warmbly.env | xargs) warmblyctl setup-link

warmblyctl 与容器镜像提供的运维 CLI 相同,从环境读取 PRIMARY_DB 等值。因此,在裸主机运行 warmblyctl 参考中的每条命令,都可使用上方 env $(grep ...) 前缀。Shell 别名可以减少输入:

alias warmblyctl='sudo -u warmbly env $(sudo grep -E "^(PRIMARY_DB|REDIS|AUTH_SECRET|APP_URL|API_PUBLIC_URL|MAIL_TRANSPORT|SMTP_HOST|EMAIL_ADDRESS|EMAIL_NAME)=" /etc/warmbly/warmbly.env | xargs) /opt/warmbly/bin/warmblyctl'
warmblyctl status

首次运行说明了数据库已有账户时的处理,以及通过 WARMBLY_BOOTSTRAP_EMAIL 无人值守配置的方法。

在其他主机上运行工作节点

工作节点的作用是把发送分布到不同机器身份,因此多数安装最终会在其他主机添加节点。有两条路径,控制平面都不需要 Docker:

加入脚本,即管理面板“添加机器”流程生成的脚本,会在远程主机上以容器运行节点。仅远程机器需要 Docker 和 systemd。后端在 GET /join.sh 提供脚本,它嵌入二进制,因此无需配置脚本路径。

原生工作节点使用与上方相同的二进制和环境文件,只需更改地址。后端可根据加入令牌生成完整环境文件,因此无需手动复制各项配置:

# On the remote host, with /opt/warmbly/bin/worker built for it:
curl -fsS https://api.yourdomain.com/api/v1/fleet/join \
  -H 'Content-Type: application/json' \
  -d '{"token":"<join-token>","role":"worker","region":"eu-central"}' \
  | sed -n 's/.*"env_b64"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
  | base64 -d | sudo tee /etc/warmbly/worker.env >/dev/null
sudo chmod 0600 /etc/warmbly/worker.env
sudo cp deploy/systemd/warmbly-worker.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now warmbly-worker

生成的文件已包含该节点的 WORKER_ID,因此其分配行和节点行代表同一台机器。使用 warmblyctl fleet join-token 签发令牌;过期前可以用于任意数量机器,默认七天,--ttl 最多 30 天,或在签发新令牌后失效。响应包含解密材料,因此只能通过 HTTPS 操作。登记任何节点前,将后端的 API_PUBLIC_URL、NATS_URL 和 REDIS 设为远程主机可访问的地址,并向它开放对应端口:生成文件继承后端自身值,而其他机器上的 127.0.0.1 并不指向控制平面。

NATS 和 Redis 对另一台机器开放后,就不再受回环绑定保护,因此应先为两者配置凭据和 TLS。对于 NATS,扩展 /etc/nats.conf 并重启:

listen: 0.0.0.0:4222
http: 127.0.0.1:8222
jetstream { store_dir: /var/lib/nats }
authorization { token: "<openssl rand -hex 32>" }
tls {
  cert_file: "/etc/letsencrypt/live/app.yourdomain.com/fullchain.pem"
  key_file:  "/etc/letsencrypt/live/app.yourdomain.com/privkey.pem"
}

然后设置 NATS_URL=tls://<token>@nats.yourdomain.com:4222,写入 warmbly.env 和 worker.env,URL 中的令牌供各服务进行身份验证。Redis 配置 requirepass 和 REDIS=redis://:<password>@redis.yourdomain.com:6379,最好通过私有网络或隧道连接。防火墙也应将两个端口限制为工作节点地址。使用 BLOB_PROVIDER=s3 和双方都可访问的存储桶:工作节点需要读取后端写入的正文,因此文件系统提供程序仅在双方共享存储且权限匹配时可用。自托管指南有更详细的相同注意事项。

因为工作节点不在容器中,加入脚本的 systemd 服务和更新定时器不适用于它。应使用 systemctl 和下方更新步骤管理,手动设置版本,而不是通过 warmblyctl fleet version。

升级

scripts/upgrade-bare-metal.sh 按顺序执行上述构建步骤:以检出目录所有者身份构建本机运行的全部产物,再通过 sudo 调用 root 所有的安装器 warmbly-install-release。安装器安装产物,先重启后端,让只能向前应用的迁移在启动时执行,再重启其他服务,并在复制前端时保留各自的 config.js。先将这一个脚本安装为 root 所有,再以检出目录所有者身份运行升级:

sudo install -o root -g root -m 0755 /opt/warmbly/src/deploy/systemd/warmbly-install-release.sh /usr/local/sbin/warmbly-install-release
echo "$USER ALL=(root) NOPASSWD: /usr/local/sbin/warmbly-install-release" | sudo tee /etc/sudoers.d/warmbly-upgrade >/dev/null
sudo chmod 0440 /etc/sudoers.d/warmbly-upgrade
cd /opt/warmbly/src && scripts/upgrade-bare-metal.sh --pull    # git pull --ff-only, build, install, restart

sudoers 配置让脚本无需密码提示即可调用安装器;它只允许这一个 root 所有的命令,不允许其他命令。

要固定发布版本而不是跟随分支,先检出标签 git checkout vX.Y.Z,再不带 --pull 运行。

管理面板也可以代你运行相同脚本。侧边栏底部版本标记会显示新发布;安装更新器单元后,其中的更新并重启会拉取、运行脚本并报告结果。更新提供所需服务单元、sudoers 配置,以及后端指向更新器的变量。

备份

以下三项缺一不可:

sudo -u postgres pg_dump warmbly > backup.sql       # 1. the database
sudo tar czf blobs.tgz -C /var/lib/warmbly blobs      # 2. uploads and stored message bodies
sudo cp /etc/warmbly/*.env somewhere-safe/            # 3. the env files, above all the two encryption keys

在后端启动前恢复到空数据库,再启动后端,它只会应用比备份更新的迁移。参见恢复,将 docker compose exec 命令替换为普通 psql。

故障排查

现象原因
后端启动退出,并指出 AUTH_SECRET 或其他变量APP_ENV=prod 拒绝公开开发默认值,环境文件仍有占位符
第一次发布报流或 JetStream 错误/etc/nats.conf 没有 jetstream 块
go build 或 cargo build 因权限不足失败检出目录使用 sudo 克隆,归 root 所有;用 chown -R "$USER" 调整,再以自己的用户构建
nginx -t 因证书文件缺失失败在运行 certbot certonly 前就启用了站点,或 --cert-name 与文件路径不匹配
首次发送报 mkdir /var/lib/warmbly/blobs/...: permission deniedBLOB_FS_ROOT 不可由 warmbly 用户写入,或服务单元的 ReadWritePaths 未包含它
实时服务单元启动后立即退出发布构建只在 PHX_SERVER=true 时监听;随附单元已设置。其他情况可通过 journalctl -u warmbly-realtime 查看缺失变量
实时服务报 JWT_SECRET ... required生产发布读取 JWT_SECRET、SECRET_KEY_BASE 和 DATABASE_URL,不使用后端变量名;应在文件中保留两套名称
控制台加载,但每个请求都是 403CORS_ALLOW_ORIGINS 未包含浏览器所在来源;必须匹配 APP_URL,即写入 config.js 的值
控制台始终没有实时更新,通知无响应WEBSOCKET_URL 错误,或启用 CHECK_ORIGIN=true 时,PHX_HOST 不是控制台主机
工作节点日志报 encryptedkeys.http: ... unexpected status 401ENCRYPTED_KEYS_WORKER_TOKEN 与后端 INTERNAL_API_TOKEN 不一致
发送报 email account not found in worker节点使用新的 WORKER_ID 启动;在协调器迁移前,邮箱仍属于旧节点。应固定 ID
打开和点击未记录TRACKING_DOMAIN 与 nginx 代理到 :3000 的主机不一致,或跟踪服务无法访问 BACKEND_INTERNAL_URL
邮箱连接后约一小时失败工作节点环境缺少 BOX_* OAuth 客户端;只有工作节点负责刷新令牌

warmblyctl status 执行与管理面板实例 > 设置与健康状态 > 服务相同的实例检查,只要存在错误级别问题,就以非零状态退出。故障排查页面覆盖不局限于无 Docker 运行的其他问题。

快速参考

/opt/warmbly/bin/{backend,forms,consumer,worker,tracking,migrate,warmblyctl}
/opt/warmbly/realtime/bin/realtime        mix release
/opt/warmbly/web, /opt/warmbly/admin      static builds + config.js
/opt/warmbly/src                          the checkout (worker installer is served from here)
/etc/warmbly/warmbly.env                  backend, forms, consumer, tracking, realtime
/etc/warmbly/worker.env                   worker
/var/lib/warmbly/blobs                    BLOB_FS_ROOT
/etc/systemd/system/warmbly-*.service     from deploy/systemd/
/etc/nginx/sites-available/warmbly.conf   from deploy/nginx/
systemctl status 'warmbly-*'
journalctl -u warmbly-backend -f
warmblyctl status                # with the alias above
warmblyctl setup-link

另见:使用 Docker 自托管、配置参考、warmblyctl、架构。

本页目录