前言
有一台 VPS 和一个域名,就想给自己搭个邮箱。这里使用 Docker Mailserver 收发邮件,先配置客户端使用。网页端可选,后面再用 Docker 部署 Roundcube 和 Nginx。账号只由管理员创建,不开放注册。
这次使用的环境:
- Ubuntu 20.04.6 LTS,内存约 2.4 GiB,Swap 约 1.2 GiB
- Docker Mailserver v16.0.1
- Roundcube + SQLite
- Cloudflare DNS,Snap 版 Certbot
邮箱域名是 f0rget.me,邮件服务器使用 mail.f0rget.me,网页入口使用 webmail.f0rget.me。下面记录配置和部署过程。
Docker Mailserver
Docker Mailserver,下面简称 DMS,将 Postfix、Dovecot 等邮件组件整合到一个容器中。Postfix 负责接收、排队和转发邮件,Dovecot 负责认证、投递和 IMAP 访问,配置通过环境变量和本地文件管理。项目介绍
这里选择 DMS,主要是账号数量不多,用命令行管理就够了。邮件和配置都挂载到宿主机目录,后续备份和迁移也方便,不用自己从头组合这些服务。
考虑到服务器内存不大,配置中关闭了 ClamAV 和 Amavis,使用 Rspamd 处理反垃圾。ClamAV 关闭后没有它提供的病毒扫描,具体能承受多少邮箱和并发还要看使用情况。
Roundcube
Roundcube 是一个开源网页邮件客户端,可以在浏览器中读信、写信、上传附件和管理联系人。它通过 IMAP 读取 DMS 中的邮件,通过 SMTP 发信。项目介绍
网页端只需要基本收发功能,因此选了 Roundcube。数据库用 SQLite,省去单独部署数据库容器,联系人和偏好等数据保存在本地。镜像说明
登录用的是 DMS 中已有的邮箱账号,密码也由 DMS 管理。Roundcube 保存自己的用户设置,不负责创建邮箱;停掉它以后,仍然可以用 Thunderbird、Gmail 等客户端软件连接 DMS 收发邮件。
部署架构

完整部署包含以下三个目录。只使用邮件客户端时,部署 DMS 即可:
/opt/docker-mailserver:容器名docker-mailserver,使用自己的默认 bridge 网络,发布邮件端口。/opt/roundcube:容器名roundcube,加入外部 bridge 网络webmail-net。/opt/webmail-proxy:容器名webmail-nginx,也加入webmail-net,发布 80 和 443。
Nginx 和 Roundcube 加入同一个 webmail-net 网络,上游直接写 http://roundcube:80,由 Docker 解析容器名称。Roundcube 则连接 mail.f0rget.me:993/587,DMS 使用自己的默认 bridge 网络即可。Docker 网络说明
Roundcube 不向宿主机发布端口,Nginx 通过 webmail-net 访问它的容器内 80 端口。
收发流程
收信: 对方查询域名的 MX,连接邮件服务器的 25 端口。Postfix 接收邮件后交给 Dovecot 投递,保存在 /var/mail。
读信: 浏览器访问 Nginx 的 443 端口,Nginx 转发到 Roundcube,Roundcube 再通过 993 端口读取邮箱。
发信: Roundcube 通过 587 端口认证并提交邮件,Rspamd 按配置签名,再由 Postfix 连接收件方的 25 端口投递。客户端显示发送成功后,还可以在投递日志中查看对方是否接受。
域名与端口
| 名称 | 用途 |
|---|---|
f0rget.me |
邮箱地址右边的域名,也是 MX、SPF、DMARC 等记录所属的域 |
mail.f0rget.me |
SMTP / IMAP 主机名、邮件证书名称、PTR 对应的主机名 |
webmail.f0rget.me |
浏览器网页邮箱入口、网页证书名称 |
[email protected] |
实际创建的邮箱账号,登录时使用完整地址 |
下面的 DNS 示例用 203.0.113.10 占位,部署时替换成自己的公网 IP。
邮件协议
SMTP 用于发送和转交邮件。客户端把邮件提交给自己的服务器,服务器再转发给收件方,两段连接都使用 SMTP,但用途和端口不同。这里客户端使用 465 或 587,并验证邮箱账号;服务器之间使用 25,允许外部服务器向本域的有效收件人投递,不要求对方持有本机账号,也不代表允许它任意转发第三方邮件。
IMAP 用于访问服务器上的邮箱。邮件、文件夹和已读状态以服务器为中心,电脑、手机和网页端可以同步操作,因此这里启用 IMAP。它负责读取和管理邮件,不负责发信,客户端仍要另外配置 SMTP。IMAP 协议
POP3 主要用于将邮箱中的邮件下载到客户端,下载后可以保留服务器副本,也可以按客户端设置删除。它不提供 IMAP 那样的文件夹和状态同步,这次不使用,所以设置 ENABLE_POP3=0,也不发布 110、995。POP3 协议
邮件端口
| 端口 | 协议与用途 | 本次配置 |
|---|---|---|
| TCP 25 | SMTP 服务器间投递,可协商 STARTTLS | DMS 发布 25;还要允许连接对方服务器的目的端口 25 |
| TCP 465 | SMTP Submission,连接开始就建立 TLS,再认证发信 | DMS 发布 465,可供客户端使用 |
| TCP 587 | SMTP Submission,先连接 SMTP,再通过 STARTTLS 升级为 TLS | DMS 发布 587,Roundcube 使用它 |
| TCP 993 | IMAP over TLS,连接开始即 TLS | DMS 发布 993,客户端和 Roundcube 读取邮箱 |
| TCP 143 | IMAP,可通过 STARTTLS 升级 | 本次不发布,使用 993 |
| TCP 110 | POP3,可通过 STLS 升级 | 本次关闭 POP3,不发布 |
| TCP 995 | POP3 over TLS | 本次关闭 POP3,不发布 |
| TCP 4190 | ManageSieve,用于管理服务端邮件过滤规则 | 本次不发布 |
465 和 993 的“SSL/TLS”设置表示隐式 TLS,也就是连接开始就进行 TLS 握手。587 使用 STARTTLS,先发送 SMTP 问候和能力协商,再升级到加密连接,账号密码在 TLS 建立后提交。客户端的端口和加密选项要对应,不能把 587 配成连接开始即 TLS。TLS 与端口约定
25 的入站用于收信,出站用于把邮件投递给其他域。这里“出站 25”指连接对方的目的端口,源端口由系统分配。即使客户端能连接 587 提交邮件,如果 VPS 商家阻断出站 25,Postfix 仍无法直接向外部 MX 投递。服务器间的 25 可以协商 STARTTLS,但不保证每一跳都加密。DMS 端口说明
网页与管理端口
| 端口 | 用途 | 本次配置 |
|---|---|---|
| TCP 80 | 浏览器 HTTP 入口,跳转 HTTPS | 由 Nginx 发布,可选网页端使用 |
| TCP 443 | 浏览器 HTTPS 入口 | 由 Nginx 发布,可选网页端使用 |
| TCP 22 | SSH 管理服务器 | 保留实际管理端口 |
| 容器内 TCP 80 | Roundcube 的 HTTP 服务 | 仅由 Nginx 通过 webmail-net 访问,不映射到宿主机 |
当前宿主机监听的是 22、25、465、587、993、80 和 443,没有 8080 映射。Rspamd、Redis 等内部服务也不需要发布到公网。
服务器还要能够出站访问 DNS 和 HTTPS,用于解析域名、拉取镜像和申请证书;不需要因此开放入站 53。可以查看当前环境和监听情况:
docker version
docker compose version
free -h
df -h /opt
ss -lntup
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}'
在 VPS 安全组和宿主机防火墙中放行对应端口,并保留 SSH 管理入口。Docker 发布端口会生成转发规则,实际可达性从另一台机器测试。
基础 DNS
先在 Cloudflare 配好 A、MX 和 SPF。DKIM 公钥要等 DMS 生成密钥后再填写,DMARC 也放到后面一起配置。下面的 203.0.113.10 替换成自己的公网 IP。
| 类型 | 名称 | 内容 | 用途 |
|---|---|---|---|
| A | 203.0.113.10 | 邮件主机的公网 IPv4 | |
| MX | @ | mail.f0rget.me,优先级 10 | 将发给 @f0rget.me 的邮件交给该主机 |
| TXT | @ | v=spf1 mx -all | 授权 MX 对应的地址代表本域发信 |
mail 的 A 记录使用 仅 DNS / 灰云。普通 Cloudflare HTTP 代理不能转发 SMTP 和 IMAP。根域名已有网站时,A 记录保持原来的值即可;没有可用 IPv6 时,不添加 mail 的 AAAA。
SPF 检查实际发信 IP 是否被信封发件人域授权。这里 mx 授权 MX 主机对应的地址,-all 表示其他来源不获授权,所以 A、MX 和 SPF 一起设置。以后增加第三方发信服务时,合并到同一条 SPF 记录中。
PTR 反向解析
A 记录将域名解析到 IP,PTR 则将 IP 反向解析成主机名。这里设置成:
公网 IP --PTR--> mail.f0rget.me --A--> 同一个公网 IP
SMTP HELO/EHLO 主机名:mail.f0rget.me
这样正向和反向解析就能对应起来。PTR 通常在 VPS 商家后台设置,或者提交工单,由 IP 所属方处理。收件服务器会用它检查发信 IP 与主机名是否一致,Google 的发信指南也有这项要求。发信要求
申请如下:

客服回应:

PTR 配好后,仍然要配置 SPF、DKIM 和 DMARC。它们检查的内容不同,投递结果还会受到 IP 信誉等因素影响。
解析检查
dig +short A mail.f0rget.me
dig +short MX f0rget.me
dig +short TXT f0rget.me
# 替换成自己的公网 IP
dig +short -x 203.0.113.10
此时 MX 应指向 mail.f0rget.me,SPF 应能查到 v=spf1 mx -all,PTR 与邮件主机名对应即可。这里还不检查尚未配置的 DKIM 和 DMARC。

部署准备
以下命令在 VPS 的 root shell 中执行。已有部署先备份,账号、DKIM 密钥和数据库继续沿用。目录安排如下:
/opt/docker-mailserver/
├── compose.yaml
├── mailserver.env
├── secrets/cloudflare.ini
└── docker-data/dms/
├── mail-data/ → /var/mail
├── mail-state/ → /var/mail-state
├── mail-logs/ → /var/log/mail
└── config/ → /tmp/docker-mailserver
/opt/roundcube/
├── compose.yaml
├── db/ → /var/roundcube/db
└── www/ → /var/www/html
/opt/webmail-proxy/
├── compose.yaml
└── nginx/conf.d/webmail.conf
/etc/letsencrypt/
├── live/
├── archive/
├── renewal/
└── renewal-hooks/deploy/
install -d /opt/docker-mailserver/docker-data/dms/{mail-data,mail-state,mail-logs,config}
install -d -m 700 /opt/docker-mailserver/secrets
install -d /opt/roundcube/{www,db}
install -d /opt/webmail-proxy/nginx/conf.d
Docker 和 Compose 已安装的话可以跳过安装。新服务器按 Docker Ubuntu 安装说明安装后,确认 docker compose version 有输出。下面记录的是这次使用的 Ubuntu 20.04 环境,新装系统时另行确认当前支持情况。
镜像版本
Compose 中使用了 latest,这次 DMS 的启动日志显示 v16.0.1,Roundcube 显示 1.7.4。这个标签之后还会更新,如果迁移时想使用相同镜像,可以在原服务器记录 digest:
docker inspect --format '{{.Name}} requested={{.Config.Image}} image-id={{.Image}}' \
docker-mailserver roundcube
docker image inspect ghcr.io/docker-mailserver/docker-mailserver:latest \
--format '{{json .RepoDigests}}'
docker image inspect roundcube/roundcubemail:latest \
--format '{{json .RepoDigests}}'
Nginx 也可以用相同方法记录。恢复时将 image 写成对应的 仓库@sha256:...;旧镜像无法拉取时,也可以通过 docker image save 和 docker image load 保存、导入,注意新旧机器的 CPU 架构。
证书申请
先申请 mail.f0rget.me 的邮件证书,供 SMTP 和 IMAP 使用。网页端的证书在部署网页时再申请。
证书使用 Let’s Encrypt,由宿主机上的 Certbot 管理。验证方式选 DNS-01:Certbot 调用 Cloudflare API,临时写入 _acme-challenge TXT 记录,验证域名后签发证书。这个过程不占用服务器的 80 端口。Cloudflare 插件说明
DMS 的 SSL_TYPE=letsencrypt 用来读取已有证书,申请和续期仍然交给宿主机 Certbot,再把证书目录挂载到容器中。DMS TLS 配置
安装 Certbot
这里使用 Snap 版 Certbot,安装时的版本是 5.8.0。已有 Certbot 的机器先确认安装来源,避免和系统包中的版本混用:
command -v certbot
certbot --version
snap list certbot
# 新机没有 Snap 时,先通过系统包管理安装 snapd
# apt-get update
# apt-get install -y snapd
snap install --classic certbot
snap set certbot trust-plugin-with-root=ok
snap install certbot-dns-cloudflare
snap connect certbot:plugin certbot-dns-cloudflare
/snap/bin/certbot plugins
后面统一使用 /snap/bin/certbot。安装后查看插件列表,能看到 dns-cloudflare 即可
Cloudflare API Token
在 Cloudflare 创建 API Token,只授权所需域名区域的 DNS 编辑权限,区域读取权限,创建后记录 token

然后保存凭据文件:
install -m 600 /dev/null /opt/docker-mailserver/secrets/cloudflare.ini
nano /opt/docker-mailserver/secrets/cloudflare.ini
文件内容:
dns_cloudflare_api_token = 在此填写实际Token
chown root:root /opt/docker-mailserver/secrets/cloudflare.ini
chmod 600 /opt/docker-mailserver/secrets/cloudflare.ini
stat -c '%U:%G %a %n' /opt/docker-mailserver/secrets/cloudflare.ini
这个文件包含 API Token,保留为 root 所有、权限 600。
申请证书
/snap/bin/certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /opt/docker-mailserver/secrets/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
--cert-name mail.f0rget.me -d mail.f0rget.me
/snap/bin/certbot certificates
ls -l /etc/letsencrypt/live/mail.f0rget.me/
按提示填写通知邮箱并接受服务条款。首次搭建可以先填一个已有的外部邮箱。这里将 DNS 等待时间设为 30 秒,申请成功后查看邮件证书的路径和有效期。
live 下的 fullchain.pem、privkey.pem 通常是指向 archive 的符号链接,所以挂载时使用整个 /etc/letsencrypt:ro,备份也保存整个目录。Certbot 说明

Docker Mailserver 部署
mailserver.env
保存为 /opt/docker-mailserver/mailserver.env:
OVERRIDE_HOSTNAME=mail.f0rget.me
ACCOUNT_PROVISIONER=FILE
ENABLE_IMAP=1
ENABLE_POP3=0
ENABLE_CLAMAV=0
ENABLE_RSPAMD=1
ENABLE_OPENDKIM=0
ENABLE_OPENDMARC=1
ENABLE_POLICYD_SPF=0
ENABLE_FAIL2BAN=1
ENABLE_AMAVIS=0
SSL_TYPE=letsencrypt
[email protected]
SPOOF_PROTECTION=1
DMS_VMAIL_UID=5000
DMS_VMAIL_GID=5000
LOG_LEVEL=info
参数说明如下:
| 变量 | 说明 |
|---|---|
OVERRIDE_HOSTNAME=mail.f0rget.me |
显式指定邮件主机 FQDN,与 Compose hostname、证书和 PTR 对齐;不等于邮箱只能用 @mail.f0rget.me |
ACCOUNT_PROVISIONER=FILE |
通过持久化文件提供账号和别名,适合管理员创建账号的方式 |
ENABLE_IMAP=1 |
开启 IMAP,网页和桌面客户端读取邮箱需要它 |
ENABLE_POP3=0 |
不使用 POP3,本文不开放 POP3 端口 |
ENABLE_CLAMAV=0 |
关闭 ClamAV 病毒扫描,减轻资源负担;Rspamd 反垃圾不等于 ClamAV 病毒扫描 |
ENABLE_RSPAMD=1 |
开启 Rspamd,参与反垃圾与邮件身份检查等处理 |
ENABLE_OPENDKIM=0 |
关闭独立 OpenDKIM 进程;本次实际由 Rspamd 签名,不表示没有 DKIM |
ENABLE_OPENDMARC=1 |
保留实际启用的 OpenDMARC;它与 Rspamd 的 DMARC 功能重叠,详见下文 |
ENABLE_POLICYD_SPF=0 |
关闭独立 policyd-spf;不意味着删除域名 SPF TXT,也不意味着 Rspamd 不检查 SPF |
ENABLE_FAIL2BAN=1 |
开启 Fail2Ban,Compose 还需要 NET_ADMIN;需要验证封禁规则是否实际生效 |
ENABLE_AMAVIS=0 |
关闭 Amavis,沿用本次 Rspamd 路线 |
SSL_TYPE=letsencrypt |
使用宿主机已有的 Let’s Encrypt 证书,需要挂载到容器可见路径 |
[email protected] |
指定管理员地址,不会自动建立可登录账号或别名,需要单独配置收件去向 |
SPOOF_PROTECTION=1 |
限制已登录用户使用未经授权的发件地址;合法别名也要按账号关系配置 |
DMS_VMAIL_UID=5000 |
邮件存储的数值 UID;保持实际配置值 5000 |
DMS_VMAIL_GID=5000 |
邮件存储的数值 GID;保持实际配置值 5000 |
LOG_LEVEL=info |
DMS 启动与配置脚本输出一般信息;不等于所有内部服务都采用相同日志详细度 |
参数说明参考 环境变量文档和 v16.0.1 配置样本。没有写出的变量沿用镜像默认值或已有自定义配置。
Compose 配置
在 /opt/docker-mailserver 下创建 compose.yaml,内容如下:
services:
mailserver:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
container_name: docker-mailserver
hostname: mail.f0rget.me
env_file:
- mailserver.env
ports:
- "25:25"
- "465:465"
- "587:587"
- "993:993"
volumes:
- ./docker-data/dms/mail-data/:/var/mail/
- ./docker-data/dms/mail-state/:/var/mail-state/
- ./docker-data/dms/mail-logs/:/var/log/mail/
- ./docker-data/dms/config/:/tmp/docker-mailserver/
- /etc/letsencrypt:/etc/letsencrypt:ro
- /etc/localtime:/etc/localtime:ro
cap_add:
- NET_ADMIN
restart: unless-stopped
启动与首个账号
下面通过绝对路径指定 Compose 文件,从 /root 或其他目录执行都可以:
docker compose -f /opt/docker-mailserver/compose.yaml config -q
docker compose -f /opt/docker-mailserver/compose.yaml up -d
首次启动时 Dovecot 会等待创建邮箱,接着在容器中添加账号:
docker exec -it docker-mailserver setup email add [email protected]
按提示输入并确认密码,已有账号则跳过添加。首次无账号的等待有超时限制,应接着完成创建,然后查看启动日志和账号列表:
docker logs --tail 100 docker-mailserver
docker exec -it docker-mailserver setup email list
setup 是 DMS 容器内的管理命令,前面的 docker exec 用于进入指定容器执行。账号管理说明

DKIM 与 DMARC
DKIM
首次部署时在容器中生成密钥:
docker exec -it docker-mailserver setup config dkim
已有部署或迁移时保留原密钥,跳过重新生成。命令会输出要添加的 TXT 名称和公钥。本次域为 f0rget.me,selector 为 mail,在 Cloudflare 中填写:
| 类型 | 名称 | 内容 |
|---|---|---|
| TXT | mail._domainkey | 命令输出的完整 v=DKIM1; k=rsa; p=… 公钥记录 |
这里的省略号只是说明格式,不能原样填入 DNS。复制本机命令输出的完整内容;名称补全后应是 mail._domainkey.f0rget.me。私钥保留在服务器中,不上传到 DNS。
这里关闭 OpenDKIM 后仍然有 DKIM 签名,因为签名由 Rspamd 完成。上面的命令执行后,输出来自 rspamd-dkim,本次生成的文件是:
/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-f0rget.me.private.txt
/tmp/docker-mailserver/rspamd/override.d/dkim_signing.conf
这是容器路径,对应宿主机:
/opt/docker-mailserver/docker-data/dms/config/rspamd/dkim/
/opt/docker-mailserver/docker-data/dms/config/rspamd/override.d/dkim_signing.conf
备份时带上 config/rspamd 即可保存这部分配置和密钥。测试邮件在 Gmail 的邮件头中显示过 dkim=pass,selector 是 mail。
配置中还保留了 ENABLE_OPENDMARC=1,因此会同时运行 OpenDMARC 和 Rspamd。启动日志对此有一条提示,官方建议使用 Rspamd 时关闭对应的旧服务。Rspamd 配置说明
这里保留实际配置 ENABLE_OPENDMARC=1。如后续改为仅使用 Rspamd,可单独调整为 0,再验证收发和 DMARC 检查。
添加后查询并检查配置:
dig +short TXT mail._domainkey.f0rget.me
docker exec docker-mailserver cat /tmp/docker-mailserver/rspamd/override.d/dkim_signing.conf
docker exec docker-mailserver rspamadm configtest
公钥较长时,dig 可能将同一条 TXT 显示成相邻的两段字符串,这是正常的。本次配置中的签名域为 f0rget.me,selector 为 mail,与 DNS 名称对应。
DMARC
DMARC 不生成私钥,它是一条 DNS 策略记录。配置好 SPF 和 DKIM 后,再添加本次使用的记录:
| 类型 | 名称 | 内容 |
|---|---|---|
| TXT | _dmarc | v=DMARC1; p=none; rua=mailto:[email protected] |
p=none 用于观察,不要求接收方因 DMARC 失败而隔离或拒收;rua 指定汇总报告地址,这里使用已经创建的邮箱。
DMARC 检查用户看见的 From: 域:SPF 或 DKIM 至少一项通过,并且相应验证域与 From 域对齐,才满足 DMARC 验证。因此 SPF 通过也不一定意味着可见发件地址已经验证。机制说明
dig +short TXT _dmarc.f0rget.me
客户端与收发测试
账号管理
# 增加新账号,交互输入密码
docker exec -it docker-mailserver setup email add [email protected]
# 修改已有账号密码
docker exec -it docker-mailserver setup email update [email protected]
# 查看账号
docker exec -it docker-mailserver setup email list
# 查看别名
docker exec -it docker-mailserver setup alias list
# 给管理员地址配置收件去向:若已经存在,先检查,不重复添加
docker exec -it docker-mailserver setup alias add [email protected] [email protected]
账号保存在持久化配置目录中的 postfix-accounts.cf,别名在 postfix-virtual.cf。上面的别名将发给 [email protected] 的邮件交给 [email protected],已有别名时跳过添加。
客户端设置
| 项目 | 设置 |
|---|---|
| 登录用户名 | [email protected],完整邮箱地址 |
| 接收服务器 | mail.f0rget.me |
| 接收端口 | 993,SSL/TLS |
| 发送服务器 | mail.f0rget.me |
| 发送端口 | 587,STARTTLS;或 465,SSL/TLS |
| SMTP 验证 | 开启,使用邮箱账号密码 |
我这里以 Gmail 为例子
接收配置:

发送配置:

收发测试
在 Thunderbird/Gmail 等客户端中配置上述账号。从外部邮箱给 [email protected] 发一封信,确认可以收到,再从客户端回复,检查外部邮箱是否收到。
接着查看接收方原始邮件头中的 Authentication-Results,确认 SPF、DKIM 和 DMARC 的结果。这里还没有部署网页端,收发测试直接通过邮件客户端完成。
docker exec docker-mailserver postqueue -p
docker logs --since 30m docker-mailserver 2>&1 \
| grep -Ei 'status=(sent|deferred|bounced)|sasl|authentication failed|warning|error' \
| tail -100
docker exec docker-mailserver setup fail2ban status
docker exec docker-mailserver postconf smtpd_relay_restrictions
客户端通过邮箱账号认证发信,保持现有中继限制即可。下面就是测试通过的

网页邮箱(可选)
客户端能够正常收发后,邮件服务就可以使用了。需要网页入口时,再部署 Roundcube 和 Nginx。
网页域名与证书
在 Cloudflare 添加网页入口,下面仍使用公网 IP 占位:
| 类型 | 名称 | 内容 |
|---|---|---|
| A | webmail | 203.0.113.10 |
这里按仅 DNS 配置,然后申请网页证书:
/snap/bin/certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /opt/docker-mailserver/secrets/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
--cert-name webmail.f0rget.me -d webmail.f0rget.me
/snap/bin/certbot certificates
Roundcube
创建共享网络一次即可:
docker network inspect webmail-net >/dev/null 2>&1 || docker network create webmail-net
docker network inspect webmail-net --format '{{.Name}} {{.Driver}}'
在 /opt/roundcube 下创建 compose.yaml,并将 Roundcube 加入 webmail-net:
services:
roundcube:
image: roundcube/roundcubemail:latest
container_name: roundcube
restart: unless-stopped
environment:
ROUNDCUBEMAIL_DB_TYPE: sqlite
ROUNDCUBEMAIL_DB_DIR: /var/roundcube/db
ROUNDCUBEMAIL_DEFAULT_HOST: ssl://mail.f0rget.me
ROUNDCUBEMAIL_DEFAULT_PORT: 993
ROUNDCUBEMAIL_SMTP_SERVER: tls://mail.f0rget.me
ROUNDCUBEMAIL_SMTP_PORT: 587
ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE: 20M
volumes:
- ./www:/var/www/html
- ./db:/var/roundcube/db
networks:
- webmail-net
networks:
webmail-net:
external: true
ssl:// 表示连接开始就使用 TLS,tls:// 表示 STARTTLS,因此这里分别搭配 993 和 587。20M 只设置网页上传限制,邮件能发送多大还要看 Postfix 和收件方的上限,附件编码后也会变大。参数说明
docker compose -f /opt/roundcube/compose.yaml config -q
docker compose -f /opt/roundcube/compose.yaml up -d
docker logs --tail 80 roundcube
启动时会在 www 写入应用文件,在 db 创建 SQLite 数据库。容器没有 ports 映射,网页等 Nginx 配置完成后再访问。后续迁移将这两个目录一起带走。
Nginx 部署
使用 nginx:stable-alpine 镜像,配置目录挂载为 ./nginx/conf.d。
Compose 配置
保存 /opt/webmail-proxy/compose.yaml:
services:
nginx:
image: nginx:stable-alpine
container_name: webmail-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
networks:
- webmail-net
networks:
webmail-net:
external: true
反向代理配置
保存 /opt/webmail-proxy/nginx/conf.d/webmail.conf:
server {
listen 80;
server_name webmail.f0rget.me;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name webmail.f0rget.me;
ssl_certificate /etc/letsencrypt/live/webmail.f0rget.me/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/webmail.f0rget.me/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 20M;
location / {
proxy_pass http://roundcube:80;
proxy_http_version 1.1;
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 https;
proxy_set_header X-Forwarded-Port 443;
proxy_read_timeout 300s;
}
}
保存后启动 Nginx,检查配置和网页访问:
docker compose -f /opt/webmail-proxy/compose.yaml config -q
docker compose -f /opt/webmail-proxy/compose.yaml up -d
docker exec webmail-nginx nginx -t
curl -I http://webmail.f0rget.me
curl -I https://webmail.f0rget.me
访问 HTTP 会跳转到 HTTPS,随后进入 Roundcube 登录页。这次测试 HTTPS 返回了 HTTP/1.1 200 OK,接下来使用邮箱账号登录,测试收发即可。


证书续期

Certbot 续期后会更新磁盘上的证书,还需要让 Nginx 和 DMS 重新加载。这里用 deploy hook,证书更新成功后自动执行相应命令。
这次两张证书都通过了 certbot renew --dry-run --run-deploy-hooks 测试。输出里出现过 Nginx 的 notice 和 Docker 的 Restarting/Started 信息,虽然写在 stderr,最终模拟续期仍然成功。
续期脚本
创建 /etc/letsencrypt/renewal-hooks/deploy/restart-mailserver.sh,内容按实际脚本保留:
#!/bin/sh
set -eu
# 只处理邮件服务器的证书
case " ${RENEWED_DOMAINS:-} " in
*" mail.f0rget.me "*) ;;
*) exit 0 ;;
esac
# 证书续期成功后,重启邮件服务以加载新证书
/usr/bin/docker compose \
-f /opt/docker-mailserver/compose.yaml \
restart mailserver
这个脚本根据 RENEWED_DOMAINS 判断续期证书是否包含 mail.f0rget.me,匹配后重启 DMS。
部署了网页端时,再创建 /etc/letsencrypt/renewal-hooks/deploy/reload-webmail-nginx.sh:
#!/bin/sh
if docker ps --format '{{.Names}}' | grep -qx webmail-nginx; then
docker exec webmail-nginx nginx -s reload
fi
网页脚本不区分是哪张证书续期,只要 webmail-nginx 正在运行就 reload。所以邮件证书续期时,也可能看到 Nginx 的重载输出,这是当前脚本的行为。Certbot hook 说明
保存后赋予执行权限,并检查语法:
chmod 700 /etc/letsencrypt/renewal-hooks/deploy/restart-mailserver.sh
sh -n /etc/letsencrypt/renewal-hooks/deploy/restart-mailserver.sh
# 部署了网页端时再执行下面两条
chmod 700 /etc/letsencrypt/renewal-hooks/deploy/reload-webmail-nginx.sh
sh -n /etc/letsencrypt/renewal-hooks/deploy/reload-webmail-nginx.sh
续期测试
snap services certbot
/snap/bin/certbot renew --dry-run --run-deploy-hooks
docker inspect --format '{{.State.Status}}' docker-mailserver
只部署 DMS 时,续期邮件证书并执行邮件脚本即可。部署网页端后再检查:
docker exec webmail-nginx nginx -t
curl -I https://webmail.f0rget.me
snap services certbot 显示 enabled inactive timer-activated,表示等待定时触发,不需要再添加一份 cron。测试中的 --run-deploy-hooks 会实际执行脚本,因此会短暂重启 DMS。

证书检查
openssl s_client -connect mail.f0rget.me:993 \
-servername mail.f0rget.me -verify_hostname mail.f0rget.me \
-verify_return_error </dev/null
openssl s_client -starttls smtp -connect mail.f0rget.me:587 \
-servername mail.f0rget.me -verify_hostname mail.f0rget.me \
-verify_return_error </dev/null
openssl s_client -connect mail.f0rget.me:465 \
-servername mail.f0rget.me -verify_hostname mail.f0rget.me \
-verify_return_error </dev/null
# 部署网页端后再检查 443
openssl s_client -connect webmail.f0rget.me:443 \
-servername webmail.f0rget.me -verify_hostname webmail.f0rget.me \
-verify_return_error </dev/null
查看输出中的证书域名、有效期和验证结果。这里的 TLS 用于连接加密,邮件正文的端到端加密需要另外配置。
备份与迁移

备份目录
完整部署备份下面四个目录。只部署 DMS 时,保存 /opt/docker-mailserver 和 /etc/letsencrypt 即可,下面涉及 Roundcube、Nginx 的命令也跳过。
| 目录 | 内容 |
|---|---|
/opt/docker-mailserver |
Compose、env、邮件、账号、别名、队列、Rspamd 配置和 DKIM 私钥、Cloudflare 凭据 |
/opt/roundcube |
Compose、应用配置、SQLite 数据库、联系人和偏好 |
/opt/webmail-proxy |
Nginx Compose 和反向代理配置 |
/etc/letsencrypt |
证书、私钥、ACME 账号、续期配置和脚本 |
如果另有挂载目录或 Docker 数据卷,也一并备份。/etc/letsencrypt/live 中的证书是符号链接,实际文件位于 archive,因此保存整个 /etc/letsencrypt。
打包备份
备份前暂时停止服务,避免复制过程中邮件队列或 SQLite 数据库仍在写入。下面操作会短暂停止收发和网页访问。
umask 077
BACKUP_FILE="/root/mail-stack-$(date +%F-%H%M%S).tar.gz"
# 暂停证书续期和容器
snap stop certbot.renew
docker compose -f /opt/webmail-proxy/compose.yaml stop
docker compose -f /opt/roundcube/compose.yaml stop
docker compose -f /opt/docker-mailserver/compose.yaml stop
# 保存四个目录,保留数值所有权及文件权限
tar --acls --xattrs --numeric-owner -czpf "$BACKUP_FILE" \
-C / opt/docker-mailserver opt/roundcube opt/webmail-proxy etc/letsencrypt
sha256sum "$BACKUP_FILE" > "$BACKUP_FILE.sha256"
# 日常备份完成后启动服务
docker compose -f /opt/docker-mailserver/compose.yaml up -d
docker compose -f /opt/roundcube/compose.yaml up -d
docker compose -f /opt/webmail-proxy/compose.yaml up -d
snap start certbot.renew
打包成功后将备份传到安全位置,备份包含邮件、账号和私钥,离机保存时加密。打包失败也要恢复服务;如果接下来直接迁移,最后一次备份后旧机保持停止。
新服务器恢复
新机先安装 Docker、Compose、Snap Certbot 和 Cloudflare 插件,确认 TCP 25 可出入站、PTR 可设置。镜像尽量沿用旧机版本。
将备份包和校验文件传到新机。在尚未部署邮箱的新服务器上执行,文件名替换为实际名称:
cd /root
sha256sum -c mail-stack-YYYY-MM-DD-HHMMSS.tar.gz.sha256
tar --acls --xattrs --numeric-owner \
-xzpf mail-stack-YYYY-MM-DD-HHMMSS.tar.gz -C /
docker network create webmail-net
docker compose -f /opt/docker-mailserver/compose.yaml up -d
docker compose -f /opt/roundcube/compose.yaml up -d
docker compose -f /opt/webmail-proxy/compose.yaml up -d
webmail-net 已存在时跳过创建。解包时保留原数值所有权,env 保留原值;不需要重新创建邮箱或生成 DKIM 密钥。证书域名不变且仍在有效期内时,可以继续使用。
DNS 切换
迁移前可提前降低 DNS TTL,等待原 TTL 过期。恢复完成后修改以下记录:
| 项目 | 操作 |
|---|---|
mail、webmail 的 A / AAAA |
改为新服务器对应地址 |
| PTR | 在新 VPS 商家后台设置为 mail.f0rget.me |
| MX | 主机名不变时保持原记录 |
| SPF | 当前 v=spf1 mx -all 随 MX 地址变化;若写了固定 IP,则更新 IP |
| DKIM、DMARC | 保留原密钥、域名和报告地址时,无需更改 |
最后验证网页登录、旧邮件和联系人、外部双向收发,再测试自动续期:
/snap/bin/certbot renew --dry-run --run-deploy-hooks
snap services certbot
DNS 缓存更新期间,部分发件服务器仍可能连接旧 IP。旧机保持停止,等待对方重试,并保留数据一段时间。若新机已经收到新邮件,回滚前先处理新增邮件和队列,避免数据分散。