upadate:不是latest的0.16 就不修改配置文件了。
“Caddy (前置网关) + Stalwart (隐藏后端) 分离部署架构”,包含了多阶段构建、证书热重载、Layer 4 透传以及管理端隐身等高级安全实践。
🛡️ 架构手册:Caddy L4/L7 + Stalwart
架构核心思想
- 分离解耦:Caddy 与 Stalwart 独立在不同的
docker-compose.yml运行,通过名为stalwart_default的 Docker 网络互通。 - 最小权限/端口收敛:剥夺 Stalwart 直接绑定宿主机端口的权利,彻底隐藏在内网。Caddy 成为全网唯一的出入口。
- 协议透传:采用 Proxy Protocol v2,让隐藏在后端的 Stalwart 依然能获取访客的真实 IP。
- 纵深防御:废弃老旧明文端口(587/143),主域名隐藏管理入口,启用独立的管理子域名。
📂 目录结构建议
建议在宿主机上创建两个完全独立的目录,例如:
/opt/mail/backend/(存放 Stalwart)/opt/mail/gateway/(存放 Caddy)
阶段一:后端部署 (Stalwart)
进入 /opt/mail/backend/,创建或修改 docker-compose.yml。
核心关注点:删除所有的 ports 映射;挂载 Caddy 的证书目录以供共享。
services:
mail-server:
image: stalwartlabs/mail-server:latest
container_name: stalwart
restart: unless-stopped
# ⚠️ 绝对不要在这里配置任何 ports
volumes:
- ./stalwart_data:/opt/stalwart-mail
# 将 Caddy 所在目录的证书源文件以只读方式挂载进来
# 请将前半段替换为你 Caddy 实际所在的绝对路径
- /opt/mail/gateway/caddy_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory:/caddy_certs:ro
networks:
- stalwart_default
networks:
stalwart_default:
name: stalwart_default新建docker网络:docker network create stalwart_default
启动命令:docker compose up -d
阶段二:网关构建与部署 (Caddy)
进入 /opt/mail/gateway/。我们需要三个文件。
1. 动态编译文件 Dockerfile
必须使用多阶段构建,将 layer4 模块编译进 Caddy。
# 阶段 1:使用 Caddy 官方 builder 构建带插件的版本
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/mholt/caddy-l4
# 阶段 2:构建最终运行镜像
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy2. 网关配置 Caddyfile
核心关注点:指向 10xxx 代理接收端口;拦截主域名的 /admin 路径;开放独立的管理子域名。
{
layer4 {
# SMTP (服务器间投递)
0.0.0.0:25 {
route {
proxy {
proxy_protocol v2
upstream stalwart:10025
}
}
}
# IMAPS (收邮件)
0.0.0.0:993 {
route {
proxy {
proxy_protocol v2
upstream stalwart:10993
}
}
}
# SMTPS (发邮件)
0.0.0.0:465 {
route {
proxy {
proxy_protocol v2
upstream stalwart:10465
}
}
}
# ManageSieve (邮件过滤规则)
0.0.0.0:4190 {
route {
proxy {
proxy_protocol v2
upstream stalwart:14190
}
}
}
}
}
# 网页版邮箱 (主域名)
mail.example.com {
# 阻断直接从主域名访问管理后台的尝试 (隐身模式)
abort /admin*
# 代理 JMAP/Webmail 到 Stalwart 内部的安全 HTTPS 端口
reverse_proxy https://stalwart:10443 {
transport http {
proxy_protocol v2
tls_server_name mail.example.com
}
}
}
# 独立的管理员后台 (子域名)
gw-admin.example.com {
reverse_proxy stalwart:8080
}
# Thunderbird 自动配置
autoconfig.sample.com {
reverse_proxy stalwart:8080
}
# Outlook / iOS 自动配置
autodiscover.sample.com {
reverse_proxy stalwart:8080
}
(注:请将 example.com 替换为你的真实域名。Caddyfile 中缩进请使用纯空格,避免非间断空格报错)
3. 网关容器 docker-compose.yml
连接到已有的 stalwart_default 网络。
services:
caddy_proxy:
build:
context: .
dockerfile: Dockerfile
container_name: caddy_edge
restart: always
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "5"
ports:
- "80:80"
- "443:443"
- "443:443/udp"
- "25:25"
- "465:465"
- "993:993"
- "4190:4190"
environment:
- TZ=Asia/Shanghai
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy_data:/data
- ./caddy_config:/config
networks:
- stalwart_default
networks:
stalwart_default:
external: true启动命令:docker compose up -d --build
阶段三:证书共享与热重载
1. 配置 Stalwart 读取证书【挂载卷方式正确的话点证书页的Reload config应该自己出现;但还是确认下】
通过管理后台(或修改 config.toml),将 TLS 证书路径指向挂载进来的 Caddy 目录:
- 证书路径:
%{file:/caddy_certs/mail.example.com/mail.example.com.crt}% - 私钥路径:
%{file:/caddy_certs/mail.example.com/mail.example.com.key}%
2. 宿主机自动化 Crontab
在 Stalwart 生成 API Token 后,在宿主机执行 crontab -e,添加以下任务,每月热重载一次证书缓存:
0 4 1 * * curl -X GET -H "Accept: application/json" -H "Authorization: Bearer <YOUR_TOKEN>" https://mail.example.com/api/reload/certificate⚠️:【在 Token 的权限列表中,找到 Refresh system settings 并选中 On。】
这个动作的本质就是让正在运行的服务重新加载(Reload)状态或缓存,完美对应 reload/certificate 的需求。它不会改变系统原有的配置参数。
⚠️:最佳实践:使用另一个管理域名代替mail.sample.com作为8080的反代入口。
修改上面的任务为:
curl -i -X GET -H "Accept: application/json" -H "Authorization: Bearer <你的TOKEN>" https://gw-admin.example.com/api/reload/certificate阶段四:业务初始化
当访问 https://gw-admin.example.com 登录后台后,第一件事必须是配置邮件域名:
- 区分概念:
mail.example.com是物理主机名,example.com是你的邮件域名。 - 导航至 Directory -> Domains,添加根域名
example.com。 - 导航至 Directory -> Accounts,创建用户时,从下拉列表中选择
example.com,即可拥有[email protected]的标准邮箱。
阶段五:安全策略和运维⚠️ 【不可缺少的后续步骤】
💡 在白名单里加入docker内网地址(放置caddy代理的8080端口被stalwart防火墙误ban)需要修改两处。
第 1 :让 Stalwart 信任新网段(读取真实 IP)
- 登录 Web 管理页面。
- 进入 Settings -> Server -> General。
- 找到 Trusted networks(受信任的网络)。
- 在里面添加你最新的网段:
172.20.0.0/16。 - 点击 Save changes。
👉第二个选项
第 2 :加入安全白名单
- 进入 Settings -> Security -> IP Filter。
- 找到 Whitelist(白名单)。
- 同样加上:
172.20.0.0/16。 - 点击 Save changes。
第 3 步:全局重载
- 点击右上角的 Save & Reload。
添加listener和信任的docker网段


⚠️以10993为例tls设置的区别(10025不需要implicit TLS)
这些+10000的端口会被stalwart内部转发,不需要写在caddy的ports部分)
正常设置后的日志里应该类似这样:
caddy和stalwart共享卷的权限问题
# 1. 进入证书所在的物理目录
cd /opt/caddy/caddy_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory
# 2. 赋予所有用户读取权限 (从 600 提升到 644)
# 文件夹需要 755 (rx),文件需要 644 (r)
chmod -R 755 .
chmod 644 mail.sample.com/mail.sample.com.crt
chmod 644 mail.sample.com/mail.sample.com.key虽然眼下系统能用,但如果caddy更新后,很大概率证书文件夹的权限会被改变。
所以需要下面的方案 或参考官方手册,使用systemd同步证书:https://stalw.art/docs/server/reverse-proxy/caddy/【推荐】