在 NixOS 上部署 Stalwart 邮件服务器:一段比想象中曲折,但最后还是跑通了的旅程
自建邮件服务器这件事,总有点复古工程学的味道。
它不像搭一个 Web 服务那样一把梭。更像修一座旧邮局。门牌要挂对,地图要画对,来往信差要认得路,门锁还得换成合适的。DNS、证书、SMTP、IMAP、反向解析、发信信誉,哪一块没接上,最后都可能表现成一句很朴素但很让人抓狂的话:"为什么还是不行?"
最近我在 NixOS 上折腾了一套 Stalwart Mail Server。它是一款比较新的全栈邮件服务器,目标很直接,就是把 SMTP、IMAP、JMAP、垃圾邮件过滤这些东西尽量装进一个系统里,而不是让人继续把 Postfix、Dovecot、Rspamd、数据库和一堆边角料东拼西凑。官方文档的介绍也差不多是这个意思。详见 Stalwart 官方文档:https://stalw.art/docs/。
从理念上说,这对 NixOS 很有吸引力。组件少一点,声明式配置就清爽一点。NixOS Wiki 也已经有了 Stalwart 页面,说明它至少不是什么完全野路子的尝试。见 NixOS Wiki 的 Stalwart 条目:https://wiki.nixos.org/wiki/Stalwart。
理论很美。实践嘛,还是那句老话,邮件系统从来不是一个会轻易让人优雅收工的东西。
为什么选 Stalwart
我最开始注意到 Stalwart,其实原因很简单:它是 Rust 写的。
邮件服务器这种长期运行、并且直接暴露在公网的服务,用 Rust 实现总让人安心一些。没有 GC,也很少见到那种服务跑着跑着内存慢慢涨上去的情况。
另外一个让我印象挺深的点是资源占用。
Stalwart 的设计目标之一就是保持较低的资源消耗。官方 README 里也提到这一点。在我自己的服务器上实际运行时,常驻内存大概在 120MB 到 150MB 左右。
对一个同时提供 SMTP、IMAP、JMAP、Web 管理界面和垃圾邮件过滤能力的邮件服务器来说,这个体量其实相当克制。
项目主页在这里: https://github.com/stalwartlabs/stalwart
当然,Stalwart 并不是把邮件服务器变“简单”了,而是把传统那一整套 Postfix + Dovecot + Rspamd 的组合整合进了一个系统里。
对 NixOS 用户来说,这反而更舒服一些,因为组件越少,声明式配置就越容易保持清晰。
第一件事:先把域名和角色想明白
我一开始有个很典型的混淆,就是把 mail.initsnow.top 当成了邮箱域名本身。后来才真正理顺:
mail.initsnow.top 更适合做邮件服务器主机名。
真正的邮箱域名,一般是 initsnow.top。
也就是说,比较正常的结构是这样:
- 邮件服务器主机名是
mail.initsnow.top - 邮箱地址是
[email protected] - MX 记now.toptsnow.topl.in而不是录把
ini
这一点不只是命名洁癖,而是后面 DNS、证书、客户端配置全都要围着它转。
所以最基础的 DNS 至少应该先有:
initsnow.top MX 10 mail.initsnow.top.
mail.initsnow.top A <你的服务器IP>
如果你还要给 Web 管理界面单独用一个主机名,比如 webadmin.initsnow.top,那也要对应加上 A 或 AAAA 记录。
NixOS 模块比想象中“薄”
Stalwart 的 NixOS 模块看起来挺完整,但真正看源码就会发现,它并不会理解 Stalwart 的业务配置。它只是把你写在 services.stalwart-mail.settings 里的 Nix attrset 转成 TOML,然后交给 stalwart-mail --config=... 去跑。
源码里最核心的一句其实就是:
configFile = configFormat.generate "stalwart-mail.toml" cfg.settings;
这句话的意思很朴素:NixOS 只负责生成配置文件,不负责替你校验你写的 Stalwart 键名是不是对的,也不负责替你展开 Stalwart 自己的配置宏。
这件事后来让我踩了两个坑。一个是证书配置,一个是我一开始总以为某些键不存在,因为模块源码里看不见。后来才明白,看不到不代表不能配,关键还是要回 Stalwart 官方文档确认语法。
TLS 证书那一段,确实有点绕
如果只是配一个普通 Web 服务,TLS 这件事在 NixOS 里已经算是很顺手了。但 Stalwart 自己也有一套 TLS 和 ACME 逻辑,所以就会出现一种很微妙的交错感:到底是交给 Nginx,还是交给 Stalwart,还是两边各管一部分?
我的最终结论是:别把所有流量都试图塞进一套证书处理流程里,那样很容易把自己绕晕。
比较清楚的分工是这样的:
WebUI 走 Nginx 反向代理,本地监听 HTTP。
SMTP 和 IMAP 由 Stalwart 自己对外提供 TLS。
这样一来,Web 管理界面的 HTTPS 和邮件协议的 TLS 就分开了。你不用逼 Nginx 去接管邮件协议,也不用把 Stalwart 的 Web 入口直接裸露在公网。
我最开始试过手动配置证书,甚至纠结过 %{file:/path/to/file}% 这种宏到底有没有在 Nix 阶段展开。后来彻底想通了:它不会。Nix 只是把字符串原样写进去,真正解释 %{file:...}% 的是 Stalwart 运行时。Stalwart 的宏文档可以参考:https://stalw.art/docs/configuration/macros/。
这件事本身没错,但会让人误以为“值没读进去”。实际上,WebUI 显示的是配置值本身,不是运行时展开后的结果。
说实话,我后来懒得继续在这个问题上打转,干脆改成了让 Stalwart 自己用 ACME 申请证书。
最后跑通的思路:Stalwart 自己用 ACME,Cloudflare 做 DNS-01
既然我的域名本来就在 Cloudflare,最自然的做法其实就是让 Stalwart 直接使用 dns-01 挑战。
这件事官方文档已经写得比较清楚了,Stalwart 支持配置自己的 ACME provider,Cloudflare 就是其中之一。见官方 ACME 文档:https://stalw.art/docs/server/tls/acme/configuration/。
而且 NixOS Wiki 上也给了一个很接近的示例。这种时候,参考现成经验比自己硬猜配置键靠谱得多。
最后跑通的 Nix 配置大概像这样:
{
config,
pkgs,
lib,
...
}:
let
domain = "yourdomain.here";
mxHost = "mail.${domain}";
in
{
sops.secrets."stalwart/admin-password" = {
owner = "stalwart-mail";
group = "stalwart-mail";
mode = "0400";
};
sops.secrets."stalwart/acme-secret" = {
owner = "stalwart-mail";
group = "stalwart-mail";
mode = "0400";
};
services.stalwart-mail = {
enable = true;
openFirewall = true;
settings = {
server = {
hostname = mxHost;
listener = {
smtp = {
protocol = "smtp";
bind = [
"0.0.0.0:25"
"[::]:25"
];
};
submissions = {
protocol = "smtp";
bind = [
"0.0.0.0:465"
"[::]:465"
];
tls.implicit = true;
};
submission = {
protocol = "smtp";
bind = [
"0.0.0.0:587"
"[::]:587"
];
};
imaps = {
protocol = "imap";
bind = [
"0.0.0.0:993"
"[::]:993"
];
tls.implicit = true;
};
http = {
protocol = "http";
bind = [ "127.0.0.1:8080" ];
};
};
};
lookup.default = {
hostname = mxHost;
domain = domain;
};
acme."letsencrypt" = {
directory = "https://acme-v02.api.letsencrypt.org/directory";
challenge = "dns-01";
provider = "cloudflare";
contact = "[email protected]";
domains = [ domain mxHost ];
secret = "%{file:${config.sops.secrets."stalwart/acme-secret".path}}%";
};
authentication.fallback-admin = {
user = "admin";
secret = "%{file:${config.sops.secrets.ath}}%"rt/admin- 而不是"stalwa};
};
};
}
这份配置的好处,是结构非常清楚。
管理员密码和 Cloudflare token 都交给 sops。
WebUI 只监听在 127.0.0.1:8080。
SMTP、SMTPS、Submission、IMAPS 则由 Stalwart 自己对外监听。
邮件协议用 Stalwart 自己申请的证书,WebUI 则继续可以放在 Nginx 后面。
WebUI 不只是管理界面,它还会帮你生成 DNS
这一点我很想单独提一下,因为它真的很实用。
我最开始以为 DNS 记录得全靠自己手写。后来才发现,Stalwart WebUI 其实已经把这件事做得很顺手了。
在 /manage/directory/domains 路径下,点开你创建的域名,然后选择 View DNS records,它会直接给出一组推荐 DNS 记录。这里面包括 MX、SPF、DKIM、DMARC、SRV 等项目。
更重要的是,这些记录可以保存成文件,然后 Cloudflare 可以直接导入。
这比手动一条条复制粘贴省心得多,也更不容易把长长的 DKIM 值抄错。对于第一次搭邮件服务器的人来说,这个细节简直像有人在迷宫墙上悄悄画了出口箭头。
所以如果要写成教程,我会建议把这一步明确写进去:
先在 WebUI 里创建域名,再到 /manage/directory/domains 里点 View DNS records,导出后再导入 Cloudflare。
这样 DNS 配置会轻松很多。
Dashboard 里那个 “Not found”,不用太慌
刚把系统拉起来的时候,我还被 WebUI Dashboard 上的 Not found 吓了一下。
后来发现,这通常不代表服务没起来。更像是某些统计数据或后台对象还不存在,或者系统里还没有实际业务数据。只要管理登录正常、域名能创建、账户能创建、SMTP 和 IMAP 能连上,那个页面一开始空着其实不是什么大问题。
邮件服务器不是开机就有故事的系统。它得先收几封信,发几封信,建立一些状态,仪表盘才会慢慢像仪表盘,而不是一块还没接线的黑板。
Thunderbird 连接成功那一刻,才算真的有点踏实
光是 WebUI 能登录,其实还不算完。
真正让我确认这台机器已经从“配置对象”变成“邮件服务器”的,是 Thunderbird 成功连接上它的那一刻。
客户端这边,IMAP 和 SMTP 我最后用的是最普通也最靠谱的配置:
IMAP 用 mail.initsnow.top:993,连接安全选择 SSL/TLS。
SMTP 用 mail.initsnow.top:465,连接安全同样选择 SSL/TLS。
用户名要写用户名前半段,也就是 user,而不是写完整邮箱地址。
如果 465 不想用,也可以开 587,让客户端走 STARTTLS。兼容性会更好一点。
真正难的部分,其实是“邮件信誉”
当 SMTP、IMAP 和 TLS 都配置完成之后,人很容易有一种错觉:系统已经部署好了。
实际上,这还只是前半场。
邮件服务器真正的门槛,不在于它能不能发信,而在于别人愿不愿意收你的信。
这时候就会进入一个有点隐形、但极其重要的领域:DNS 信誉系统。
最基本的几项是:
PTR 反向解析。
SPF。
DKIM。
DMARC。
如果这些东西没有配好,你的邮件技术上仍然可能发出去,但很容易进垃圾箱,甚至直接被拒收。
PTR:邮件服务器的来电显示
PTR 是反向 DNS。
普通 DNS 是域名指向 IP,比如:
mail.initsnow.top A <server-ip>
PTR 则是反过来,让 IP 反查回主机名。
也就是说,你最好形成一个闭环:
mail.initsnow.top -> IP
IP -> mail.initsnow.top
这叫 forward-confirmed reverse DNS。很多邮件服务器在 SMTP 握手时都会检查这一点。
重要的是,PTR 不是在 Cloudflare 里配的,而是在你的 VPS 提供商那里配置。因为 IP 地址属于你的 VPS 商,而不是你的 DNS 托管商。
这件事我自己也是后来才完全理顺的。第一次找 PTR 的时候,直觉总会把它往 Cloudflare 面板里找。其实不在那里。
如果你的 VPS 商默认绑了一个类似 vps123.provider.example 的反向解析,那你通常需要把它改成 mail.initsnow.top。有些面板允许改,有些需要提交工单。
SPF:谁可以替这个域发邮件
SPF 是一个 TXT 记录,用来告诉别人:哪些服务器有权代表你的域发邮件。
例如:
initsnow.top TXT "v=spf1 mx -all"
意思是,只有 MX 记录里的服务器可以合法发信。除此之外的都不算数。
这不是加密,也不是签名,更像是一张对外公告。
DKIM:给邮件盖章
DKIM 是给邮件内容做数字签名。发送时,Stalwart 用私钥签名,收件方从 DNS 里取公钥验证。
所以 DNS 里会出现类似:
202603e._domainkey TXT "v=DKIM1; ..."
如果内容被中途篡改,签名就对不上。
这也是为什么 Stalwart WebUI 生成 DNS 记录那么有用。DKIM 的值通常很长,自己手敲风险太高。
DMARC:最后的处理策略
DMARC 是在 SPF 和 DKIM 的基础上再加一层政策。
比如:
_dmarc TXT "v=DMARC1; p=reject; rua=mailto:[email protected]"
这表示,如果 SPF 或 DKIM 验证失败,就直接拒绝。你也可以先从 p=none 开始,先观察再逐步收紧。
很多大厂邮箱对 DMARC 的态度越来越严。没有它不一定马上死,但迟早会感受到阻力。
证书变更和 DNS,不一定永远无关
一开始我以为换 TLS 证书和 DNS 是完全无关的。后来看到 NixOS Wiki 对 TLSA 的说明,才意识到事情没那么绝对。
如果你没有启用 TLSA / DANE,那么换 ACME 证书时,通常不需要修改 DNS。
但如果你发布了 TLSA 记录,那证书变更后,对应指纹也可能需要更新。NixOS Wiki 甚至提到,Stalwart 目前不会自动更新 TLSA 记录,所以有人会写脚本定时计算证书哈希并通过 Cloudflare API 更新 DNS。见 NixOS Wiki 的说明:https://wiki.nixos.org/wiki/Stalwart。
不过实话实说,这已经属于更进阶的优化了。至少在我这次部署过程中,我先把精力放在 MX、SPF、DKIM、DMARC、PTR 和基础 TLS 上。先让邮件正常收发,比一开始就把所有边角都打磨到极致更重要。
所以,这东西到底难在哪
如果只看配置文件,自建邮件服务器好像是一些监听端口、一些 TXT 记录、一个证书,再加一个 Web 管理界面。
但真正难的地方在于,邮件不是一套你自己说了算的协议。它是一个全世界都在给你打分的系统。
你不仅要能发信,还得让别人相信你有资格发这封信。
这就是为什么你会在一个部署流程里,同时碰到:
操作系统配置。
DNS。
TLS。
反向解析。
邮箱客户端兼容性。
垃圾邮件信誉。
看起来每个点都不算特别神秘,但拼在一起,就会让这个过程显得格外像一段缓慢而有点烦人的修路工程。
最后的感受
回头看,这次在 NixOS 上部署 Stalwart 的过程并不算轻松。它当然比传统拼装式邮件服务器省了一些组件层面的麻烦,但并没有消灭邮件系统本身的复杂性。
只是把问题集中到了一个更现代、文档也更完整的系统里。
而这已经很不错了。
至少到最后,我拿到的不是一地散落的脚本和几份半懂不懂的配置,而是一套真的能跑、能登录、能发信、也有希望慢慢变稳的系统。
如果你也打算走这条路,我会给出两个并不潇洒、但很真诚的建议。
不要一开始就试图把所有东西都一次性想通。邮件系统不是那种会因为你足够聪明就立刻对你温柔起来的东西。它更像需要一层层拨开的洋葱,而且还挺辣眼睛。
尽量相信现成资料,而不是和自己的想象力较劲。
最靠谱的三样东西通常是:
- Stalwart 官方文档
- NixOS Wiki
- nixpkgs 里的模块源码
有时候 Wiki 上的例子已经很接近答案,但如果想确认模块到底做了什么,最直接的方法其实是去看源码。
Stalwart 的 NixOS 模块在这里: https://github.com/NixOS/nixpkgs/blob/nixos-25.11/nixos/modules/services/mail/stalwart-mail.nix
看一眼就能知道模块是怎么生成 stalwart-mail.toml 的,哪些默认值是模块自动加进去的。很多时候,比猜配置键快得多。
Stalwart 官方文档: https://stalw.art/docs/
NixOS Wiki: https://wiki.nixos.org/wiki/Stalwart
如果这篇文章能帮后来的人少绕一点弯路,那这次折腾就算没白折腾。