Posted on ::

在 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

如果这篇文章能帮后来的人少绕一点弯路,那这次折腾就算没白折腾。

Table of Contents