Stalwart 日常运维手册

Stalwart v0.16.26 日常运维实操手册:日志体系、邮件队列管理、DKIM 轮换与 DNS 维护、备份恢复、版本升级演练与排障实录,全部命令实测通过

StalwartStalwart邮件服务器2026/10/18

Stalwart 日常运维手册

目标:面向已完成部署的 Stalwart v0.16.26 管理员,提供一套可落地的日常运维操作手册——日志体系与查看方法、邮件队列管理、DKIM 密钥与 DNS 维护、数据备份与恢复、版本升级演练与日常巡检清单。所有操作与回显均在 Ubuntu 22.04 + Stalwart v0.16.26 实测通过,遇到故障可直接对照第五章的排障实录处理。

一、环境信息

  • 操作系统:Ubuntu 22.04,Stalwart v0.16.26,systemd 托管(服务名 stalwart)
  • 目录布局:二进制 /usr/local/bin/stalwart、配置 /etc/stalwart/、数据 /var/lib/stalwart/、日志 /var/log/stalwart/
  • 管理面:WebAdmin http://<服务器>:8080,JMAP 管理 API http://127.0.0.1:8080/jmap,CLI stalwart-cli(1.0.13)
  • 管理员凭据:admin@lab.example.com / q1w2e3r4(CLI 通过环境变量注入,见 3.1)
  • 测试域 lab.example.com、测试账号 demo@lab.example.com(密码同)

二、日志体系与日常查看

2.1 日志的双通道布局

v0.16 的日志走两条通道,各有用途:

ls -la /var/log/stalwart/

实测输出:

total 124
drwxr-x--- 2 stalwart stalwart   4096 Oct 18 08:57 .
drwxr-xr-x 1 root     root       4096 Oct 18 08:49 ..
-rw-r--r-- 1 stalwart stalwart 109236 Oct 18 09:35 stalwart.2026-10-18
  • 文件通道:/var/log/stalwart/stalwart.<日期> 按天一个文件,结构化键值格式,是排障主力;
  • journald 通道:进程 stdout 经 SyslogIdentifier=stalwart 进 journald,journalctl -u stalwart 查看,适合跟踪启动与服务级事件。

日志行的统一格式为 <ISO8601时间> <级别> <事件标识> <键值对...>,实测样例(队列投递事件):

2026-10-18T09:33:16Z INFO Delivery attempt ended (delivery.attempt-end) queueId = 335619801751750656, queueName = "remote", from = "demo@lab.example.com", to = ["test@gmail.com"], size = 221, total = 1, elapsed = 3756ms

解读要点:括号内的事件标识(如 delivery.attempt-end)是 grep 的最佳锚点;queueId 可与队列管理(三章)联动追踪单笔邮件。

2.2 高频查看命令速查

# 实时跟踪日志(文件通道)
tail -f /var/log/stalwart/stalwart.$(date +%F)

# 按事件标识过滤(投递、认证、DKIM)
grep "delivery" /var/log/stalwart/stalwart.2026-10-18 | tail -20
grep -E "auth|535" /var/log/stalwart/stalwart.2026-10-18 | tail -10

# 启动/服务级事件(journald 通道)
journalctl -u stalwart --since today --no-pager | head -30

第三条实测输出(启动段节选,可看到监听器逐个起启):

localhost systemd[1]: Started Stalwart.
localhost stalwart[3017]: Started listener smtp ([::]:25)
localhost stalwart[3017]: Started listener submissions ([::]:465)
localhost stalwart[3017]: Started listener submission ([::]:587)
localhost stalwart[3017]: Started listener imaps ([::]:993)
...

2.3 邮件身份校验日志解读

每封入站邮件的身份校验结果都会留痕,排"为什么被判垃圾/被拒收"时先看这三类事件:

grep -E "spf|dkim" /var/log/stalwart/stalwart.2026-10-18 | head -5

实测输出(测试环境无公网 DNS,故 SPF/DKIM 校验为 none/fail 属预期):

2026-10-18T09:03:53Z INFO SPF EHLO check failed (smtp.spf-ehlo-fail) listenerId = "smtp", localPort = 25, remoteIp = 127.0.0.1, ..., domain = "mail.lab.example.com", result = No SPF record (spf.none), elapsed = 472ms
2026-10-18T09:03:54Z INFO SPF From check failed (smtp.spf-from-fail) ... result = No SPF record (spf.none), ...
2026-10-18T09:03:54Z INFO DKIM verification failed (smtp.dkim-fail) listenerId = "smtp", localPort = 25, remoteIp = 127.0.0.1, strict = false, result = [], elapsed = 0ms

解读:spf.none 表示查询域没有发布 SPF 记录(不是校验出错);smtp.dkim-fail 中 strict = false 表示非严格模式、result = [] 表示信上无可验签名。公网生产环境 DNS 配好后,正常邮件这里会是 spf.pass / dkim.pass 类结果。

2.4 WebAdmin 的日志与遥测

WebAdmin 左侧 Logs/Telemetry 面板对同一份遥测数据做了可视化(按时间过滤、按事件类型筛选、查看单封邮件完整轨迹),日常巡检可直接用网页替代命令行;数据同源,结论一致。

三、邮件队列管理

3.1 CLI 连接准备

所有队列操作用官方 stalwart-cli(1.0.13 实测),先注入连接环境变量:

export STALWART_URL=http://127.0.0.1:8080 \
       STALWART_USER=admin@lab.example.com \
       STALWART_PASSWORD=q1w2e3r4

(可写入 /root/.bashrc 或独立的运维脚本文件;密码文件注意 chmod 600。)

3.2 查看队列与消息详情

stalwart-cli query QueuedMessage

实测输出(一封发往外部域、正在重试的邮件):

Id            Sender                Next Retry            Received              Size
jkc2qxlsadqa  demo@lab.example.com  2026-10-18T09:34:00Z  2026-10-18T09:34:00Z  236
stalwart-cli get QueuedMessage jkc2qxlsadqa

实测输出(完整结构):

Sender
  Sender:     demo@lab.example.com
  Recipients:
    Key                  Next Retry            Retry Count  Next Notification     Notify Count  Queue   Flags   Original Recipient
    retrytest@gmail.com  2026-10-18T09:34:00Z  0            2026-10-19T09:34:00Z  0             remote  <none>  <none>
  Size:       236

Delivery
  Next Retry:        2026-10-18T09:34:00Z
  Next Notification: 2026-10-19T09:34:00Z

Details
  Priority:    0
  Envelope ID: <none>
  Flags:       Message was received from an authenticated session

Origin
  Received From IP:  127.0.0.1

字段解读:Retry Count 已重试次数;Next Notification 下次向发件人发延迟告警(DSN)的时间;Queue = remote 远程投递队列(本地投递为 local);Priority 数值越大越优先投递。

3.3 队列消息的改期/优先级/清除

实测三个管理动作(消息 ID 同上):

# 延后重试时间(人工窗口避让)
stalwart-cli update QueuedMessage jkc2qxlsadqa --json '{"nextRetry":"2026-10-18T11:30:00Z"}'

实测输出:

Updated QueuedMessage jkc2qxlsadqa
# 提升优先级
stalwart-cli update QueuedMessage jkc2qxlsadqa --json '{"priority":1}'

实测输出:

Updated QueuedMessage jkc2qxlsadqa
# 清除(放弃投递)
stalwart-cli delete QueuedMessage --ids jkc2qxlsadqa

实测输出:

jkc2qxlsadqa deleted
1 deleted, 0 failed

清除后 query QueuedMessage 确认队列已空(实测无输出即空队列)。批量清除用逗号拼接:--ids id1,id2,id3。

3.4 队列事件与 DSN 机制

队列的自动行为在日志里全量留痕,两个最典型场景(实测日志):

场景一,收件域不存在(DNS 无 MX),立即永久失败并自动生成退信(DSN):

2026-10-18T09:32:41Z INFO MX record lookup failed (delivery.mx-lookup-failed) queueId = ..., domain = "slowmx.test", details = "No MX records were found, attempting implicit MX.", ...
2026-10-18T09:32:41Z INFO DSN permanent failure notification (delivery.dsn-perm-fail) ... to = ["test@slowmx.test"], details = "DNS lookup failed: no MX record found.", ...
2026-10-18T09:32:41Z INFO Queued DSN for delivery (queue.dsn-queued) ... from = "<>", to = ["demo@lab.example.com"], ...

场景二,对端返回 5xx 拒收(收件人不存在),同样永久失败 + DSN:

2026-10-18T09:33:16Z INFO DSN permanent failure notification (delivery.dsn-perm-fail) ... details = "Unexpected response for RCPT TO:<test@gmail.com>: Code: 550, ... The email account that you tried to reach does not exist. ..."

运维要点:永久失败(5xx/DNS 无记录)不占队列、自动退信;临时失败(超时/4xx)留在队列按退避策略自动重试,只有"确认不再投"或"等不及人工放行"时才需要 3.3 节的人工干预。

四、DKIM 密钥与 DNS 维护

4.1 现状确认

v0.16 在域名上启用 Automatic DKIM 管理后,签名密钥全自动轮换(《Stalwart v0.16.26 配置参数详解》 4.4/4.5 节有对象级解读):实测域名 lab.example.com 的轮换参数为 90 天轮换(rotateAfter)、7 天退役(retireAfter)、30 天后删除(deleteAfter),选择器模板 v{version}-{algorithm}-{date-%Y%m%d},当前活跃签名 v1-rsa-20261018 与 v1-ed25519-20261018(stage = active)。密钥生成时日志有留痕:

2026-10-18T08:57:49Z INFO DKIM signature created (dkim.signature-created) id = "v1-ed25519-20261018", details = "lab.example.com"
2026-10-18T08:57:49Z INFO DKIM signature created (dkim.signature-created) id = "v1-rsa-20261018", details = "lab.example.com"

4.2 DNS 记录维护流程

轮换产生新选择器后,必须把新公钥的 TXT 记录发布到 DNS,否则新签名无法被对端验证。日常维护节奏:

  1. 加新域后:从域名对象导出 dnsZoneFile(MX/SPF/DKIM/DMARC/MTA-STS 全套),整体发布到 DNS(导出命令与完整实测输出见 《Ubuntu 22.04 安装 Stalwart v0.16.26 指南》 4.4 节);
  2. 日常巡检:对比当前 stage = active 签名的 selector 与 DNS 已发布记录是否一致(dig TXT v1-rsa-20261018._domainkey.lab.example.com);
  3. 轮换窗口:新签名创建后先发布 DNS,再等旧选择器过了 retireAfter 自然退役——顺序不能反,否则对端验签失败。

注意 DNS 记录的生效依赖域名 DNS 配置权限,本手册不伪造验证结果;测试/内网环境可按 2.3 节日志判断校验行为。

五、数据备份与恢复

5.1 备份什么

Stalwart 的全部状态(账号、配置对象、邮件、队列、遥测)都在数据存储里,因此备份的核心是数据存储;再辅以两份配置文件即可完整复原一个实例:

备份对象路径方式
数据存储全量RocksDB /var/lib/stalwart/stalwart --export(官方逻辑导出,推荐)
引导配置/etc/stalwart/config.json文件复制
环境变量/etc/stalwart/stalwart.env文件复制

5.2 导出实测(--export)

导出需停服保证一致性(小流量窗口内操作):

systemctl stop stalwart
mkdir -p /var/backups/stalwart
/usr/local/bin/stalwart --config=/etc/stalwart/config.json --export /var/backups/stalwart/snapshot-20261018

实测输出(逐个子空间导出):

Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_p.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_s.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_b.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_g.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_d.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_o.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_l.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_e.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_x.
Exporting database to /var/backups/stalwart/snapshot-20261018/subspace_q.

产物与体量:

ls -la /var/backups/stalwart/snapshot-20261018/ | head -6
du -sh /var/backups/stalwart/snapshot-20261018

实测输出:

-rw-r--r-- 1 root root   21 Oct 18 09:34 subspace_a
-rw-r--r-- 1 root root 26723 Oct 18 09:34 subspace_b
-rw-r--r-- 1 root root  1747 Oct 18 09:34 subspace_d
...
808K    /var/backups/stalwart/snapshot-20261018

(subspace_* 按存储内名字空间拆分:目录、配置、邮件、Blob、队列等各有归属;测试环境体量小,生产环境随邮件量增长。)完成后恢复服务:

systemctl start stalwart && sleep 8 && systemctl is-active stalwart

实测输出:

active

5.3 恢复(--import)与日常备份建议

恢复是导出的逆过程:在空数据目录的目标实例上执行 stalwart --config=/etc/stalwart/config.json --import /path/to/snapshot-xxx(帮助信息实测:-i, --import <PATH> Import store data from a specific path),导入完成再启动服务。

日常备份建议(结合 5.1 的三件套):

  • 每周全量 --export + 每日 /var/lib/stalwart 文件级快照(停服窗口或接受 RocksDB 快照的轻微不一致);
  • 备份产物异地存放(对象存储/另一台机器),本地保留 2~4 份轮换;
  • 每季度做一次恢复演练:在隔离环境用备份 + 两份配置文件拉起实例,验证能登录 WebAdmin、能查收历史邮件。没演练过的备份等于没备份。

六、版本升级

v0.16 的升级就是"换二进制 + 重启",数据存储格式由服务自行兼容迁移。以下流程在实测环境完整演练(重装同版本,生产换新版本号即可):

# 1) 备份(升级前必做,见五章)
systemctl stop stalwart
/usr/local/bin/stalwart --config=/etc/stalwart/config.json --export /var/backups/stalwart/pre-upgrade-$(date +%F)

# 2) 下载新版本并校验(以 v0.16.26 为例)
cd /tmp
curl -fsSL -O "https://github.com/stalwartlabs/stalwart/releases/download/v0.16.26/stalwart-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL -O "https://github.com/stalwartlabs/stalwart/releases/download/v0.16.26/stalwart-x86_64-unknown-linux-gnu.tar.gz.sha256"
sha256sum -c stalwart-x86_64-unknown-linux-gnu.tar.gz.sha256

实测输出:

stalwart-x86_64-unknown-linux-gnu.tar.gz: OK
# 3) 替换二进制并启动
tar zxf stalwart-x86_64-unknown-linux-gnu.tar.gz
install -m 0755 stalwart /usr/local/bin/stalwart
systemctl start stalwart && sleep 8

# 4) 验证
systemctl is-active stalwart && stalwart --version

实测输出:

active
0.16.26

再补两个冒烟检查:ss -ltn 确认监听端口齐、WebAdmin 能登录。回滚预案:保留上一版本二进制副本(cp /usr/local/bin/stalwart /usr/local/bin/stalwart.v0.16.25.bak),新版本异常时停服换回旧二进制启动即可;跨大版本升级(如 v0.13 → v0.16)涉及配置模型迁移,必须先读官方 UPGRADING 文档并在隔离环境演练。

七、日常巡检清单

给一条可 cron 化的巡检命令序列(每项都有"健康标准"):

# 1. 服务活性:期望 active
systemctl is-active stalwart

# 2. 端口齐备:期望列出 25/465/587/143/993/995/4190/443/8080
ss -ltn | grep -E ':(25|465|587|143|993|995|4190|443|8080)\b'

# 3. 队列积压:正常应接近空(远端重试除外)
export STALWART_URL=http://127.0.0.1:8080 STALWART_USER=admin@lab.example.com STALWART_PASSWORD=q1w2e3r4
stalwart-cli query QueuedMessage

# 4. 错误日志扫描:期望无 ERROR/WARN 突增
grep -cE " ERROR | WARN " /var/log/stalwart/stalwart.$(date +%F)

# 5. 磁盘水位:数据目录与日志目录
df -h /var/lib/stalwart /var/log/stalwart | tail -2

# 6. 冒烟收发(内网闭环)
swaks --to demo@lab.example.com --from test@lab.example.com \
  --server 127.0.0.1:25 --helo mail.lab.example.com \
  --header "Subject: daily probe $(date +%T)" --body "probe" 2>&1 | grep -c "250"

任一项不满足健康标准,按第五章开始定位。

八、常见问题(排障实录)

以下问题均在本次实测中真实出现,按"现象 → 定位 → 处理"整理。

8.1 端口被占用,服务起不来或监听缺失

现象:ss -ltn 缺端口,或启动日志报 Address already in use。

定位与处理:

ss -ltnp | grep -E ':(25|587|143|8080)\b'

输出会显示占用进程(常见:遗留 postfix/exim4、其他服务占用)。停用冲突服务(systemctl disable --now postfix 等);脚本安装的 unit 自带 Conflicts=postfix.service sendmail.service exim4.service 会自动处理 systemd 服务,其他平台级端口映射需自行调整。

8.2 认证失败(535)

现象:Submission 587 认证被拒。实测回显(错误密码场景):

<~* 535 5.7.8 Authentication credentials invalid.
*** No authentication type succeeded

定位顺序:① 凭据本身输错/密码已轮换(用管理 API 核对账号凭据再试);② 未 STARTTLS(587 明文直连服务器不提供 AUTH 机制,swaks 加 -tls);③ 账号被停用。日志侧 grep "auth" /var/log/stalwart/stalwart.$(date +%F) 可看到每次失败尝试。

8.3 DKIM/SPF 校验失败

现象:日志出现 smtp.spf-ehlo-fail / smtp.spf-from-fail(result = No SPF record (spf.none))或 smtp.dkim-fail(实测样例见 2.3 节)。

定位思路:这类事件多数是 DNS 配置问题而非程序故障——发件域没发布 SPF、DKIM 选择器记录缺失/未同步(对照四章 4.2 的轮换流程)、或查询域根本无 MX。内网/测试环境没有公网 DNS 时 spf.none/dkim-fail 属预期,不影响本地投递;公网生产按 DNS 侧逐项补齐即可。

8.4 外域投递失败与退信(DNS/对端拒收)

现象:发往外域的邮件消失或收到退信。实测两类根因日志(见 3.4 节):

  • delivery.mx-lookup-failed + delivery.dsn-perm-fail:收件域 DNS 无 MX,永久失败,系统自动向发件人投递退信(DSN);
  • 对端 550 拒收(收件人不存在)同样走 DSN。

处理:核对收件地址拼写与目标域 DNS;确认本机可解析公网(getent hosts gmail-smtp-in.l.google.com)。注意 v0.16 使用自身 DNS 解析逻辑,/etc/hosts 的域名映射对出站投递不生效(实测黑洞域名走 hosts 未被命中,仍按 DNS 查询)。

8.5 RocksDB 锁冲突(LOCK 文件)

现象:启动报错(实测回显):

⚠️ Startup failed: Failed to open database: Error { message: "IO error: While lock file: /var/lib/stalwart//LOCK: Resource temporarily unavailable" }

原因:已有 stalwart 进程持有数据目录锁(多进程误启、旧进程未退干净)。处理:ps aux | grep [s]talwart 找到全部进程,只保留一个实例(杀干净旧进程或重启服务器)后再启动。日常避免手工重复拉起进程,统一走 systemd。

8.6 WebAdmin 资源加载失败

现象:/admin 页面空白,日志出现 Failed to fetch webui bundle 类告警。

原因:WebAdmin 前端资源包需从外部拉取,无外网/直连 GitHub 受限时失败。处理:在 /etc/stalwart/stalwart.env 配置 https_proxy/http_proxy 后重启服务(实测配置代理后资源下载成功、页面正常)。

8.7 升级后数据不一致/服务异常

现象:换二进制后起不来或行为异常。

处理:先看 journalctl -u stalwart -n 50 定位报错;多数情况用五章备份回退——停服、清空数据目录、--import 升级前快照、换回旧版二进制、启动。跨大版本必须先在隔离环境演练迁移。

九、参考链接