Projects/Self-hosted Email (Brevo Relay)
Mail

Self-hosted Email (Brevo Relay)

Send and receive email with your own domain: receive on a VPS and send via Brevo's free relay, with SPF/DKIM/DMARC passing across the full chain.

What This Is

This is a private email system I built on my own domain xiejun-server.top: docker-mailserver runs on a VPS at home and handles incoming mail, while outgoing mail goes through Brevo’s free relay. Clients use IMAP 993 and SMTP 465/587 to send and receive, and the experience is no different from a regular email account.

Why I Built It

First, I wanted a long-term email address that truly belongs to me and is not tied to any vendor. Second, outbound port 25 on the Tencent Cloud VPS is blocked at the network layer, and allowing it in the security group does not help, so outgoing mail can only go through an ESP relay. I also used this opportunity to thoroughly understand the whole domain authentication setup: SPF, DKIM, and DMARC.

Current Status

Launched on 2026-10-18, currently running. Inbound port 25 has been verified as publicly reachable, and external parties can deliver mail directly. Outbound mail has been tested and can be delivered to external mailboxes. mail-tester reports dmarc=pass, dkim=pass, and spf=pass, with a score of 4.7/10, and SpamAssassin does not classify it as spam. The only limitation that cannot be eliminated is that the .top suffix itself has a reputation penalty. This is inherent to the domain and cannot be solved at the code level.

Features

  • VPS 上用 docker-mailserver 收信,Dovecot 提供 IMAP/SSL 993
  • 出站 25 被封,改由 Brevo 免费中继投递(300 封/天)
  • Brevo 完成根域与 send 子域认证,由 Brevo 用自有域名重签 DKIM
  • DMARC 设 p=quarantine 并留报告地址,mail-tester 三项全 pass
  • Let's Encrypt 证书经 frp 隧道走 HTTP-01 自动续期
  • 邮件数据放 8G loop ext4 镜像,便于备份与迁移

Stack: docker-mailserver · Docker Compose · Postfix · Dovecot · OpenDKIM · Brevo SMTP 中继 · Let's Encrypt · frp

Deployment Steps

  1. Preparation: A domain, a VPS with inbound port 25 reachable, and Docker and Compose installed. First confirm whether the provider blocks outbound port 25; if blocked, you must use a relay.
  1. Basic DNS: MX @ → mail.yourdomain (priority 10), A mail → VPS public IP. Do not rush to start services before DNS resolution takes effect.
  1. Bring up mailserver: Prepare compose.yaml and mailserver.env. Key variables: set OVERRIDE_HOSTNAME to the mail hostname, set NETWORK_INTERFACE to the host NIC (use host networking), and set SSL_TYPE=manual pointing to the certificate path. For the data directory, it is recommended to mount a separate image file to facilitate snapshots.
  1. Certificates: Let's Encrypt uses HTTP-01. Here I use an frp tunnel to expose port 80, and restart the container after cron renews the certificate. In manual certificate mode, renewal does not automatically reload, so remember to restart.
  1. Create accounts: docker exec mailserver setup email add <address> <password>; use setup email delete to delete and setup email list to list. Catch-all is disabled by default, and addresses that have not been created are rejected directly.
  1. Integrate Brevo: Register a free account and complete phone verification. Perform domain authentication in the backend—both the root domain and the sending subdomain must be authenticated, because Brevo only signs DKIM with your domain for authenticated domains. Add the TXT record for brevo-code and the CNAME records for brevo1/brevo2._domainkey to DNS. Then configure RELAY_HOST, RELAY_PORT=587, RELAY_USER, and RELAY_PASSWORD (SMTP key) in mailserver.env, and restart the container.
  1. DMARC: Add a _dmarc TXT record. To start, you can use p=quarantine and leave an rua report address. Sending goes through Brevo's SPF, so our domain no longer needs SPF; DMARC passes through DKIM alignment.
  1. Clients: IMAP host mail.yourdomain 993 SSL/TLS; SMTP same host 465 or 587.
  1. Verification: Send a test email with mail-tester and check whether spf/dkim/dmarc all pass and whether SpamAssassin is below the threshold. Then send one email each to Gmail and QQ to confirm inbox delivery.