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
sudoprivileges. This guide usesyour_useras 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.
| Type | Name | Value | Purpose |
|---|---|---|---|
| A | mail.your_domain | your_server_ip | Resolves the mail host |
| MX | your_domain | 10 mail.your_domain | Tells other servers where to deliver mail for the domain |
| TXT | your_domain | v=spf1 mx ~all | SPF: only the MX hosts may send for the domain |
| TXT | _dmarc.your_domain | v=DMARC1; p=none; rua=mailto:postmaster@your_domain | DMARC 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(notmail.your_domain). This value goes into/etc/mailnameand becomes the domain of mail sent by local users, soyour_usersends asyour_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 outin 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 withmailqand force a retry withsudo 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 withsudo postconf -e "inet_protocols = ipv4"followed bysudo systemctl restart postfix. Relay access deniedwhen receiving: the domain in the recipient address is missing frommydestination.status=bounced (unknown user: ...): there is no Linux user or alias with that name. Create the user or add an alias in/etc/aliasesand runsudo newaliases.- DKIM does not sign: look for
opendkimlines in/var/log/mail.log. If outgoing messages get noDKIM-Signature field addedline, it usually means theFromdomain does not match theDomainvalue 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.
