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
- 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.
- Basic DNS:
MX @ → mail.yourdomain(priority 10),A mail → VPS public IP. Do not rush to start services before DNS resolution takes effect.
- Bring up mailserver: Prepare
compose.yamlandmailserver.env. Key variables: setOVERRIDE_HOSTNAMEto the mail hostname, setNETWORK_INTERFACEto the host NIC (use host networking), and setSSL_TYPE=manualpointing to the certificate path. For the data directory, it is recommended to mount a separate image file to facilitate snapshots.
- 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.
- Create accounts:
docker exec mailserver setup email add <address> <password>; usesetup email deleteto delete andsetup email listto list. Catch-all is disabled by default, and addresses that have not been created are rejected directly.
- 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-codeand the CNAME records forbrevo1/brevo2._domainkeyto DNS. Then configureRELAY_HOST,RELAY_PORT=587,RELAY_USER, andRELAY_PASSWORD(SMTP key) inmailserver.env, and restart the container.
- DMARC: Add a
_dmarcTXT record. To start, you can usep=quarantineand leave anruareport address. Sending goes through Brevo's SPF, so our domain no longer needs SPF; DMARC passes through DKIM alignment.
- Clients: IMAP host
mail.yourdomain993 SSL/TLS; SMTP same host 465 or 587.
- 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.