其实我一直有在关注Stalwart Mail这个项目,早在2年多以前我就写过一篇部署的文章,当时的Stalwart Mail还没有Web UI,所有操作都只能通过CLI完成。我还有一台服务器一直在使用Stalwart Mail,只不过这台服务器运行的版本比较旧了:
0.14还是升级过的,如果我没记错的话,我应该是从0.11升级上来的。时过境迁,这个项目也从最初的几百个star变成了如今的破万star,我觉得我有资格谈一谈这个程序目前面临的一些问题。
用了这么多年这个邮件服务器,让我觉得最操蛋的地方就是每次大版本更新简直就是灾难,隔个版本就来一大堆破坏性的更改,每次更新都得跟着官方release里面的步骤小心翼翼一步步来,没有哪一次大版本更新是能够轻松完成的。在我的印象里,像这种打包成一个二进制文件或者用一个docker容器就能跑起来的程序,更新不就是pull个新的image然后up就完事了么,但是Stalwart Mail很任性,你要想这么简单的升个级那等着你的绝对是boom,这也就是为什么我之前这台服务器一直运行的是0.14版本,我已经不打算把它升级到0.15然后再从0.15升级到目前最新的0.16了。
现在好就好在作者意识到这个问题了,且之前有说过0.16是个很关键的版本,在0.16后应该不会再引入大规模的破坏性更改,且在今年晚些时候会发布具有里程碑意义的1.0版本:《立即升级到0.16,或者等待1.0版本》
然后就是这个项目的定位我觉得有点问题,作者似乎在开发这块有点割裂,想让Stalwart Mail成为一个新手小白都能快速上手的All-in-One邮件服务器,但同时又想兼顾具备专业知识和有能力的人去折腾,这就导致了两个问题:
1.官方文档写的摸棱两可,不清不楚,明明给个示例配置就完事儿的事情,非要在那里车轱辘话说一大堆,到头来新手看了半天文档还是不知道怎么操作,老鸟又没有看的必要。。我认为一篇优秀的文档应该是理论与实践并行,缺一不可,如果只能2选1,那我宁愿选择实践。
2.Web UI对每个功能的可配置性我觉得有点过于细致了,作者似乎想把有关邮件服务器内所有能配置的东西都给你在Web UI上列出来,让你可以看到每一个细节,修改每一个参数。这看上去很美好,但是新手一看到这个Web UI头皮都是麻的,完全看不懂这些设置项有什么用,该怎么配置。别说新手了,就算是具备一定专业知识的人可能都不能完全了解这些设置的作用,再加上文档极其抽象,第一次接触到这个Web UI的人就只会感觉到复杂与繁琐。
当然这些只是我的猜测,作者真实的开发意愿我无从得知,我也无意干预作者对项目开发的路线,我只是想说如果能在这之间找到一个平衡点那当然是最好的,如果找不到平衡点那就专攻一个点去做就行了。
好就好在现在的0.16版本似乎在朝着这个“平衡”的方向发展,在用户首次登录的时候会有一个“配置向导”,只要你按照这个向导去完成配置,那么剩下的那些“让人看不懂”的配置基本就可以不用管了,你的邮件服务器也能正常工作。对于喜欢折腾或者有更多需求的人可以在向导完成后去修改那些更高级的配置。
除了上述我说的这些问题外,还有一个我有点担心的问题是,作者能否保持初心不去恰烂钱,目前来看似乎有点这方面的趋势,小声bb:Masked Emails功能。但愿作者后续不会把关键且核心的功能加到企业版本付费才能使用。
让我们正式开始部署,本文尽量用大白话把一些容易出问题的地方给讲清楚,同时为方便理解,本文不对敏感内容脱敏。准备工作,一个域名(本文示例:ohsb.cc)接入到Cloudflare并做好如下DNS解析:
| 名称 | 类型 | 内容 |
|---|---|---|
| A | 152.53.89.75 | |
| bulwark | A | 152.53.89.75 |
我们不在这里配置MX、SPF、DKIM、DMARC等DNS记录,这些记录稍后统一交给Stalwart自动管理,由Stalwart自行添加。除了这些正向DNS记录外,你的服务器最好支持设置PTR/rDNS,这里我以Netcup的VPS为例,值与正向DNS中的A记录匹配:
验证PTR/rDNS是否生效:
dig -x 152.53.89.75 +short
安装NGINX、CertBot、Docker:
apt update
apt install curl nginx python3-certbot-nginx
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
创建目录和compose文件:
mkdir -p /opt/stalwart-mail && cd /opt/stalwart-mail && nano docker-compose.yml
写入如下内容:
services:
stalwart:
image: stalwartlabs/stalwart:v0.16
container_name: stalwart
restart: unless-stopped
ports:
- "8443:443"
- "8080:8080"
- "25:25"
# - "587:587"
- "465:465"
# - "143:143"
- "993:993"
# - "110:110"
# - "995:995"
# - "4190:4190"
volumes:
- ./stalwart-etc:/etc/stalwart
- ./stalwart-data:/var/lib/stalwart
bulwark:
image: ghcr.io/bulwarkmail/webmail:latest
container_name: bulwark
restart: unless-stopped
environment:
BULWARK_TELEMETRY: off
ports:
- "3008:3000"
depends_on:
- stalwart
volumes:
- bulwark-settings:/app/data/settings
- bulwark-config:/app/data/admin
- bulwark-state:/app/data/admin-state
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://127.0.0.1:3000/api/health",
]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
volumes:
bulwark-settings:
bulwark-config:
bulwark-state:
1.由于stalwart容器使用的是bind-mounts,且该镜像以非特权用户(UID2000)身份运行,所以主机目录的所有者必须是UID2000,我们就自行创建目录并修改所有者:
mkdir stalwart-data stalwart-etc && chown 2000:2000 stalwart-data/ stalwart-etc/
2.根据官方的安全实践,禁用了587 / 143 / 110 / 995 / 4190这些非必要且可能有安全问题的端口,如果你有需要可以取消相应的注释。
3.考虑到stalwart后续的大版本更新可能会比较麻烦,官方也不建议将镜像的tag改成latest,所以这里固定使用v0.16,避免导致后续升级时的混乱,例如当前使用的latest指向的是0.16,但这期间官方连发了2个大版本,最新的latest已经指向0.18了,那现在本地的这个0.16去拉最新的0.18升级肯定是要出问题的。
4.官方文档关于NGINX反向代理的示例配置是使用TCP直通,既NGINX不终止TLS,直接将TLS会话原封不动地传递给stalwart。我不打算使用这种方式,我觉得这种方式有点脱裤子放屁的意思,谁没事会在一台服务器上面运行两个甚至多个邮件服务器?完全没有必要通过NGINX去转发25 / 465 / 993的流量嘛,且一旦NGINX使用stream模块监听443端口后,443端口会被stream模块独占,那么http模块就不能再监听443端口了,如果你的服务器上还运行着其它网站,那这些网站都将无法访问。除了这些以外,你还得去配置Proxy Protocol,不然stalwart拿不到客户端真实IP一大堆的功能都会出问题。这简直是一个亏损最大化的反代方式。。。如果要我用这种方式,那我不如单独拿一台服务器出来只跑stalwart得了。。。
考虑再三,我决定使用一个折中的方案,既我们只使用NGINX反向代理stalwart的HTTP端口(8080),其它的邮件服务端口,如25 / 465 / 993这些全部由stalwart自身处理。这种方案唯一的缺点是你需要维护两套TLS证书,既NGINX需要一套证书,stalwart自身也需要一套证书,但好在证书申请和配置都比较简单,NGINX有certbot,stalwart则可以通过ACME DNS 01全自动完成。这种方案也不会导致stalwart的功能有异常或者缺失,因为stalwart 8080端口提供的服务(OAuth、OIDC、JMAP、Autoconfig等)与stalwart 443端口提供的服务完全一致。
启动:
docker compose up -d
查看临时的管理员账号和密码:
docker logs stalwart 2>&1 | grep -A8 'bootstrap mode'
注意这个管理员账号和密码仅用于引导程序运行设置向导,等向导完成后会重新配置一个永久的管理员帐户:
配置NGINX反向代理,先配置stalwart的反代:
nano /etc/nginx/sites-available/stalwart
写入如下内容:
server {
listen 80;
server_name mail.ohsb.cc autoconfig.ohsb.cc autodiscover.ohsb.cc ua-auto-config.ohsb.cc mta-sts.ohsb.cc;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 支持 JMAP Push (WebSocket)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
1.注意server_name不仅仅要有mail.ohsb.cc,自动配置、自动发现以及mta-sts这些功能也需要配置单独的主机名。
2.JMAP依赖WebSocket,务必启用。
启用站点:
ln -s /etc/nginx/sites-available/stalwart /etc/nginx/sites-enabled/stalwart
继续配置bulwark webmail的反代:
nano /etc/nginx/sites-available/bulwark
写入如下内容:
server {
listen 80;
server_name bulwark.ohsb.cc;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:3008;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
启用站点:
ln -s /etc/nginx/sites-available/bulwark /etc/nginx/sites-enabled/bulwark
签发证书:
certbot --nginx
访问https://mail.ohsb.cc/admin打开Web UI,如果你发现访问https://mail.ohsb.cc/admin报错502,那么暂时先使用http://152.53.89.75/admin,这大概率是因为stalwart没有获取到客户端的真实IP,将所有传入的请求都识别为Docker桥接网络的网关IP,然后因为有公网扫描器执行的一些探测扫描被stalwart识别到了,stalwart就把Docker桥接网络的网关IP给ban了导致的,这个问题稍后等安装向导完成后再来解决。
使用临时管理员账号登录开始安装向导,配置主机名和邮箱域名:
配置存储,直接用默认的RocksDB就好了,单机部署最佳选择:
账户目录,直接用默认的内部目录即可:
日志目标,这里默认的是文件形式,且保存路径在/var/log/stalwart:
由于我们之前没有挂载这个目录,建议将这里的日志输出模式改为console,后续可以通过如下命令来查看日志:
docker compose logs
如果硬要以文件形式来保存日志,那么应该在compose文件内增加如下内容:
volumes:
- ./stalwart-etc:/etc/stalwart
- ./stalwart-data:/var/lib/stalwart
- ./stalwart-log:/var/log/stalwart
并主动创建目录及修改所有者:
mkdir stalwart-log && chown 2000:2000 stalwart-log
然后就到了非常关键的一步了:自动DNS管理。将Cloudflare的API Token输入到这里,即可让stalwart自动为你创建DNS记录,例如MX、SPF、DKIM、DMARC等。注意MX记录默认是没有为你创建的,需要后续手动配置,我稍后会详细说明:
向导完成后,会重新打印一个管理员账号和密码,这就是永久的管理员账号了,请妥善保管:
重启容器以使新配置生效:
docker compose down
docker compose up -d
再次登录到Web UI,让我们继续完善设置。向导虽然为我们配置了大多数设置,但还有以下步骤缺一不可,请务必全部按照操作完成。
1.找到HTTP Server页面,启用Obtain remote IP from Forwarded header,这将确保stalwart能够获取到客户端的真实IP:
2.找到HTTP Security页面,启用Permissive CORS policy,这将解决bulwark webmail无法正常登录,报错CORS禁止的问题:
3.如果你之前访问https://mail.ohsb.cc/admin报错502,那么请找到Blocked IP addresses页面,不出意外的话,这里会列出你的Docker桥接网络的网关IP:
把这个IP从封禁列表删除,然后找到Actions页面,点击Blocked IPs list按钮重载配置,使其生效:
之前已经启用了Obtain remote IP from Forwarded header,所以stalwart不会再将所有通过NGINX代理的IP都识别为Docker桥接网络的网关IP,这个问题也就解决了,且自动封禁的功能也将正常工作。
4.我们使用stalwart自动管理DNS记录,所以找到Domains页面,点击你的域名进入详情页:
找到DNS Management内的Record Types,可以看到这里默认没有MX记录:
勾选MX记录,保存设置:
找到Actions,重载服务器配置使其生效,请注意,大多数配置修改后都需要这一步才能使新的配置生效:
最后我们还需要找到Tasks页面,创建一个新的任务:
Task type选择Perform DNS management for a domain,Record Types选择MX records,Due设置为你当前时间的后1分钟:
5.在你的个人账户设置页面,创建一个名为Archive的文件夹,以适配bulwark webmail及其它邮件客户端的归档功能:
到这里stalwart就配置好了,接下来配置bulwark webmail,在你首次访问bulwark webmail的时候会让你输入一个安装token:
使用如下命令查看bulwark容器日志以获取安装token:
docker compose logs -f bulwark
bulwark webmail是一个基于JMAP协议的客户端,这里需要配置你的JMAP server URL:
测试一下邮件发送,能发保底10分:
能收:
文章篇幅有限,更多高级功能如PGP加密,反垃圾邮件配置,这些内容另外单独用一篇文章说明,未完待续。。。
荒岛













































