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/caddy

2. 网关配置 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 登录后台后,第一件事必须是配置邮件域名:

  1. 区分概念:mail.example.com 是物理主机名,example.com 是你的邮件域名。
  2. 导航至 Directory -> Domains,添加根域名 example.com。
  3. 导航至 Directory -> Accounts,创建用户时,从下拉列表中选择 example.com,即可拥有 [email protected] 的标准邮箱。

阶段五:安全策略和运维⚠️ 【不可缺少的后续步骤】

💡 在白名单里加入docker内网地址(放置caddy代理的8080端口被stalwart防火墙误ban)需要修改两处。

第 1 :让 Stalwart 信任新网段(读取真实 IP)

  1. 登录 Web 管理页面。
  2. 进入 Settings -> Server -> General。
  3. 找到 Trusted networks(受信任的网络)。
  4. 在里面添加你最新的网段:172.20.0.0/16。
  5. 点击 Save changes。
    👉第二个选项image

第 2 :加入安全白名单

  1. 进入 Settings -> Security -> IP Filter。
  2. 找到 Whitelist(白名单)。
  3. 同样加上:172.20.0.0/16。
  4. 点击 Save changes。

第 3 步:全局重载

  • 点击右上角的 Save & Reload。

添加listener和信任的docker网段

image

image
⚠️以10993为例tls设置的区别(10025不需要implicit TLS)
这些+10000的端口会被stalwart内部转发,不需要写在caddy的ports部分)
image
正常设置后的日志里应该类似这样:
image

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/【推荐】

https://ytai.de/t/topic/123

最后,在thunderbird里测试时需要注意⚠️,默认的用户名不是 完整邮箱地址 而是username(@之前的,不包括域名);thunderbird里设置时最好手动先调试,IMAP和SMTP不要选自动加密,而是选SSL/TLS,密码选plain txt。