跳至主要内容
五折优惠 全部方案,限时优惠。起价 $2.48/mo
14 min left
Web 与商业应用

如何用 Listmonk 自托管你的邮件通讯

C 作者 Chike 14 分钟阅读
How to self-host your newsletter with Listmonk on a VPS: Docker Compose, SMTP relay, and SPF/DKIM/DMARC deliverability.

通过自托管,Listmonk 跑在你本就在付费的 VPS 上。发信成本就是你的 SMTP 中继按每千封邮件收取的费用。订阅者数量不会改变这两个数字中的任何一个。这正是结构性的转变,让自托管在你的需求超出托管版免费额度之后值得花那点搭建时间。

Listmonk 是一款基于 Go 的开源邮件通讯管理工具。只需一台 VPS 加一个 SMTP 中继账号的成本,你就能拥有无限的订阅者、列表和发信活动。在你敲第一条命令之前,有一点必须搞清楚:Listmonk 处理除了实际发信之外的一切。你的邮件落入收件箱还是垃圾邮件箱,取决于你配置的 SMTP 中继和你在发信域名上设置的 DNS 记录。

本指南涵盖什么

  • 用 Docker Compose 在带 HTTPS 的 Nginx(或 Caddy)反向代理后面部署 Listmonk 和 PostgreSQL
  • 根据你的发信量和预算选对 SMTP 中继(Amazon SES、Postmark、Brevo 或其他)
  • 在你的发信域名上配置 SPF、DKIM 和 DMARC
  • 避开四种上线后才暴露、且往往不会报出明确错误的故障模式
  • 预计耗时:如果你已经备好 VPS 和域名,30 分钟。
  • 不在范围内:滴灌自动化、事务性邮件、多实例部署(见 FAQ)

什么情况下 Listmonk 是错的工具

Listmonk 是某种特定场景下的正确答案。如果你的场景不同,就会有更好的答案。

每月发信量低于约 1 万封。 在这个规模下,Brevo 或 Mailchimp 的托管版免费额度,全算下来可能比一台 VPS 加一个 SMTP 中继更便宜。自托管要等你跨过这个区间才开始划算。部署之前,先拿你真实的订阅者数量和发信频率把账算一遍。

非技术团队。 对于不在终端里工作的人,Mailchimp 和 Brevo 的界面确实更好用。Listmonk 默认团队里有人能 SSH 进服务器、看懂 Docker 日志、判断 DNS 是否生效。如果没有这样的人,托管服务才是正确选择。

需要自动化工作流。 Listmonk 发送发信活动。它不支持滴灌序列、行为触发邮件,也没有可视化工作流编排器。如果你需要这些,就运行 Mautic 或者把 Listmonk 接到 n8n 上来承担自动化这一层。

受 GDPR 约束的订阅者列表。 如果你的订阅者主要在欧盟,或你的列表受 GDPR 数据驻留规则约束,就把 Listmonk 部署在欧洲数据中心。我们提供符合欧盟驻留要求的法兰克福和伦敦机房。

开始之前你需要准备什么

Listmonk 加 PostgreSQL 再加一份中等强度的队列负载,至少需要 2 GB RAM 打底。4 GB 是舒适的生产环境目标。

硬件。 对于每月发信量低于 5 万封的个人列表,一台配 2 vCPU、4 GB RAM 和 120 GB NVMe 存储的 VPS 就够用。每月 20 万封以上、还在增长的列表需要 4 vCPU 和 8 GB RAM。我们这套 Compose 部署跑在法兰克福一台 4 GB 的 VPS 上。如果可以,挑一个离你订阅者近的机房。发信延迟无所谓;管理后台的响应速度才重要。

域名。 一个通过 A 记录指向你 VPS 的域名。用一个子域名作管理界面,例如 mail.example.com。发信域名和管理子域名可以是同一个主域名。

SMTP 中继账号。 先别急着创建。中继的选择是本指南中最关键的决定,而它取决于你的发信量。先跳到“选择你的 SMTP 中继”那一节,挑一个服务商,再回到这里,手上备好 SMTP 主机、端口、用户名和密码。

VPS 上的软件。 Ubuntu 22.04 LTS 或 24.04 LTS。Docker Engine 24.0 或更高版本,并带 Docker Compose 插件。UFW 或同等防火墙,开放 22、80、443 端口。以非 root 的 sudo 用户身份获得 SSH 访问权限。

用 Docker Compose 部署 Listmonk

Listmonk and PostgreSQL deployed with Docker Compose on a VPS, behind an Nginx or Caddy reverse proxy that terminates HTTPS.

为这次部署建一个目录,然后放进一个 docker-compose.yml 文件,里面有两个服务:postgres 作数据库,listmonk 作应用。两者都在失败时重启。Listmonk 绑定到 127.0.0.1 因此反向代理是唯一能访问它的东西。

Docker Compose 文件

以下是 docker-compose.yml。对照 Listmonk 官方安装文档核对确切的镜像标签和环境变量名。它们会随每次发布而更新。

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    container_name: listmonk-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: listmonk
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: listmonk
    volumes:
      - listmonk-postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U listmonk"]
      interval: 10s
      timeout: 5s
      retries: 6

  app:
    image: listmonk/listmonk:latest
    container_name: listmonk-app
    restart: unless-stopped
    # Bind to loopback only. The reverse proxy is the public entrypoint.
    ports:
      - "127.0.0.1:9000:9000"
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      LISTMONK_app__address: "0.0.0.0:9000"
      LISTMONK_db__host: postgres
      LISTMONK_db__port: 5432
      LISTMONK_db__user: listmonk
      LISTMONK_db__password: ${POSTGRES_PASSWORD}
      LISTMONK_db__database: listmonk

volumes:
  listmonk-postgres:

创建 .env 文件包含 POSTGRES_PASSWORD= 设为一段很长的随机字符串。然后启动整套服务,运行一次性的数据库安装:

# Pull images and start the database first
docker compose up -d postgres

# Run the install step (creates schema and the first admin user)
docker compose run --rm app ./listmonk --install --idempotent --yes

# Start the application
docker compose up -d

当前 --install 命令会提示输入管理员邮箱和密码。把它们保存下来。确认两个容器都在运行:

docker compose ps

预期输出:列出两个服务,状态都是 Up。postgres 那一行应显示 (healthy)。

当前 127.0.0.1:9000 这个绑定是有意为之。Listmonk 没有内置的认证限速器,也没有 IP 白名单。把 9000 端口暴露到公网,意味着地球上任何人都能撞到你的管理员登录页。反向代理的作用,就是让这个登录页只能通过 HTTPS 访问。

Nginx 反向代理与 SSL

从 Ubuntu 仓库安装 Nginx 和 Certbot。在 /etc/nginx/sites-available/listmonk 创建一个站点配置,带上 Listmonk 生成正确发信活动链接所需的代理头:

# /etc/nginx/sites-available/listmonk
server {
    listen 80;
    server_name mail.example.com;

    location / {
        proxy_pass http://127.0.0.1:9000;
        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;

        # Listmonk streams campaign progress over WebSocket
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

把它软链到 sites-enabled,测试配置,重载 Nginx,然后签发证书:

sudo ln -s /etc/nginx/sites-available/listmonk /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d mail.example.com

Certbot 会重写 server 块,让它用新证书监听 443,并加上一条 HTTP 到 HTTPS 的跳转。验证:

curl -I https://mail.example.com

预期输出:HTTP/2 200,带一个有效的 strict-transport-security 头。如果你遇到重定向循环,检查上面 Nginx 配置里 X-Forwarded-Proto 头是否设好。十有八九,循环就是这个头引起的。

如果这台 VPS 上只跑 Listmonk,那就改用 Caddy。Caddyfile 只有三行,而且无需 cron 任务就能处理证书续期:

mail.example.com {
    reverse_proxy 127.0.0.1:9000
}

修复 Message-ID 头

默认情况下,Listmonk 在外发的 Message-ID 头里使用系统主机名。如果你的 VPS 主机名是 localhost 或者任何不是有效 FQDN 的东西,Listmonk 就会发出 Message-ID: <[email protected]>。Gmail 和 Outlook 的垃圾邮件过滤器会立刻把它标记出来。这一点记录在 Cloudron 论坛第 15410 号帖子.

修复方法是 Listmonk 的 config.toml里的一行。对于全新安装,通过 docker compose run --rm app ./listmonk --new-config生成该文件。然后设置:

[app]
hostname = "mail.example.com"

编辑后重启应用容器:

docker compose restart app

在你发出第一封发信活动之前就做好这件事。一个被 localhost.localdomain Message-ID 污染过的列表,比一个一开始就干净的列表更难挽救。

Pro Tip

If you would prefer to skip the Compose setup, check out our 一键 Listmonk VPS 几分钟内一键部署 Listmonk。实例已预装并配好 PostgreSQL。你仍然需要配置你的 SMTP 中继并添加你的 DNS 记录。无论你怎么部署,这些步骤都不是可选的。

选择你的 SMTP 中继

Choosing an SMTP relay for Listmonk: Amazon SES, Postmark, Brevo, and Mailgun compared by cost structure, bounce webhooks, and best fit.

所有发信都经过你配置的中继。中继的 IP 信誉、速率限制和退信处理,才是决定你的邮件落入收件箱还是垃圾邮件箱的因素。

下面是功能对比。定价和免费额度会变。在决定之前,请在各家服务商的官方定价页上逐一核实。

提供商成本结构退信 webhook最适合
Amazon SES按封计费,量大时极低支持,通过 SNS量大时成本低;如果你已在用 AWS
Postmark月度基础费加按封计费支持,原生送达率优先;事务性邮件信誉好
Brevo低发信量有免费额度,更高量有付费档低发信量,且有升级路径
Mailgun按封计费无原生 webhook 端点;需要时用通用退信 API。开发者熟悉

以上只是对各家 SMTP 中继的简略一瞥。接下来,我们会逐一深入讲解。

SES 是量大时最便宜的选择,也是 Listmonk 社区里讨论最多的。它的搭建步骤比 Postmark 或 Brevo 多,但在任何真实发信量下,每封邮件的成本差距都大到足以让这点工作物有所值。

分三个阶段搭建。第一,创建一个带 AmazonSESFullAccess 策略的 IAM 用户(或一个只含 ses:SendRawEmailses:GetSendQuota的更收紧的自定义策略)。第二,在 SES 控制台里验证你的发信域名。SES 会带你完成要添加的 DKIM CNAME。第三,在 SES SMTP 设置面板里生成 SMTP 凭据。这些不是你的 AWS 访问密钥;当你点击“Create SMTP credentials”时,SES 会生成一对单独的、专用于 SMTP 的用户名和密码。

在 Listmonk 管理后台的 Settings → SMTP 里,添加一个新服务器:

  • 主机: email-smtp.<region>.amazonaws.com (用你验证域名时所在的 SES 区域)
  • Port: 587
  • Auth protocol: LOGIN
  • TLS: STARTTLS
  • 用户名和密码:SES 生成的那对 SMTP 凭据

SES 在 587 端口上要求 STARTTLS。 如果你把 TLS 设为 none,或选了 465 端口,Listmonk 会连上,SES 返回 530 Must issue a STARTTLS command first,而管理后台里的 SMTP 凭据测试可能仍然显示成功。在跑任何发信活动之前,先给一个你能掌控的个人收件箱发一封真实的测试邮件。

新的 SES 账号都从沙盒模式开始。在沙盒里你只能发给已验证的邮箱地址,这对订阅者列表毫无用处。从 SES 控制台开一张工单,申请生产访问权限。审批通常需要一个工作日。

Postmark(送达率优先的替代方案)

Postmark 每封邮件的成本比 SES 高,但它原生支持退信 webhook,并且凭借严格的发信人策略,在收件箱送达率上有口碑。如果你的邮件通讯事关业务存亡,或者你不想去管 SES 从沙盒到生产的审批,那它就值这个价。

Listmonk 的配置形式和 SES 一样:主机、587 端口、STARTTLS、来自 Postmark 服务器 API 令牌面板的凭据。在 Postmark 的签名设置里验证你的发信域名,添加 Postmark 生成的 DKIM 记录,你就可以发信了。

当送达率比每封成本更重要时,选 Postmark。当发信量比省心更重要时,选 SES。

关于 SMTP 凭据测试的一个警告。 Listmonk 管理后台里的连接测试总是报告成功,哪怕凭据是错的。这一点记录在 几个 GitHub issue里。别信它。配置任何中继之后,先给单个测试订阅者发一封发信活动,并在目标收件箱里确认收到,再发给你的完整列表。

别用 Mailersend 来做批量发信活动。它每个连接 5 封邮件的上限会产生 421 Service not available 错误,而 Listmonk 会把这些记为已发送,尽管投递其实失败了。这场发信活动在 Listmonk 里看起来很成功,却悄无声息地丢掉了大部分邮件。

让邮件真正送达:SPF、DKIM 和 DMARC

SPF, DKIM, and DMARC DNS records on the sending domain that let receiving mail servers verify Listmonk's email and keep it out of spam.

这是你发信域名上的三条 DNS 记录,它们告诉接收方邮件服务器:你的域名授权了这个中继代你发信。漏掉其中任何一条,规模化之后都会有相当一部分发信落进垃圾邮件箱,无论你的中继或文案多干净。在发出第一封发信活动之前,就在你的 DNS 服务商那里把它们加好。

SPF 记录

SPF 授权特定的 IP 或发信服务为你的域名发信。在你发信域名的根上加一条 TXT 记录,带上你中继的 include。对于 SES,这条记录长这样:

v=spf1 include:amazonses.com ~all

对于 Postmark,把 include 换成 include:spf.mtasv.net。务必查阅你中继的官方 SPF 文档,确认确切的 include 值。它因服务商而异,有时还因区域而异。

一个域名只能有一条 SPF 记录。如果你已经为另一项服务(Google Workspace、Microsoft 365)配了一条,就把 include 合并进现有记录,而不是再加一条。

DKIM

DKIM 给外发邮件附上一个加密签名,接收方服务器会用你 DNS 里的公钥来验证它。密钥对由你的中继生成。你把公钥作为一条 TXT 记录加到一个选择器子域名(例如 sel1._domainkey.example.com)上,值就用中继给你的那个确切值。

Listmonk 不负责 DKIM 签名。中继负责。没有 Listmonk 专属的 DKIM 配置。跟着你中继的 DKIM 设置向导走,加上它给你的记录,然后等 DNS 生效(通常不到 30 分钟;有时要几个小时)。

DMARC

DMARC 告诉接收方服务器,对未通过 SPF 或 DKIM 校验的邮件该怎么处理。先用 p=none 进入监控模式,这样你能在汇总报告里看到失败情况,又不会在你排查配置错误期间影响送达率。在以下位置加一条 TXT 记录: _dmarc.example.com:

v=DMARC1; p=none; rua=mailto:[email protected]

两三周的报告都干净之后,再把策略收紧为 p=quarantine or p=reject。别跳过监控阶段。如果你 SPF include 里有个笔误,第一天又配上了 p=reject ,那一上来就会把你自己合法的邮件全部清掉,而且没有任何信号告诉你出了问题。

List-Unsubscribe 头(RFC 8058)由 Listmonk 自动生成。在 Settings → General 下确认它已启用。Gmail 和 Apple Mail 会把这个头呈现为一键退订选项,从而保护发信人信誉。

上线后真正会出问题的地方

四种故障模式,要等你发出第一封真实的发信活动才会冒出来。赶在你的订阅者之前抓住它们。

问题 1:退信率和你中继的数字对不上。 Listmonk 处理退信的方式,是通过 POP3 读取一个指定的退信邮箱地址,并删除它读到的每一封邮件。这其中包括休假回复、投递回执和外出自动回复,全都被归为退信。而你的中继只统计接收方邮件服务器返回的真正投递失败。如果 SES 报告 0.6% 而 Listmonk 报告 4%, 这就是差距所在。修复办法是配置退信 webhook 回调,取代 POP3。对于 SES,用 SNS 把退信通知投递到 Listmonk 的 webhook 端点。对于 Postmark,把它的原生 webhook 指向同一个端点。Webhook 退信是准确的;POP3 退信会虚高。

问题 2:SMTP 凭据测试明明是错的却说成功。 正如中继那一节提到的,连接测试无论凭据是否有效都报告成功。别信它。配置或更改任何 SMTP 设置之后,永远先发一封真实的测试邮件。

问题 3:一场发信活动发到一半就停了,没有报错。 哪怕只有 60% 的订阅者收到了邮件,Listmonk 也会把发信活动标记为 Finished。剩下那些发信,要么被中继拒绝,要么在 VPS 网络层被限流,而 Listmonk 不会把这两种情况作为发信活动级别的错误呈现出来(Cloudron 论坛第 13165 号帖子)。如果一场发信活动显示的发送数少于订阅者数,就打开你中继在该发送时间窗内的仪表盘,把中继接受的数量和 Listmonk 的数量做对比。真相在中继那边。

问题 4:没人在备份 PostgreSQL。 Compose 卷会在重启之间持久化数据。它防不住主机故障、误删 docker 卷,或损坏的升级。加一个每日的 pg_dump:

0 2 * * * docker exec listmonk-postgres pg_dump -U listmonk listmonk > /backups/listmonk-$(date +\%Y\%m\%d).sql

先手动跑一遍这行命令。在你信任这条 cron 条目之前,先确认输出文件非空。一个会写出零字节文件却不报错的备份脚本,比根本没有备份更糟,因为你会从此不再操心它。

在你把这一切交付到生产环境去信任之前,先给一个订阅者发一封测试发信活动,并在目标收件箱里确认收到。如果那一封邮件干净落地,接下来的一万封也会。

常见问题

为什么我的 Listmonk 退信率比 Amazon SES 报告的高?

Listmonk 的 POP3 退信处理会把外出回复和休假自动回复当作退信来读,从而虚高计数。配置 SES 的 SNS webhook 回调以获得准确的计数。

Listmonk 支持事务性邮件吗?

Listmonk 是一个邮件通讯和群发活动工具。它不原生处理事务性邮件(密码重置、订单确认、一对一的触发式邮件)。要从同一个发信域名发事务性邮件,请单独配置你中继的事务性端点,或者在 Listmonk 之外使用 Postal 或 Postmark 的事务性 API 这类专门工具。

我怎么把我的 Mailchimp 订阅者导入 Listmonk?

从 Audience → Export Audience 把你的 Mailchimp 列表导出为 CSV。在 Listmonk 里,进入 Subscribers → Import 并上传该 CSV。出现提示时映射 email 和 name 列。Listmonk 接受来自 Mailchimp、ConvertKit 以及大多数邮件通讯平台的标准 CSV 导出,无需格式转换。

当有人退订一场 Listmonk 发信活动时会发生什么?

Listmonk 默认在每一封发信活动邮件里加一个退订链接。当订阅者点击它时,他们会被加入屏蔽列表,并从所有未来的发信活动中移除。List-Unsubscribe 头(RFC 8058)会自动包含,因此支持一键退订的邮件客户端(Gmail、Apple Mail)会原生呈现它。该订阅者的记录会出于审计目的留在数据库里,但不会再有发信活动发给他们。

Share

博客更多内容

继续阅读。

准备好部署了吗? 起价 $2.48/月。

独立云厂商,自 2008 年起。AMD EPYC、NVMe、40 Gbps。14 天退款保证。