Postfix is the mail transfer agent (MTA) used by default on Debian and Ubuntu: it accepts mail from other servers on port 25, delivers it to local mailboxes and sends outgoing mail to the rest of the internet. In this tutorial you will configure Postfix on Ubuntu 24.04 so it receives mail for your_domain into Maildir mailboxes, sends mail that passes SPF, DKIM and DMARC checks at Gmail and Outlook, and uses a Let's Encrypt certificate for TLS. Mail clients (IMAP and authenticated sending on port 587) are added with Dovecot in the follow-up guide.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS with a public IPv4 address, for example a CubePath VPS. A clean server is best; do not install Postfix next to another MTA.
  • A non-root user with sudo privileges. This guide uses your_user as the mailbox that receives mail.
  • A domain name you control (your_domain) and access to its DNS zone.
  • The ability to set the reverse DNS (PTR) record of the server's IP address.
  • Outbound TCP port 25 allowed. On CubePath, as with most cloud providers, outbound port 25 is closed by default to prevent spam; open a support ticket describing your mail use to request it. Step 1 shows how to test it.

Throughout the guide, replace your_domain with your domain, mail.your_domain with the mail server's hostname and your_server_ip with its public IPv4 address.

Step 1 - Setting the hostname and checking port 25

Mail servers identify themselves by hostname in every SMTP conversation, and receiving servers check that the name, the IP address and the reverse DNS agree. Set the hostname to the fully qualified name:

sudo hostnamectl set-hostname mail.your_domain

Add it to /etc/hosts so the server can resolve its own name without DNS:

sudo nano /etc/hosts
127.0.1.1 mail.your_domain mail

Confirm the result:

hostname -f
mail.your_domain

Now check that outbound port 25 is open by connecting to Gmail's inbound mail server:

nc -vz -w 5 gmail-smtp-in.l.google.com 25
Connection to gmail-smtp-in.l.google.com (142.250.x.x) 25 port [tcp/smtp] succeeded!

If the command times out, outbound SMTP is blocked. You can still finish the setup and receive mail, but you cannot deliver to other servers until it is opened.

Step 2 - Creating the DNS records

Create these records in your domain's DNS zone. The DKIM record is added later in Step 7, once the key exists.

TypeNameValuePurpose
Amail.your_domainyour_server_ipResolves the mail host
MXyour_domain10 mail.your_domainTells other servers where to deliver mail for the domain
TXTyour_domainv=spf1 mx ~allSPF: only the MX hosts may send for the domain
TXT_dmarc.your_domainv=DMARC1; p=none; rua=mailto:postmaster@your_domainDMARC policy and reports

Set the PTR (reverse DNS) record of your_server_ip to mail.your_domain. Reverse DNS belongs to whoever owns the IP address, so it is set through your hosting provider rather than in your domain's zone. Without a matching PTR record, Gmail and Outlook reject or junk your mail.

~all (soft fail) and p=none are safe starting values while you test. Once the DMARC reports show that all legitimate mail passes, tighten them to -all and p=quarantine or p=reject.

Verify the records from the server. DNS changes can take a few minutes to propagate:

dig +short MX your_domain
dig +short A mail.your_domain
dig +short -x your_server_ip
dig +short TXT your_domain
10 mail.your_domain.
your_server_ip
mail.your_domain.
"v=spf1 mx ~all"

Step 3 - Installing Postfix

Update the package index and install Postfix:

sudo apt update
sudo apt install postfix

The installer asks two questions:

  • General mail configuration type: choose Internet Site.
  • System mail name: enter your_domain (not mail.your_domain). This value goes into /etc/mailname and becomes the domain of mail sent by local users, so your_user sends as your_user@your_domain.

Postfix starts automatically. Check that it is listening on port 25:

sudo ss -ltnp | grep ':25 '
LISTEN 0      100          0.0.0.0:25        0.0.0.0:*    users:(("master",pid=2215,fd=13))
LISTEN 0      100             [::]:25           [::]:*    users:(("master",pid=2215,fd=14))

If you selected the wrong options, run sudo dpkg-reconfigure postfix to answer them again.

Step 4 - Getting a TLS certificate

Other mail servers encrypt the connection with STARTTLS when your server offers it, and Gmail marks mail from servers without TLS. Get a free certificate for mail.your_domain from Let's Encrypt. Certbot's standalone mode runs its own temporary web server on port 80, so allow SSH and HTTP in the firewall first:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 25/tcp
sudo ufw enable

Install Certbot and request the certificate. The deploy hook reloads Postfix after each automatic renewal so it picks up the new files:

sudo apt install certbot
sudo certbot certonly --standalone -d mail.your_domain --deploy-hook "systemctl reload postfix"

Certbot saves the certificate under /etc/letsencrypt/live/mail.your_domain/ and installs a systemd timer that renews it. Test renewal:

sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/mail.your_domain/fullchain.pem (success)

Step 5 - Configuring Postfix

Postfix's main settings live in /etc/postfix/main.cf. Instead of editing the file by hand, use postconf -e, which changes a single parameter and avoids duplicate lines. Back up the original first:

sudo cp /etc/postfix/main.cf /etc/postfix/main.cf.orig

Set the hostname and the domains this server accepts mail for:

sudo postconf -e "myhostname = mail.your_domain"
sudo postconf -e "mydestination = \$myhostname, your_domain, localhost.localdomain, localhost"

Deliver mail to a Maildir folder in each user's home directory. Maildir stores one file per message and is the format Dovecot uses in the next guide. Clearing mailbox_command makes sure no external delivery program overrides it:

sudo postconf -e "home_mailbox = Maildir/"
sudo postconf -e "mailbox_command ="

Point Postfix at the Let's Encrypt certificate and enable opportunistic TLS in both directions:

sudo postconf -e "smtpd_tls_cert_file = /etc/letsencrypt/live/mail.your_domain/fullchain.pem"
sudo postconf -e "smtpd_tls_key_file = /etc/letsencrypt/live/mail.your_domain/privkey.pem"
sudo postconf -e "smtpd_tls_security_level = may"
sudo postconf -e "smtp_tls_security_level = may"

may is correct for server-to-server mail on port 25: TLS is used whenever the other side supports it, and mail from the few servers that do not is still accepted.

Add basic restrictions that reject obviously broken or forged senders before the message is accepted:

sudo postconf -e "smtpd_helo_required = yes"
sudo postconf -e "disable_vrfy_command = yes"
sudo postconf -e "smtpd_helo_restrictions = permit_mynetworks, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname"
sudo postconf -e "smtpd_sender_restrictions = permit_mynetworks, reject_non_fqdn_sender, reject_unknown_sender_domain"

Ubuntu's default smtpd_relay_restrictions (permit_mynetworks permit_sasl_authenticated defer_unauth_destination) already prevents the server from being an open relay: only local processes can send to external domains. Leave it as is.

If the server has more than one public IPv4 address, bind outgoing mail to the one whose PTR record is mail.your_domain:

sudo postconf -e "smtp_bind_address = your_server_ip"

Check the configuration for errors and reload:

sudo postfix check
sudo systemctl reload postfix

postfix check prints nothing when everything is fine. Review all the settings that differ from the defaults with:

postconf -n

Step 6 - Setting up aliases and receiving mail

Mail for postmaster@your_domain and root@your_domain must reach a person: DMARC reports and bounce notices go there. Open the aliases file:

sudo nano /etc/aliases
postmaster: root
root: your_user

Rebuild the aliases database:

sudo newaliases

Now send a message from an external account such as Gmail to your_user@your_domain. Follow the log while it arrives:

sudo tail -f /var/log/mail.log

A successful delivery ends with a line like this:

postfix/local[3120]: 4XbQ2K1d7Wz9sWb: to=<your_user@your_domain>, relay=local, delay=0.12, delays=0.08/0.01/0/0.03, dsn=2.0.0, status=sent (delivered to maildir)

Press Ctrl+C to stop following the log. The message is stored as a file in the user's Maildir:

ls ~/Maildir/new/

Postfix creates the Maildir directory on the first delivery, so it does not exist before the first message arrives.

Step 7 - Signing outgoing mail with DKIM

DKIM adds a cryptographic signature to each outgoing message, and receiving servers verify it against a public key published in DNS. Together with SPF it is what lets DMARC pass. OpenDKIM signs the mail and Postfix hands messages to it through the milter interface.

Install OpenDKIM:

sudo apt install opendkim opendkim-tools

Generate a 2048-bit key pair with the selector default:

sudo mkdir -p /etc/opendkim/keys/your_domain
sudo opendkim-genkey -b 2048 -d your_domain -D /etc/opendkim/keys/your_domain -s default -v
sudo chown -R opendkim:opendkim /etc/opendkim/keys

This creates default.private (the private key) and default.txt (the DNS record). Open the OpenDKIM configuration:

sudo nano /etc/opendkim.conf

Find the existing Socket local:/run/opendkim/opendkim.sock line and comment it out with #. Then add these lines at the end of the file:

Domain      your_domain
Selector    default
KeyFile     /etc/opendkim/keys/your_domain/default.private
Socket      inet:8891@localhost

A TCP socket on localhost avoids permission problems with Postfix, which runs its SMTP server inside a chroot on Ubuntu. Restart OpenDKIM and check that it listens:

sudo systemctl restart opendkim
sudo ss -ltnp | grep 8891
LISTEN 0      4096       127.0.0.1:8891      0.0.0.0:*    users:(("opendkim",pid=4012,fd=3))

Connect Postfix to OpenDKIM. non_smtpd_milters makes sure mail submitted locally with sendmail is signed too, and milter_default_action = accept keeps mail flowing if OpenDKIM is down:

sudo postconf -e "milter_default_action = accept"
sudo postconf -e "smtpd_milters = inet:localhost:8891"
sudo postconf -e "non_smtpd_milters = \$smtpd_milters"
sudo systemctl reload postfix

Publish the public key. Display the generated record:

sudo cat /etc/opendkim/keys/your_domain/default.txt

The file splits the key into several quoted strings. In your DNS panel, create a TXT record named default._domainkey.your_domain whose value is all the strings joined together without quotes or spaces between them, starting with v=DKIM1; h=sha256; k=rsa; p= followed by the key.

When the record has propagated, test it:

sudo opendkim-testkey -d your_domain -s default -vvv
opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: checking key 'default._domainkey.your_domain'
opendkim-testkey: key not secure
opendkim-testkey: key OK

key not secure only means the zone is not signed with DNSSEC. key OK is the line that matters.

Step 8 - Testing outgoing mail

Send a test message to an external mailbox you can read, such as a Gmail address. The sendmail command is provided by Postfix:

printf 'Subject: Postfix test\n\nHello from mail.your_domain.\n' | sendmail -f your_user@your_domain [email protected]

Check the log for the result:

sudo grep 'status=' /var/log/mail.log | tail -n 3
postfix/smtp[4410]: 4XbQ7m2Hc1z9sWd: to=<[email protected]>, relay=gmail-smtp-in.l.google.com[142.250.x.x]:25, delay=1.2, delays=0.05/0.01/0.4/0.7, dsn=2.0.0, status=sent (250 2.0.0 OK ...)

status=sent with dsn=2.0.0 means the receiving server accepted the message. In Gmail, open the message, choose Show original and check that SPF, DKIM and DMARC all show PASS.

Troubleshooting

  • connect to ...:25: Connection timed out in the log: outbound port 25 is blocked. Test it as in Step 1 and request it from your provider. Messages stay in the queue and are retried automatically; see them with mailq and force a retry with sudo postqueue -f.
  • Mail is rejected or lands in spam: check that the PTR record matches myhostname (dig +short -x your_server_ip), and read the headers in Show original for which check failed. If your server also has IPv6, the PTR and SPF must cover the IPv6 address too, or restrict Postfix to IPv4 with sudo postconf -e "inet_protocols = ipv4" followed by sudo systemctl restart postfix.
  • Relay access denied when receiving: the domain in the recipient address is missing from mydestination.
  • status=bounced (unknown user: ...): there is no Linux user or alias with that name. Create the user or add an alias in /etc/aliases and run sudo newaliases.
  • DKIM does not sign: look for opendkim lines in /var/log/mail.log. If outgoing messages get no DKIM-Signature field added line, it usually means the From domain does not match the Domain value in /etc/opendkim.conf.

Conclusion

Your server now accepts mail for your_domain on port 25, stores it in Maildir, and sends mail over TLS with a valid PTR record, SPF, DKIM and DMARC. The next step is to install Dovecot so users can read mail over IMAP and send through the server from a mail client on port 587 with authentication. After that, consider adding spam filtering with Rspamd and moving your DMARC policy to p=quarantine once the reports look clean.