简介:Mailcow 是基于 Docker 容器化技术的开源邮件服务器解决方案,集成 SMTP、IMAP、POP3 与 Webmail 服务,内置反垃圾、反病毒、DKIM/DMARC/SPF 多重安全校验,适合需要自建邮件系统的系统管理员、运维工程师及中小企业 IT 人员快速搭建安全可控的邮件环境。压缩包含 2000 个文件,整体约 10.98MB,以 1515 个 PHP 源码文件为主体,辅以 YML 容器编排配置、Shell 部署初始化脚本、JSON/XML 配置、Markdown 说明文档及 CSS/JS 前端资源,覆盖容器编排、业务逻辑到界面展示的完整链路。资源中提供 Docker Compose 管理方式的配置示例、安全策略文件、第三方依赖与密钥证书类文件,可对照理解 Mailcow 各组件的协作方式,也可作为二次开发或私有化部署的参考基线。目前已有 200 人学习下载,适合邮件系统原理梳理和容器化运维实践。
1. 自己搭邮件服务器,难点从来不是装软件
很多团队在评估 Mailcow 之前,已经在虚拟机里手工拼过一轮 Postfix、Dovecot 和 Webmail。装完才发现,真正的门槛是三个:一是公网 25 端口被限、PTR 记录缺失导致发信被拒收;二是 DKIM、DMARC、SPF 三条 DNS 记录没配齐,进了对方垃圾箱还没察觉;三是日志散落在十几个进程里,出了问题只能靠猜。Mailcow 是一套把 Postfix、Dovecot、Rspamd、SOGo、ClamAV 等组件用 Docker Compose 串起来、自带统一 Web 管理后台的开源邮件服务器解决方案,它把这些零散的坑提前替你踩平了一大半。适合手里有一个域名和至少 4G 内存的小团队、多域名管理的运维,以及想从老旧的商业邮件系统迁移出来、又不想把所有事情都外包出去的从业者。这篇文章我用一线部署的视角,把从 DNS 准备、容器启动到反向代理避坑的完整过程拆给你看。
2. Mailcow 的架构逻辑:为什么用容器把邮件系统拆成十几个服务
初次打开docker compose ps看到十几个容器,很多人第一反应是“太重了”。但邮件系统本身就是一个长链路:外部通过 SMTP 25 端口进来,Postfix 做收信路由;用户用 IMAP 从 Dovecot 拉信;收发过程中每一封邮件都要经过 Rspamd 打分,附件要过 ClamAV 查毒;Webmail 界面 SOGo 又要读写同一个账号体系。把这些放在一台虚拟机里手工配置,任何一个组件升级都会牵连另外几个,这才是真正重的部分。
2.1 Postfix、Dovecot、Rspamd、SOGo 各管哪一段
单独看 Mailcow 的镜像列表会觉得很乱,但按“信在哪、谁在管”这条线梳理,立刻就清楚了:
- Postfix:负责 SMTP 收信和发信,邮件进入系统的第一道门。外部服务器把信投递到你的 MX 记录指定的主机,实际落地的进程就是 Postfix。
- Dovecot:负责 IMAP/POP3 服务,邮件进入用户邮箱后由它提供读取接口,同时处理用户密码验证和邮箱目录锁定。
- Rspamd:负责每一封邮件的垃圾评分和 DKIM 签名。Mailcow 里 DKIM 私钥的签名动作也由 Rspamd 承担,而不是 Postfix。
- SOGo:Webmail 客户端,提供网页收信、日历、联系人同步,支持 ActiveSync。
- ClamAV:附件病毒扫描,默认开启,在内存不足时是第一个应该被关闭的组件。
- MySQL:保存账号、域名、别名等元数据;邮件正文则存在 Dovecot 的卷目录里。
- Redis:缓存 Rspamd 的统计数据、SOGo 会话等热数据。
这套分工里我最看重的设计是:Rspamd 在 Postfix 的 smtpd 阶段就直接参与决策,垃圾评分发生在邮件落盘之前。高分的邮件可以直接被拒收、丢进隔离区或加标题,而不是先进入 Dovecot 再由客户端侧规则二次处理。实践里,Rspamd 的学习数据随时间累积,对特定业务域的误判率会越来越低,这是手工配置 SpamAssassin 很难达到的体验。
从运维角度看,容器化最大的好处是“单点可替换”。比如你觉得 ClamAV 在低配机器上太吃内存,可以只停掉clamd-mailcow容器,邮件系统其余部分完全不受影响。这比在传统 Linux 上systemctl stop clamd之后再处理一堆依赖关系要干净得多。当然,容器化也有代价:数据卷和容器生命周期一旦搞混,升级时会出现数据不丢但配置丢失的怪问题,这一点放在后面的避坑章节专门讲。
2.2 按需裁剪:内存只有 4G 时关掉哪些容器
Mailcow 官方建议的最小内存是 4GB,实际操作中 4G 跑全套会比较紧张。我一般会在docker-compose.override.yml里做减法,这个文件不会影响后续升级时主 compose 文件的改动:
services: clamd-mailcow: image: mailcow/clamd:1.10 profiles: ["disabled"] olefy-mailcow: profiles: ["disabled"] soapy-mailcow: profiles: ["disabled"]用profiles禁用服务后,docker compose up -d默认不会启动它们。这里逻辑上要注意:Mailcow 的主 compose 文件是由generate_config.sh生成的,升级时不会被整体覆盖,但docker compose在读取时会同时加载 override 文件里的合并配置。所以你把服务加进profiles之后,还需要执行docker compose up -d --remove-orphans让已经运行的旧容器停下来。
为什么先关 ClamAV 而不是关 SOGo?因为 SOGo 承担了邮箱用户唯一能看到的“界面”,关了它,团队里非技术成员就回到了纯客户端收信的方式。而 ClamAV 只影响附件病毒扫描,4G 内存的机器上关掉它省出的 1.5G 能让 MySQL 和 Rspamd 从容很多。如果安全要求不允许关,那么最低限度也要给系统加 swap,否则碰到大附件并发扫描时直接触发 OOM,Docker 守护进程会连坐杀掉其它容器。
2.3 数据落在哪里:容器卷与备份边界
Mailcow 的数据主要在三个地方:MySQL 容器里的mailcow数据库、Dovecot 容器挂载的邮件存储卷、Redis 里的缓存数据。前两个决定你的邮件系统能不能恢复,Redis 丢了只是重新学习反垃圾模型,问题不大。
邮件存储卷默认以vmail-mailcow这种命名卷的形式存在,用docker volume ls能看到。很多人升级时执行了惯用的docker compose down && docker compose up -d,这不会删除命名卷,数据是安全的;但如果有人手贱执行了docker compose down -v,那么整个邮件存储和数据库会一起消失,没有任何后悔药。我的习惯是部署完成后立刻在mailcow.conf的备份设置里打开自动备份,同时每周对一个特定目录做快照。备份不是等到要迁移时才想起的,是在第一天就设好的。
3. 从零部署 Mailcow:DNS 先配好,再跑容器
部署 Mailcow 本身只需要三个大步骤:准备 DNS、生成配置、启动容器。但我见过太多人把步骤搞反——先装好套件,再回头发现域名解析没生效、PTR 没办法立刻配上,结果容器反复重启还以为是软件 bug。所以我把 DNS 的准备放在最前面,这一部分花费的时间比容器启动时间还长。
3.1 部署前的域名与 DNS 准备:MX、A、PTR 一个都不能少
假设你的域名是example.com,邮件服务器主机名规划为mail.example.com。以下是必须在域名 DNS 管理后台配好的记录,我用表格列出,照着填就行:
| 记录类型 | 主机名 | 内容 | 作用 |
|---|---|---|---|
| A | 服务器公网 IP | 让mail.example.com可以解析到服务器 | |
| MX | @ | mail.example.com优先级 10 | 告诉别的邮件服务器,投递入口在这 |
| PTR | 服务器 IP | mail.example.com | 反向解析,很多大邮箱不发 PTR 直接拒信 |
| TXT | @ | v=spf1 mx a ip4:服务器IP ~all | 声明哪些服务器有资格代发该域名 |
| TXT | DKIM 公钥(后面导出的长串) | 邮件签名校验,防伪造 | |
| TXT | _dmarc | v=DMARC1; p=none; rua=mailto:admin@example.com | 接收方反馈未通过校验的邮件来源 |
这里最容易漏的是 PTR。它能控制权不在域名服务商,而在给你分配公网 IP 的机房或云厂商。绝大多数云控制台里都有“反向解析”或“PTR 记录”入口,你得提交工单或直接在控制台里配置,把 IP 反解到mail.example.com。PTR、A、MX 三者不一致是对方服务器判定垃圾邮件的最直接依据,这个不解决,后面 SPF、DKIM 配得再完美,你的信也大概率进回收站。
3.2 最小安装命令:下载套件并生成 mailcow.conf
DNS 配置开始生效后(可以用dig MX example.com确认),就可以在服务器上拉取部署套件。官方仓库是 GitHub 上的mailcow/mailcow-dockerized,国内服务器拉取如果速度慢,可以把github.com换成镜像地址,但我不建议改 —— 邮件系统需要长期跟随更新,还是直接用官方源,一次性拉干净:
apt update && apt install -y git curl git clone https://github.com/mailcow/mailcow-dockerized /opt/mailcow-dockerized cd /opt/mailcow-dockerized ./generate_config.shgenerate_config.sh是交互式脚本,会问你几个问题:MAILCOW_HOSTNAME要填mail.example.com,这个值必须和 MX 记录的 A 记录解析到同一台主机;HTTP_PORT和HTTPS_PORT默认分别是 8080 和 8443,如果你不想让邮件管理后台占用标准 80/443,保持默认即可,后面用 Nginx 反代过去。脚本运行完会生成mailcow.conf,所有后续自定义参数都在这个文件里改。
3.3 启动与健康检查:看日志确认各容器就绪
配置生成完毕后,首次启动要把镜像全部拉下来。这一步耗时取决于网络环境,建议在 screen 或 tmux 里跑:
docker compose pull docker compose up -d docker compose psdocker compose ps输出的 STATE 列在启动初期会显示starting,这是正常的。等到所有容器变成healthy大概需要 1 到 3 分钟。如果卡在某个容器上,不要反复up -d,先看对应容器日志。比如 MySQL 起不来,常见原因是/opt/mailcow-dockerized所在磁盘空间不足,或者之前装过别的 MySQL 留下了端口冲突:
docker compose logs mysql-mailcow --tail 200日志里出现ready for connections才说明数据库初始化完成。接着检查nginx-mailcow容器是否监听在 8080 端口,然后打开http://服务器IP:8080,看到登录页就说明 Web 管理界面已经起来了。默认管理员账号是admin,初始密码在生成配置时终端会打印,登录后第一件事就是改掉它。
3.4 把 SPF、DKIM、DMARC 写进 DNS:发信前置条件
容器全部启动后,进入管理后台的Configuration -> DKIM页面,选择example.com,会看到一把 DKIM 公钥。这个值是一个很长的 TXT 记录,主机名通常是dkim._domainkey.example.com。复制完整值的时候要小心:有些 DNS 服务商对单条 TXT 的长度有限制(超过 255 字符会分成多段),而 DKIM 记录恰好就长。关于分段导致的校验失败问题,我在避坑章节会展开说。
SPF 记录按前面表格里的写法,直接加在域名根上。DMARC 可以先从p=none开始,跑两周后看rua邮箱收到哪些来源没通过校验,再逐步收紧为p=quarantine或p=reject。这一步别跳,DMARC 报告是了解自己域名是否被仿冒的唯一廉价入口。三条记录配置完成后,用dig TXT dkim._domainkey.example.com和dig TXT example.com能正确解析,就可以用任意外部邮箱给这个域名发一封测试邮件,再回复一封来验证双向收发。
4. Web 管理后台的日常:域名、邮箱账号、别名与反垃圾策略
Mailcow 的 Web 管理后台和普通邮件系统的最大区别是:管理员不需要手工编辑 Postfix 的 virtual 文件,也不需要 SSH 到服务器敲 useradd。域名、邮箱、别名、配额全部在 UI 里点出来,后端落到 MySQL。但我建议运维至少要掌握两条终端命令,因为一旦 UI 因为反代配置问题打不开、或者要做批量导入,终端是唯一的逃生通道。
4.1 域名与邮箱账号的创建流程
登录管理后台后,进入Mailboxes -> Domains,先添加域名example.com。这里有两个选项容易被忽略:Active决定域名是否可用,Relayhost选项用于把该域名的所有来信转发到另一个 SMTP 服务器。如果只是内部测试,可以不填 Relayhost;如果公司有历史邮件网关,想先跑一段灰度,可以把 Relayhost 填成旧的网关地址。
域名创建完成后,在同一个页面切到Mailboxes,点Add mailbox创建邮箱。用户名格式是完整邮箱地址,密码最短 8 位且需要包含大小写字母。配额默认给 1GB,按团队实际使用量调整。创建完成后立刻用这个邮箱通过客户端登录一次,确认 Dovecot 的 IMAP 服务和密码验证链路正常。
批量创建账号时,我一般会直接操作数据库,而不是在 UI 里一个个点。Mailcow 用 API 也可以,但内网环境直接进容器更直观:
docker compose exec mysql-mailcow mysql -umailcow -p mailcow进入 SQL 提示符后,你可以查询mailbox表结构,确认字段后批量插入。但这里要奉劝一句:不要绕过后台直接写mailbox表来创建邮箱,因为 Mailcow 的密码存储格式不是 md5 也不是 bcrypt 的纯哈希,它绑定了每个域名的特殊密钥。批量插入后如果登录不上,又查不出原因,非常耽误时间。我把查询表结构当成排查手段,创建账号还是用 UI。
4.2 别名与邮件列表:让团队协作更顺手的两个配置
别名是邮件系统里使用频率最高的功能。比如公司有support@example.com,需要同时发给三个客服同事,不需要创建三个邮箱,只要在Aliases页面新增一个别名,把目标设成三个逗号分隔的邮箱地址即可。别名本身不占配额,也不会产生独立收件箱,所有来信都会投递到目标邮箱,回复时发件人显示为其中某个具体邮箱。
邮件列表的差异在于每个成员是独立收件人,可以单独退订或收到不同的过滤规则。Mailcow 的邮件列表也走别名机制,但使用Lists页面管理,成员地址作为独立条目维护。小团队用别名就够,对外发布邮件列表才需要单独开列表账号。
配置别名时容易遇到一个坑:如果目标邮箱里包含外部域名的地址,例如someone@othercompany.com,Mailcow 默认允许这样设置,但发过来的外部邮件会被当成转发邮件处理,经过 Postfix 的 relay 逻辑,这可能和你的Relayhost配置互相影响。碰到投递延迟,第一反应去docker compose logs postfix-mailcow看是否存在relay access denied,而不是猜 DNS 问题。
4.3 反垃圾与 Rspamd:从黑名单到按需放宽阈值
Rspamd 在 Mailcow 里默认已经打开,且接入 Postfix 的过滤流程。管理后台里Configuration -> Rspamd能实时看到每封邮件的评分明细,包括命中哪些规则、各得多少分。这个页面是我日常运维看的最多的一个地方,判断一封邮件是垃圾还是误判,靠历史数据比靠直觉准。
如果某个外部客户频繁给公司发信,但因为对方 IP 没有反解或使用了动态 IP,被 Rspamd 一路压着打,不要急着全局降低阈值。Rspamd 的 UI 里有一个“自动白名单”机制,你可以把这个来源域名加入本地白名单,让它的来信跳过垃圾评分。注意白名单粒度:加域名比加 IP 更合适,因为对方 IP 变了自动白名单就失效,而域名在邮件头的From字段里是稳定标识。
邮件进入隔离区后,普通用户和管理员在 Webmail 的Junk文件夹里能看到被丢弃的邮件。这里有个运营习惯:每周让人看一眼隔离区里有没有真实业务邮件。Rspamd 的模型本质是一个统计分类器,它需要人工纠偏来学习。只依赖默认规则,时间长了误判率一定会上升。
5. 邮件系统避坑清单:从端口封禁到容器半夜重启
跑通一套 Mailcow 只意味着搭建完成,离“稳定生产”还差一段距离。我在给客户做邮件系统的时候踩过很多次坑,下面这五条是高频中的高频,每一条都按现象、原因、解决三段来写,直接对标你上线后可能遇到的第一批问题。
5.1 发信被对方拒收:25 端口被封和 PTR 缺失
现象是邮件在队列里反复重试,日志里出现connect to gmail-smtp-in.l.google.com:25: Connection timed out或550-Please turn on SMTP Authentication。前者的直接原因是服务器出站的 25 端口被运营商封锁,这是用虚拟机邮件服务器最常见的现实问题;后者则是对方服务器认为你的服务器不可信,通常伴随着 PTR 记录不存在。
解决分两步。第一步,确认机房是否放行 25 端口,临时用telnet 某大型邮件服务器 25测试,如果连 25 端口都不通,联系机房开通出站 SMTP 权限。如果机房明确不能开,那就要换方案:把 Mailcow 的投递方式改成通过第三方 SMTP 服务中继,在mailcow.conf里启用relayhost配置。第二步,补齐 PTR 记录,这个前面说过必须在云控制台操作。我只见过 PTR 缺失导致被拒收的情况,没见过补齐之后还被拒收的情况。
5.2 DKIM 校验失败:TXT 记录过长与符号转义
现象是发出的邮件能够送达,但外部邮箱显示“通过 DKIM 校验失败”,或者邮件直接进垃圾箱。查 Rspamd 的日志能看到dkim: fail (bad signature)。原因多半不是 Mailcow 这边签名错了,而是 DNS 里的 DKIM TXT 记录被截断或转义错误。
DKIM 公钥是一整串约 200 多字符的文本,某些 DNS 服务商在后台自动把它拆成两段,每段分别加上双引号,而解析方会把两段合并时在多出的引号处解析失败。解决方法是回到 DNS 后台,确认 TXT 值是以v=DKIM1; k=rsa; p=开头的一段完整内容,中间不能有空格和换行;如果服务商强制分段,请把两段合并为一条记录再提交。除了这些,还有另一种容易忽略的场景:你从管理后台复制公钥时,末位多复制了一个空格,肉眼根本看不出来。用dig TXT dkim._domainkey.example.com查看解析结果,把输出和后台显示的值逐字符比对,才是最快的排查方式。
5.3 内存不足导致容器反复重启:ClamAV 与 OOM
现象是邮件服务运行两三天后,某个容器变成Restarting,docker compose logs里能看到Killed或Out of memory。最常背锅的是 ClamAV,它启动时加载病毒库要吃掉 1G 以上内存,如果当时 MySQL 正好在跑大查询,内核就会随机杀掉进程。这个坑在 4G 内存的虚拟机上尤其常见。
解决方式有两个层次。短期方案是把 ClamAV 容器禁用(按 2.2 节的方式加profiles),或者给它设置内存上限,让它 OOM 时只杀自己而不是连累 Redis。长期方案是给服务器增加内存或配置 swap,我一般会在/etc/fstab里加一个 4G 的 swapfile,虽然性能不如物理内存,但足以避免邮件系统半夜因瞬时内存波动而整个瘫痪。
5.4 反向代理后面登录异常:X-Forwarded-* 没配
现象是 Mailcow 跑在 Nginx 反代后面,外部用 HTTPS 访问正常,但登录后页面不断跳回登录页,或者在 SOGo 里点击邮件链接时 URL 用的是 HTTP 和奇怪端口。原因是反代没有正确传递X-Forwarded-Proto,Mailcow 的判断机制拿到的还是后端 HTTP 协议的http://,于是认为当前会话不安全,拒绝写入 cookie。
解决方式是在你的 Nginx 反代配置里显式增加两条 header:X-Forwarded-Proto $scheme和X-Forwarded-Host $host。Mailcow 的mailcow.conf里也有一项可选的HTTP(S)强制 HTTPS 跳转开关,如果反代已经承担了 TLS 终结,建议把容器自身的 HTTPS 跳转关掉,避免出现“反代到容器 8080,容器又 302 回 8443”的死循环。这个坑排查起来最费时间,因为它涉及容器内外两层协议判断,我建议反代上线前,先直接访问一次容器原生端口,确认源站正常,再对比反代访问,就能快速定位问题出在反代还是应用。
5.5 备份与还原的坑:MySQL 一致性
现象是服务器出现故障后,从备份恢复时邮件账号能登录,但邮件列表缺失或某些邮箱密码集体失效。原因大多是备份时只备份了 docker 卷文件,没有保证 MySQL 的一致性。Docker 卷的复制操作不是数据库的在线备份,MySQL 在写入过程中文件被复制走,得到的就是损坏或半截的数据。
正确做法是对数据库单独做逻辑备份。可以写一个 cron 任务,每天凌晨用mysqldump导出整个mailcow库,再把 dump 文件和邮件存储目录一起同步到异地。恢复流程也建议每季度演练一次:在一个干净的虚拟机上把 Mailcow 跑起来,导入 dump,再挂载邮件卷,确认能登录并看到历史邮件。邮件系统丢了账号可以重建,但丢了几年的业务往来邮件,对任何公司来说都是不可接受的损失。
6. 进阶调优:把 Rspamd 的评分阈值调到符合你的业务场景
Mailcow 默认的 Rspamd 策略对个人用户比较友好,但对业务邮件频繁、且经常收到陌生客户来信的公司来说,默认阈值会误杀不少首封咨询邮件。Rspamd 默认的grow_factor和评分规则可以在 UI 的 Rspamd 设置页里看到,但我更推荐直接进入容器查看完整配置,因为 UI 暴露的只是冰山一角:
docker compose exec rspamd rspamadm configdump | grep -A 20 "settings"这段命令会把 Rspamd 当前生效的所有配置以 dump 形式打印出来,settings区块下的symbol列表就是每个规则对应的加分项。你可以看到某个规则加了 15 分,而你的阈值设在 15 分,意味着命中一条就会判定垃圾。调整方式是在docker-compose.override.yml里给rspamd-mailcow服务挂载一份自定义配置文件,而不是直接在容器里改,因为容器重建会把改动全部抹掉。
针对“首封陌生客户邮件更容易被误杀”的问题,我通常的处理是:把GREYLIST这个符号的加分降低,同时把USER_IN_SHORTLIST这类基于收件人通讯录的规则加分提高。这样对完全陌生的邮件保持警惕,但对已经有过往来的地址会立刻放宽。调整后至少观察一周,每天看一眼 Rspamd UI 里的评分分布,如果垃圾邮件进隔离区的比例没有明显上升,就可以把改动固化下来。
最后还有一个容易被忽略的细节:投递日志。docker compose logs postfix-mailcow里每一行成功投递都以status=sent (250 2.0.0 OK)结尾。我养成的习惯是写一个简单的 cron,每天扫一遍日志里的status=bounced,统计退信数量,超过阈值就报警。这个习惯帮我提前发现过两次因 DNS 记录被服务商悄悄改动而导致的整域退信。邮件服务器上了生产,日常维护拼的不是花哨操作,是把这些细节一点点盯住。希望今天的这些部署思路和踩坑记录能帮你少走一段弯路。
本文还有配套的精品资源,点击获取