7.3 KiB

name description tags
mail-server-deployment Deploy Postfix+Dovecot+DKIM mail server with Let's Encrypt.
mail
postfix
dovecot
dkim
letsencrypt
smtp
imap
devops

Mail Server Deployment

Overview

Deploy a complete mail server stack on Ubuntu 22.04:

  • Postfix (SMTP/SMTPS/Submission)
  • Dovecot (IMAPS)
  • OpenDKIM (email signing)
  • Let's Encrypt (SSL certificates)
  • SASL authentication (system users)

Prerequisites

  • Ubuntu 22.04 server with root/sudo access
  • Domain with DNS managed externally (e.g., Aliyun DNS)
  • Cloud firewall (security group) capable of opening ports
  • Port 25 typically blocked by cloud providers — requires support ticket to unblock

Step 1: DNS Records

Create these five DNS records before starting:

Type Host Value
MX @ 1 mail.<domain>
A mail <server IP>
TXT @ v=spf1 mx ~all
TXT _dmarc v=DMARC1; p=none; rua=mailto:admin@<domain>

DKIM record is generated later (Step 5).

Step 2: Install Packages

sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
  postfix dovecot-core dovecot-imapd dovecot-pop3d \
  opendkim opendkim-tools

Step 3: Postfix Configuration

See references/postfix-config.md for the complete main.cf and master.cf setup.

Key settings:

postconf -e "myhostname = mail.<domain>"
postconf -e "mydomain = <domain>"
postconf -e "mydestination = localhost, mail.<domain>, <domain>"
postconf -e "home_mailbox = Maildir/"
postconf -e "smtpd_sasl_type = dovecot"
postconf -e "smtpd_sasl_path = private/auth"
postconf -e "smtpd_sasl_auth_enable = yes"
postconf -e "smtpd_tls_auth_only = yes"
postconf -e "transport_maps = hash:/etc/postfix/transport"

Enable SMTPS (465) and Submission (587) by uncommenting the submission and smtps sections in /etc/postfix/master.cf.

Step 4: Dovecot Configuration

See references/dovecot-config.md for the complete configuration.

Critical settings:

  • mail_location = maildir:~/Maildir
  • SASL auth socket at /var/spool/postfix/private/auth (mode 0660, user postfix)
  • IMAP on port 0 (disabled), IMAPS on port 993
  • POP3/POP3S disabled entirely

Step 5: Let's Encrypt SSL

sudo certbot certonly --webroot -w /var/www/html \
  -d mail.<domain> --non-interactive --agree-tos \
  --email admin@<domain>

Apply cert to postfix:

postconf -e "smtpd_tls_cert_file = /etc/letsencrypt/live/mail.<domain>/fullchain.pem"
postconf -e "smtpd_tls_key_file = /etc/letsencrypt/live/mail.<domain>/privkey.pem"

Apply cert to dovecot — see references/dovecot-config.md.

Verify TLS on all ports:

echo Q | openssl s_client -starttls smtp -connect localhost:25 | grep "Verify return code"
echo Q | openssl s_client -connect localhost:465 | grep "Verify return code"
echo Q | openssl s_client -connect localhost:993 | grep "Verify return code"

All should show Verify return code: 0 (ok).

Step 6: DKIM (OpenDKIM)

Generate key:

mkdir -p /etc/opendkim/keys/<domain>
opendkim-genkey -s default -d <domain> -D /etc/opendkim/keys/<domain>/
chown root:root /etc/opendkim/keys/<domain>/default.private
chmod 600 /etc/opendkim/keys/<domain>/default.private

Configure /etc/opendkim.conf:

Domain <domain>
Selector default
KeyFile /etc/opendkim/keys/<domain>/default.private
Socket inet:8891@localhost
Mode sv
Canonicalization relaxed/simple

Create /etc/opendkim/TrustedHosts:

127.0.0.1
::1
localhost
<hostname>
mail.<domain>
<domain>

Fix PID directory:

mkdir -p /var/run/opendkim
chown opendkim:opendkim /var/run/opendkim

Connect to postfix:

postconf -e "milter_default_action = accept"
postconf -e "milter_protocol = 6"
postconf -e "smtpd_milters = inet:localhost:8891"
postconf -e "non_smtpd_milters = inet:localhost:8891"

DKIM DNS Record

Extract the public key from /etc/opendkim/keys/<domain>/default.txt and add as a TXT record:

Type Host Value
TXT default._domainkey v=DKIM1; h=sha256; k=rsa; p=<public_key>

Verify: dig +short TXT default._domainkey.<domain>

Step 7: Firewall (Cloud Security Group)

Open these TCP ports in the cloud security group (e.g., Aliyun):

Port Protocol Purpose
25 TCP SMTP (external receive)
465 TCP SMTPS (SSL send)
587 TCP Submission (TLS submit)
993 TCP IMAPS (SSL receive)
80 TCP Certbot renewal

Port 25 is usually blocked by cloud providers — submit a support ticket to unblock.

Step 8: Test

Local delivery test

echo "Test" | mail -s "Test subject" <user>@localhost
ls ~/Maildir/new/

DKIM signing test (via SMTP — this is the path that triggers DKIM)

echo -e "EHLO mail.<domain>\nMAIL FROM:<<user>@<domain>>\nRCPT TO:<<user>@localhost>\nDATA\nSubject: DKIM test\nFrom: <user>@<domain>\n\nTest body\n.\nQUIT" \
  | timeout 10 openssl s_client -connect localhost:465 -quiet 2>/dev/null

Check the delivered email for DKIM-Signature: header.

Important: DKIM signing only works through the SMTP path (ports 25/465/587). The mail command and sendmail use the non_smtpd_milters path which may not trigger signing for all configurations. Always test DKIM via SMTP.

Queue check

mailq  # Should be empty

Pitfalls

  1. Port 25 blocked by cloud provider: Mail to user@domain will queue and fail with "Connection timed out" because postfix tries to deliver via public IP on port 25. Fix: add transport_maps entry domain local: to force local delivery, AND open port 25 on the security group.

  2. DKIM signing only via SMTP: The mail command and sendmail go through non_smtpd_milters which may or may not trigger DKIM signing depending on configuration. Always test DKIM via SMTP submission (port 465 or 587).

  3. OpenDKIM PID file: On Ubuntu 22.04, opendkim may fail with "Can't open PID file /run/opendkim/opendkim.pid". Fix: mkdir -p /var/run/opendkim && chown opendkim:opendkim /var/run/opendkim and add PidFile /var/run/opendkim/opendkim.pid to opendkim.conf.

  4. OpenDKIM key ownership: opendkim runs as root (uid 0) on Ubuntu 22.04 by default. If the key file is owned by opendkim user, opendkim will reject it. Set key ownership to root:root.

  5. Shell quoting breaks postconf: postconf -e "key = value" fails through SSH because the = gets consumed. Use postconf -e key=value (no spaces around =) or the base64-encoding pattern (see references/shell-quoting-workaround.md under socks-proxy-download skill).

  6. Let's Encrypt DNS requirement: Certbot webroot validation requires the domain to resolve publicly. If DNS is misconfigured, certbot will fail with "SERVFAIL". Run --dry-run first to verify.

  7. mydestination must include the domain: Without <domain> in mydestination, postfix will try to deliver user@domain externally via SMTP (which fails if port 25 is blocked). Add a transport_maps entry as backup: echo "<domain> local:" > /etc/postfix/transport && postmap /etc/postfix/transport.

References

File Content
references/postfix-config.md Complete postfix main.cf and master.cf
references/dovecot-config.md Complete dovecot configuration files
references/dns-records.md DNS record reference with examples