Over the past few weeks, I’ve been exploring the idea of running my own mail server. While I had a basic understanding of how email servers work, I’d never actually set one up from scratch.

To keep things simple (and sane), I decided to focus on just one piece of the puzzle: sending email. No inboxes, no IMAP or POP – just a streamlined, send-only SMTP server.

This minimalist approach has a lot of advantages. By isolating the outbound mail component, I can reuse it across multiple projects – whether it’s bulk newsletters, transactional emails from WordPress sites, or as an external relay for my personal addresses – without dragging along a full mail stack every time.

In this guide, I’ll walk you through setting up Postal, a powerful open-source SMTP server. I’ll cover installation, configuration, and best practices for deliverability.

📬 Newsletter

Get updates from my mail server journey – tips, lessons, and discoveries along the way.

I’m In!

Why Postal?

If you know me, you know that from my collection of server security guides, I’ve always tried to limit the use of third-party security software and prefer to build my own solutions.

I’ve always leaned towards the old school way of doing things, avoiding extra software and instead using the available server packages to suit my needs.

So why didn’t I just go with Postfix for this SMTP setup? I seriously considered it – but Postal offered too many advantages to ignore.

Unlike Postfix, Postal comes with a suite of modern features out of the box. And since this is a fairly complex project, I didn’t want to spend hours wiring together manual configs for every little piece.

Postal simplifies a lot of that – and has an active community (GitHub Discussions) where you can get support if you hit a wall.

🙆‍♂️
One feature that really sold me: IP pool support.

Postal makes it easy to create multiple IP pools, so you can send emails from different IP addresses instead of being stuck with just the server’s main IP.

This is huge for:

  • Separating email traffic across clients or apps.
  • Managing deliverability and sender reputation.
  • Assigning clean IPs to new users without affecting your existing ones.

If you're running multiple projects or managing email for clients, this kind of flexibility is a must-have.

👉
And that’s just scratching the surface.

Throughout this guide, you’ll discover more powerful features, such as its clean and intuitive web interface, its built-in integration with SpamAssassin to scan outgoing emails for spam, and even the ability to receive emails through routes (yep, we’ll cover that too).

Author's Note

Now that you know why I chose Postal as my SMTP server, I want to highlight a few key points that can make or break your setup – especially when it comes to deliverability.

Your IP reputation is everything.

A clean, trusted IP is crucial – not just for your main server, but for every IP you use in a pool. A poor reputation can lead to your emails being flagged as spam or blocked entirely.

That’s why choosing the right server provider matters. In my experience, Hetzner has the strictest email-sending policies.

Most providers block port 25 by default to combat spam, and while many will unblock it upon request, Hetzner is by far the most stringent in this regard. Their strict policies ensure that their IPs remain clean and trusted, making them an excellent choice for an SMTP server.

👉
New to Hetzner? Use my link to get free credits!

But even with clean IPs, don’t expect inbox success on day one. If you're using a brand-new IP, mail servers may treat it as suspicious. You’ll need to warm up the IP – sending gradually and building trust over time.

🔁
Are you planning to use multiple IPs or create IP pools? Choose a provider that supports it.

Hetzner, again, is great here. They offer Floating IPs extra public IP addresses you can attach to your VPS in addition to the main IP.

What makes Floating IPs useful:

  • They’re detached from the server itself, so if you delete or replace the server, you can reassign the same IPs to a new one.
  • This helps you preserve your sending reputation and avoid restarting from scratch.
  • Perfect for IP pools and advanced sending setups.

If you're using a provider other than Hetzner, be sure to check if they offer something similar.

And don’t forget: Make sure port 25 is open. Without it, your SMTP server won’t be able to send anything.

IP Pools

If you're interested in using IP pools with Postal, I've written a separate guide that walks through how to set them up and use them effectively.

But don’t worry about that just yet – I recommend completing this setup guide first, then coming back to the IP pools once everything is up and running.

Server Preparation

Before preparing our server to run Postal, we need to deploy it first.

Choose Ubuntu 24.04 LTS as the server image – it’s stable, well-supported, and perfect for this setup.

If you're using Hetzner, you’ll notice they offer two types of servers:

  • Shared CPU (resources are shared with other customers)
  • Dedicated CPU (resources are reserved for you)
You don’t want your outbound queue throttled because of a noisy neighbor.

I highly recommend opting for the servers with dedicated CPUs, as an SMTP server is mission-critical and should never face resource limitations.

It’s also essential to enable IPv6 on your server (many modern mail servers require it) and monitor server resources to avoid bottlenecks in production.

Initial Setup

After your server is deployed, SSH into it and begin by running the following commands to perform a full update:

apt update
apt dist-upgrade
💡
Assuming you’re accessing the server as root for the first time, remember to change your root password to something stronger afterward.

Next, make sure to set the correct timezone for your server. This isn’t just for convenience – it’s important for Postal’s internal logging and email timestamps.

For example, to set the timezone to Berlin, you can run:

timedatectl set-timezone Europe/Berlin

With the timezone in place, the next step is to configure your server’s hostname.

A typical hostname consists of two parts: the server name and the domain name. Since we’re building an SMTP server, a clear and descriptive hostname helps other mail servers recognize your server’s role.

A recommended format is:

hostnamectl set-hostname mailout.example.com

Using mailout as the server name is a good practice, as it clearly indicates to other mail servers that this server is handling SMTP functions.

For better security:

  • Create a new user with sudo privileges
  • Disable the root user
  • Set up SSH keys for the new user
  • Disable password authentication
👉
I’ve covered all of this in detail in my Server Preparation guide – check it out if you need help.

Install Dependencies and Docker

Now that your server is updated and configured, let’s install the essential tools Postal relies on.

Start by installing the required packages:

sudo apt install git curl jq

This ensures that git is available on your server, which we’ll use to clone the Postal installation helper repository:

sudo git clone https://github.com/postalserver/install /opt/postal/install

Then, create a symbolic link so you can run the postal command from anywhere on the server:

sudo ln -s /opt/postal/install/bin/postal /usr/bin/postal
💡
The postal command needs to be run with sudo privileges or as the root user.

Use a Proper Docker Setup (Not Ubuntu’s Built-in Version)

Postal runs entirely inside containers, so installing Docker Engine and the Docker Compose plugin is a major prerequisite.

But, the Docker packages included in Ubuntu’s default repositories are often outdated and may cause issues.

To avoid problems, follow my Docker installation tutorial, where I walk through installing the latest version of Docker and Docker Compose the right way – fully compatible with Postal.

Set Up MariaDB

Postal requires a database engine to store all emails and other essential configuration data, and it will automatically provision a database for each mail server you create.

We’ll run MariaDB in a container, which is the easiest option:

sudo docker run -d \
   --name postal-mariadb \
   -p 127.0.0.1:3306:3306 \
   --restart always \
   -e MARIADB_DATABASE=postal \
   -e MARIADB_ROOT_PASSWORD=postal \
   mariadb

This will start a MariaDB instance, listening on port 3306. Make sure to change the root password to a strong one and store it securely, as you’ll need it later during the Postal configuration.

You can verify that MariaDB is running with:

sudo docker ps

You should see the postal-mariadb container listed with its status as Up.

Final Prep Steps

Once everything is in place, it's a good idea to sudo reboot your server to make sure all changes are fully applied.

If you're using Hetzner, visit the Primary IPs tab and enable protection for your assigned IPv4 and IPv6 addresses.

IP protection allows you to reuse your existing IPs if you ever need to redeploy the server.

Generating Configuration Files

Before starting the installation, Postal provides a tool that automatically generates the initial configuration files required for setup.

Run the following command, replacing mailout.example.com with the actual hostname you set earlier – the one you plan to use to access the Postal web interface:

sudo postal bootstrap mailout.example.com

The command will generate three important files inside the /opt/postal/config/ directory:

  • postal.yml: The main configuration file for Postal.
  • signing.key: A private key used by Postal to sign various internal components.
  • Caddyfile: The configuration file for the Caddy web server, which Postal uses to serve its web interface.

The only file that matters to us at this point is the postal.yml file, which we’ll use to tweak and customize Postal’s configuration.

Once the files are generated, open the postal.yml file and update the passwords for both the main_db and message_db sections.

Use the password you specified earlier when creating the MariaDB instance with Docker:

main_db:
  host: 127.0.0.1
  username: root
  password: here_comes_the_password
  database: postal

message_db:
  host: 127.0.0.1
  username: root
  password: here_comes_the_password
  prefix: postal

Save and close the file – that’s all we need to do for now.

Since the Docker setup mounts /opt/postal/config as /config, any file paths referenced inside postal.yml should start with /config instead of /opt/postal/config.

DNS Setup

Correct DNS configuration is critical. If your DNS isn’t properly set up, your emails may end up in spam – or not be delivered at all.

Postal requires several DNS records to function properly. Let’s walk through each type.

A & AAAA

We’ll begin by creating two essential DNS records: an A record and an AAAA record:

Record Type Host Value (IP Address)
A mailout.example.com Primary IPv4
AAAA mailout.example.com Primary IPv6

Then, add A and AAAA records for the MX hostname that we will use later for receiving emails through routes:

Record Type Host Value (IP Address)
A mx.mailout.example.com Primary IPv4
AAAA mx.mailout.example.com Primary IPv6

All records should point to your server’s primary IPs

PTR (Reverse DNS)

PTR (Reverse DNS) records are used by receiving mail servers to verify that your server’s IP address maps back to its hostname – this is a key check for spam prevention and essential for passing many email reputation filters.

Now, set up reverse DNS (PTR) for both IPv4 and IPv6:

  • Go to Hetzner dashboard Server Details Networking tab
  • Find the IP you want to edit, click the three dots Edit Reverse DNS
  • Set the reverse DNS to match your hostname (mailout.example.com)
  • For IPv6, append ::1 and update the hostname accordingly
Your PTR record must match your server's hostname exactly.

SPF Record

It's crucial to add an SPF (Sender Policy Framework) record to your DNS setup.

This record helps prevent your emails from being marked as spam by verifying that your server is authorized to send emails on behalf of your domain.

Add a global SPF record that includes your primary IPs:

Record Type Host Value
TXT spf.mailout.example.com "v=spf1 ip4:70.130.219.212 ip6:643c:ac1f:3f7c:bb5c::1 ~all"

This is a global SPF record you will use for any domain you add to Postal.

Return Path (MX + SPF + DKIM)

The Return Path is the email address that gets used to handle bounces (rejected or undelivered emails) for any email you send.

It tells the receiving mail server where to send error messages if it can't deliver an email (like if the recipient's email address doesn't exist, or the mailbox is full).

It's different from the From address and is usually hidden from end users – but it’s essential for proper bounce handling.

Think of it like this:

  • You send an email to someone.
  • If the email can't be delivered, a bounce email will be sent back to the return path address, not your From address.

To configure a return path domain in Postal, we need to add three DNS records:

Record Type Host Mail Server Priority
MX rp.mailout.example.com mailout.example.com 10

This tells the world where to send bounce emails for the return path.

It means bounces for emails sent from mailout.example.com will go to rp.mailout.example.com (your return path domain), which will then point to your Postal server to handle them

Record Type Host Value
TXT rp.mailout.example.com "v=spf1 a mx include:spf.mailout.example.com ~all"

This authorizes your return path domain to send emails and helps prevent bounce messages from being flagged as spam.

The last DNS record we need to add is the DKIM (DomainKeys Identified Mail) record. It is an email authentication method that uses a digital signature to verify that the email was sent and authorized by the domain owner.

This digitally signs emails from the return path domain, proving they haven’t been tampered with and are authorized by your server.

Postal provides a command that generates a DKIM record for us, which begins with the postal DKIM identifier. If you'd like to customize this identifier, open the /opt/postal/config/postal.yml file and add the following to the dns: section:

dns:
  dkim_identifier: custom

Afterward, run this command to generate the DKIM record:

sudo postal default-dkim-record

In my case, I am adding a TXT record with the host value of:

custom._domainkey.rp.mailout.example.com

The value for this record will be the output generated by the command.

Route Domain

If you wish to receive incoming emails by forwarding them directly to routes in Postal, you'll need to configure a domain and point it to your server using an MX record, like this:

Record Type Host Mail Server Priority
MX routes.mailout.example.com mailout.example.com 10

This lets incoming emails sent to routes.mailout.example.com reach Postal for routing.

DMARC

DMARC (Domain-based Message Authentication, Reporting, and Conformance) is an email authentication protocol that helps prevent email spoofing and phishing by allowing domain owners to specify how incoming emails should be handled if it fails SPF or DKIM checks.

The DMARC record specifies:

  • Whether to accept, quarantine, or reject emails that fail authentication.
  • How the domain owner wants to receive reports about these emails.

For this, add a TXT record with a host value starting with _dmarc for your domain and containing the following:

"v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com"
  • p=quarantine: This policy tells receiving mail servers to treat emails that fail DMARC checks as suspicious and place them in the recipient's spam or junk folder.
  • rua=mailto:postmaster@example.com: This specifies the email address where aggregate reports about DMARC failures will be sent, allowing the domain owner to monitor and analyze the performance of their DMARC policy.

Adding a DMARC record improves both your domain's security and email deliverability.

Postal doesn't auto-suggest a DMARC record – make sure to add this manually per domain.

Verify DNS Configuration

If you open the /opt/postal/config/postal.yml file, you will see DNS records specified under the dns: section.

This section essentially tells Postal which DNS records you’ve added and how Postal should recognize them.

By default, it includes all the necessary records – except for the Track Domain, which is used for click and open tracking. Since that’s outside the scope of this guide, you can safely comment it out.

Make sure the listed records match your DNS settings. Once they do, your DNS setup is complete.

Postal also supports a couple of optional DNS-related settings you can customize in the same file:

dns:
  domain_verify_prefix: custom-verification
  custom_return_path_prefix: customrp

By default, domain_verify_prefix is set to postal-verification and custom_return_path_prefix to psrp. You can personalize these values to reflect your domain or brand.

Starting Postal

With everything configured, it’s time to bring Postal to life.

First, initialize the database and create your admin user:

sudo postal initialize
sudo postal make-user

Once that’s done, start Postal:

sudo postal start

This command launches all the necessary components using Docker containers.

You can verify that everything is running smoothly by checking the service status:

sudo postal status

If you run the sudo docker ps command, you’ll notice several new containers up and running – these are the components that power your Postal installation.

Web Access with Caddy

To access Postal’s web interface, you’ll need to set up a web proxy.

You can use any reverse proxy you're comfortable with, but in this guide, we’ll use Caddy.

It is a lightweight, modern web server that’s easy to configure and automatically handles SSL certificates via Let's Encrypt.

Run the following command to start the Caddy container:

sudo docker run -d \
   --name postal-caddy \
   --restart always \
   --network host \
   -v /opt/postal/config/Caddyfile:/etc/caddy/Caddyfile \
   -v /opt/postal/caddy-data:/data \
   caddy

Once it’s running, Caddy will automatically request and install an SSL certificate for your domain.

You should now be able to access the Postal web interface using your hostname and log in with the admin user you created earlier.

Securing the Web Interface

Someone asked about having a kind of 2FA for the web interface, since Postal doesn't provide any 2FA feature for now, and brought up a really good point.

You can restrict access to ports 80 and 443 to a static IP (like a VPN), but this doesn't work for everyone.

So, to add an extra layer of security to the normal username and password authentication method that Postal provides, we can enable Basic Authentication through Caddy, which is easy to configure.

Inside the /opt/postal/config/ directory, you'll find the Caddyfile file. Open it and add the following, so the configuration looks like this:

mailout.examle.com {
  reverse_proxy 127.0.0.1:5000
  basicauth {
    username $2a$14$dp6WI0ldDiY/lFUL5I7q7ug/29DbwHYe5oFO6Z6mN6SRDIz/1/lgK
  }
}

Replace username with the username you want to use, and what follows the username is the password, but hashed using bcrypt, which is what Caddy uses.

While the caddy hash-password command works normally, it won’t work in this case since we run Caddy in a container, and the command is not available. As a quick alternative, you can use this online tool to hash your password.

After generating the hash, replace the hash in the example above with your own, then save and close the file.

Now, run the following two commands to stop and start Caddy again:

sudo docker stop postal-caddy
sudo docker start postal-caddy

Finally, try to access the web interface, and you should not be able to do so unless you enter the username and password you just added to the Caddyfile file.

I hope this helps solve the issue of not having 2FA. Thanks to Martin Kurz for raising this point!

Time to Mail It!

With Postal installed and running, it’s time to test your setup.

Start by creating an organization. Give it a name and click Create Organization.

Once inside:

  • Go to the Settings tab
  • Update the timezone to match your local time (this keeps log timestamps accurate)

Simple but important.

A Quick Note on Domains

Before adding your first mail server, there's something important to note.

You can add a domain:

  • At the organization level: applies to all mail servers in that organization.
  • Inside a specific mail server: applies only to that one server.
Best practice: Add domains inside each mail server. This keeps things organized and avoids DNS conflicts.

DNS can get complicated and busy when the domain is linked to all mail servers created within the organization. This is something I was advised by a member of the Postal team on GitHub.

If that ever changes and I find a good use case for organization-level domains, I’ll update this guide. But for now, let’s keep it tidy.

Create a Mail Server

Go ahead and create your first mail server. When you do:

  • Set the Mode to Live
  • Once it’s created, head to the Domains tab
  • Add your sending domain (usually the same one you used to set up Postal)

I always add my main domain here and set up a route for postmaster@example.com – that way, I can receive DMARC reports as mentioned earlier in the DNS section.

Inside your mail server, go to SettingsServer Settings, and you’ll find a field called POSTMASTER:

  • This is the contact address that shows up in bounce messages if someone sends email to a nonexistent address.
  • It defaults to postmaster@example.com, and that’s exactly what most servers expect – so I recommend keeping it as is.

For example, if someone tries to email an address you haven’t set a route for, it will bounce, and the bounce message will mention the POSTMASTER address as the contact point.

👆
What mentioned above is only relevant if you want Postal to handle incoming emails and forward them to another address, like your Gmail, using routes.

That brings us to the MX record:

  • If you're not using Postal to receive mail: you can skip adding the MX record Postal suggests when adding a new domain.
  • If you're not using another service for incoming mail: I still recommend adding the MX record.

It helps make your sending email address appear more trustworthy and legitimate.

Send a Test Email

Let’s make sure it actually works.

  1. Go to the Messages tab → Send Message
  2. In the FROM field, use something like hi@example.com (make sure that domain has been added)
  3. For the TO field, use a spam-checking tool like mail-tester.com
    1. It’ll give you a temporary email address
    2. Paste that into the TO field and click Send Message

Then head back to the test tool and check your score.

🥳
I got a 10/10. If your score is lower, don’t worry – the tool gives you a breakdown of what went wrong and how to fix it.

Configuring Routes

It's time to set up routes so you can receive incoming mail.

Inside your mail server, go to the Routing tab. You’ll see three endpoint types available:

  • HTTP
  • SMTP
  • Address

For now, we’ll focus on the Address endpoint – it’s the simplest and most practical option for forwarding emails to your inbox.

🧪
Feel free to experiment with HTTP or SMTP endpoints later. If you discover something cool, share it in the discussion section at the end. I may update this guide after deeper testing.

Create an address endpoint with the email address you'd like incoming emails forwarded to – this can be your personal or business inbox.

Create a Route

Now go to the Routes sub-tab and click Add your first route.

Fill in the following fields:

  • Name: The email address you want to receive emails at (e.g. postmaster). If you've already added your domain, just select it from the dropdown.
  • Endpoint: Select the address endpoint you just created.
  • Spam Mode: Choose your preferred option (we’ll configure spam filtering later).
  • Click Create Route.
💡
postmaster@yourdomain.com is useful for receiving DMARC reports and bounce notifications.

Test Your Route

Try sending an email to the address you configured – e.g. postmaster@example.com and check if it lands in the inbox of the address you configured as the endpoint.

You can also view incoming mail in Postal:

  • Go to the Messages tab → Incoming Messages

If you open a mail, you’ll first see a summary – like whether it's marked as spam.

A Quick Security Check

Now head to the Outgoing Messages tab and open any mail you’ve sent.

In the Properties tab, you’ll see delivery info – like whether it was sent over a secure (SSL) connection.

But here's the catch: incoming mail don’t show this same detail.

😬
That means your server isn’t receiving mail over a secure connection yet.

We’ll fix that next.

SMTP TLS

By default, Postal does not have TLS enabled for incoming mail, which means your server isn’t currently receiving emails securely.

You can confirm this using the CheckTLS tool. Just enter your domain in the eMail Target field and click the Run Test button.

The test should return an error like:

TLS is not an option on this server

You can also use this command to confirm that no SSL is configured and TLS isn’t working:

openssl s_client -connect mailout.example.com:25 -starttls smtp

This command connects to your mail server on port 25 and tries to upgrade the connection to a secure one using TLS.

If TLS isn’t enabled yet, you’ll see an error like:

Didn't find STARTTLS in server response, trying anyway...

This output means your server isn’t advertising TLS as an option, so incoming connections can’t be encrypted – exactly what we’re trying to fix next.

Why This Happens

I spent three days debugging this exact issue, only to realize it was something simple – and poorly documented in Postal’s official docs.

Here’s what you need to know:

  • When sending email, Postal doesn’t need an SSL certificate – the receiving server handles encryption.
  • But when receiving email, your Postal server is the receiving server – so you must provide a certificate to support TLS.
💡
That’s why outgoing emails show Received over an SSL connection in the Properties tab, but incoming ones don’t.

When using routes to receive emails, Postal first receives the message, then forwards it to the address endpoint you specified. This forwarding process is encrypted using an SSL certificate, because – as mentioned above – during forwarding, Postal acts as the sending server, and the receiving server (e.g. Gmail) handles the encryption. However, the initial connection between the original sender and Postal is not secure unless SMTP TLS is enabled.

Also important: without TLS, apps like WordPress won’t be able to submit email to Postal securely – so enabling it is strongly recommended, even if you're not receiving mail directly. Because WordPress first submits the email to Postal, which then forwards it to the final recipient.

Issue an SSL Certificate

We need an SSL certificate for both the hostname and the MX record. To obtain one, we’ll use Certbot.

Start by installing it:

sudo apt install certbot

When issuing a certificate, Certbot needs to use port 80 for domain verification. However, that port is currently in use by the Caddy web server, which is handling web traffic for Postal's web interface.

To work around this, we’ll temporarily stop the Caddy container, issue the certificate, and then start the container again – all in one command:

sudo docker stop postal-caddy && sudo certbot certonly --standalone -d mailout.example.com -d mx.mailout.example.com --agree-tos --email postmaster@example.com --non-interactive && sudo docker start postal-caddy

Once that completes, your certificate will be successfully issued.

You can check the certificate and obtain the certificate and key paths along with other relevant details by running the sudo certbot certificates command.

Configure Postal to Use TLS

Now that we have our SSL certificate, we need to copy it to the /opt/postal/config/ directory and rename its certificate and key files for Postal to use:

sudo cp /etc/letsencrypt/live/mailout.example.com/fullchain.pem /opt/postal/config/smtp.crt
sudo cp /etc/letsencrypt/live/mailout.example.com/privkey.pem /opt/postal/config/smtp.key

Next, change the file permissions for the certificate files:

sudo chmod 644 /opt/postal/config/smtp.*

Then, open the /opt/postal/config/postal.yml file and add the following:

smtp_server:
  tls_enabled: true
  tls_certificate_path: /config/smtp.crt
  tls_private_key_path: /config/smtp.key

Finally, restart Postal to apply the changes:

sudo postal restart

To verify that SMTP TLS is working, run this command again:

openssl s_client -connect mailout.example.com:25 -starttls smtp

You should now see a successful TLS connection.

Next, run the CheckTLS test again by entering your domain as the email target and clicking Run Test. Check the results to confirm that TLS is properly enabled and functioning.

💡
Try your route again, and now you should see an indicator under the Properties tab that the email was received over an SSL connection.

Handle Auto-Renewal with Certbot

Since Certbot uses port 80 and Caddy is running, the auto-renewal will fail unless you automate stopping/starting Caddy.

If you're curious, the configuration for auto-renewal can be found inside the /etc/letsencrypt/renewal directory.

Certbot handles auto-renewals using systemd timers instead of cron jobs. This timer run twice per day and can be checked using the following command:

sudo systemctl list-timers

You can examine the content of the timer with the following command:

sudo cat /lib/systemd/system/certbot.timer

Now, to solve this problem, we can use something called Renewal Hooks.

Inside the /etc/letsencrypt/renewal-hooks directory, you’ll find three different subdirectories:

  • deploy: This directory is for scripts that run after a certificate is successfully renewed.
  • post: Scripts in this directory run after the certificate is renewed and deployed.
  • pre: These are scripts that run before Certbot tries to renew the certificate.
We use pre to stop the Caddy container, deploy to copy the renewed certificate files, change their permissions, and restart Postal, and post for starting the Caddy container again.

Inside the pre directory, create a script called stop_caddy.sh with the following content:

#!/bin/bash
docker stop postal-caddy

Inside the deploy directory, create a script called copy_ssl_cert.sh with the following content:

#!/bin/bash

# Copy new certificate files
cp /etc/letsencrypt/live/mailout.example.com/fullchain.pem /opt/postal/config/smtp.crt
cp /etc/letsencrypt/live/mailout.example.com/privkey.pem /opt/postal/config/smtp.key

# Set correct permissions
chmod 644 /opt/postal/config/smtp.*

# Restart Postal
postal restart

Finally, inside the post directory, create a script called start_caddy.sh with the following content:

#!/bin/bash
docker start postal-caddy
💡
Don't forget to give each script execute permissions but make sure to restrict them to root only.

You can test if everything is working by first checking the expiry date using the following command:

sudo certbot certificates

This should match the expiry date of the copied certificate. To check the expiry date of the certificate in Postal, run:

openssl x509 -enddate -noout -in /opt/postal/config/smtp.crt

Then, force the renewal using the following command:

sudo certbot renew --force-renewal

Lastly, check the expiry date again to make sure it matches. If it does, the setup is working correctly.

Don’t test this on the same day you issued the SSL certificate. Wait a few days or you won’t see a change in expiry, even if the copy succeeded.

Installing SpamAssassin

For filtering spam emails, we will install and use SpamAssassin, a powerful and widely-used spam filtering tool.

By default, Postal will communicate with SpamAssassin's spamd service using a TCP socket connection (port 783).

You’ll need to install SpamAssassin on your server and enable it within Postal to begin filtering spam.

For installing SpamAssassin, use the following command:

sudo apt install spamassassin

Next, you will need to enable the SpamAssassin timer:

sudo systemctl enable --now spamassassin-maintenance.timer

It is used to update the spam rules automatically.

To enable spam checking, we need to add the following to the end of the /opt/postal/config/postal.yml file:

spamd:
  enabled: true
  host: 127.0.0.1
  port: 783

Finally, restart Postal to apply your changes:

sudo postal restart

Inside Postal web interface:

  • Go to your Organization SettingsSpam tab
  • You’ll see the Spam Threshold setting (default: 5)

A score above the threshold triggers the SPAM MODE set in your route config. A score below the threshold means the message is treated as non-spam.

💡
Remember when you created your first route and selected a SPAM MODE? That setting now takes effect.

There’s also a Spam Failure Threshold (default: 20). If a mail scores above 20, it is immediately dropped.

By default, Postal doesn't filter outgoing emails, only incoming ones, after you install and enable SpamAssassin.

What Do Spam Scores Mean?

Now, let's talk about what spam scores are before we enable spam checking for outgoing emails.

SpamAssassin works by analyzing an email and giving it a spam score.

  • The lower the score, the better the chances of the email getting delivered successfully.
  • Conversely, the higher the score, the higher the likelihood the message is labeled spam/junk.
  • A score below 5 is considered decent, while anything above 5 indicates a higher chance of being filtered out.

SpamAssassin uses over 700 tests and a variety of analytical techniques to detect spam. Each attribute it checks contributes to the overall score.

Negative scores indicate the email is less likely to be spam, while positive scores suggest possible spam.

💡
A score of 0 is neutral, meaning the factor has little impact.

When setting up SpamAssassin, the score threshold can be adjusted, with 5 being the default.

To ensure successful email delivery, aim for a spam score below 5, with scores between 0-2 being optimal. Negative scores are even better, as they indicate the email is very unlikely to be marked as spam, though they can be difficult to achieve.

I recommend starting with a threshold of 5 and gradually lowering it based on your spam checking history. For instance, if you notice your outgoing emails consistently have a score of 3, consider lowering your threshold to 4.

Over time, work on improving your score and address the factors contributing to higher scores.

Enable Spam Checking for Outgoing Mail

To filter outgoing mail:

  • Go to your mail server SettingsAdvanced Settings
  • Find the Outbound Spam Threshold
  • Set it to 5
  • Click Save Server

Now outgoing mail will also be checked using SpamAssassin before it’s sent out.

This is helpful for catching mistakes early, like misconfigured headers, bad links, or overly promotional content.

System Email Configuration

To enable system-generated emails (such as password reset links) in Postal, you'll need to configure the smtp: section in the Postal configuration file.

Open the /opt/postal/config/postal.yml file and scroll to the bottom of it, and you’ll find the smtp: section. This is where you'll define the SMTP server Postal should use for sending system emails.

Although you could use an external SMTP provider, we’re going to use Postal itself to keep everything self-hosted.

Inside your Postal web interface:

  1. Go to your Mail Server
  2. Click the Credentials tab
  3. Click Add Credential
    1. Leave the TYPE set to SMTP
    2. Name it something like System to keep things organized
  4. Click Create Credential – a password will be generated automatically

Now go to the Help tab → Sending Email sub-tab. You’ll see the SMTP settings needed to complete the smtp: section in your postal.yml file.

Back in /opt/postal/config/postal.yml, fill out the smtp: section with the info you just retrieved. For the from_address, it's a good idea to use something like system@example.com.

Make sure to also change the port from 2525 to 25, since our SMTP server uses port 25 for sending emails.

Finally, restart Postal to apply your changes:

sudo postal restart

Now, try logging out of the Postal web interface and resetting your password. You should receive an email with a link to set a new one.

Enabling UFW Firewall

We finally want to enable the UFW firewall on our server, and I've left this to the end so you can check if everything still works perfectly after enabling it.

We only need to allow incoming connections, as outgoing traffic is allowed by default with UFW.

To configure this, we need to allow incoming traffic on the following ports: 22 (SSH), 25 (SMTP), 80 (HTTP), and 443 (HTTPS).

Use the following commands to allow these connections and enable the firewall:

sudo ufw limit 22/tcp
sudo ufw allow 25/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Sometimes, UFW may require a reboot to work correctly. So, if you encounter any issues, try rebooting the server.

Disaster Recovery

The final step in setting up your SMTP server is planning for disaster recovery. Think about what you’d do if something goes wrong – like a server breach, a misconfiguration, a bad update, or even something out of your control, like a fire in your provider’s data center. You need a way to quickly bring everything back online.

We already have our primary IPs secured – they stay with us no matter what, which is great. Now, we need a reliable way to restore the server and assign those same IPs to it. The best way to do this is by using snapshots.

In Hetzner, you can go to your server’s Snapshots tab and create a snapshot, which is basically a copy of your server's disk at that moment.

If something happens, you can spin up a new server from that snapshot, assign the primary IPs, and it should work immediately – no need to change any DNS settings.

Make sure to test this process a few times so you’re comfortable with it.

Snapshots are created manually. If you want automatic backups, you can enable Hetzner's backup feature. It costs 20% extra, but it keeps daily backups for a week. Once the week is over, old backups are replaced by new ones. You can convert any of these backups into a snapshot, which you can then use to restore your server.

If you delete the server, all its backups are deleted too. Always convert at least one backup into a snapshot before deleting anything. Also, enable deletion protection on your server to avoid losing everything by mistake.

Conclusion and Final Thoughts

Congratulations on reaching the end!

Remember to monitor your setup, fine-tune your spam settings, and keep your SSL certificate renewed.

And just as important – test your disaster recovery plan regularly to make sure everything still works as expected. It’s better to catch issues during a test than during a real outage.

If you run into any issues or need further help, feel free to revisit this guide or reach out for assistance. Happy emailing!

If you found value in this guide or have any questions or feedback, please don't hesitate to share your thoughts in the discussion section.