You are in the process of putting together your application. While designing your authorization solution, you realize you will need to send emails to potential clients.
Using a third-party service (like SendGrid or Mailgun) to cover your needs for now looks pretty attractive. After all, you don’t have any users yet, they offer free tiers, and their implementation is only a few lines of code. Plus, you don’t want to mess around with setting up an email server when you could be working on architecture or deployment.
They are great tools, and depending on your needs, you might even end up using them in production.
Although, oftentimes, those needs will override the usefulness of these services – whether because of API restrictions, package limitations, higher-ups saying no, or simply the price.
So if you run into the same issues, you too might start running your own email server.
All you need is a Kubernetes cluster with a fixed outbound IP address and your own domain.
Before we do anything, we need to talk about why we can’t just spin up a Docker container locally and send out emails freely. It’s because of scammers and spammers.
Email providers don’t like people trying to steal from you or fill your inbox with junk.
That’s why they require a certain level of authentication, to see who is sending the mail and from where. If these requirements are not met, your mail could end up in the spam folder (or not even be delivered).
With that in mind, we also need to set up our “credentials” to make sure users receive whatever important system emails we send them.
What we will need:
- PTR record for our cluster
- SPF record
- DKIM
- DMARC
PTR Record for Your Cluster (Reverse DNS Check)
Email providers usually perform both forward and reverse DNS checks.
That means when an email is received, they check if the domain it was sent under resolves to a valid IP address (forward). Then they check whether that IP address has a record that matches with the domain (reverse).
Forward DNS proves the domain exists and is legitimate, while reverse DNS proves the sending IP belongs to a legitimate mail server.
They are set up wherever your email server is hosted.
SPF Record (Sender Policy Framework)
SPF prevents email spoofing by listing which IP addresses/servers are authorized to send emails for your domain. Email providers check it to see if emails claiming to be from your domain actually came from authorized servers.
They are published as a TXT record in your DNS.
DKIM (DomainKeys Identified Mail)
Adds a digital signature to emails that uses cryptographic signatures to verify authenticity.
Outgoing emails are signed with a private key by the server. When the email provider receives the mail, it verifies that it hasn’t been tampered with by using the corresponding public key published in the sender’s DNS records.
This helps establish domain reputation over time.
DMARC (Domain-based Message Authentication, Reporting & Conformance)
DMARC builds on SPF and DKIM to provide complete domain protection. It tells receiving email servers what to do when emails fail SPF or DKIM checks.
Properly setting up DMARC reduces the likelihood of your domain being blacklisted and decreases false positives in spam filtering.
With that out of the way, it’s time to dig in.
First, let’s create a little Node app to send test emails.
npm init -y && npm install node-mailer
index.js
import { createTransport } from "nodemailer";
(async () => {
const transporter = createTransport({
host: "127.0.0.1",
port: 9090,
secure: false,
tls: {
rejectUnauthorized: false
}
});
const info = await transporter.sendMail({
from: '"Test" <test@expendabledomain.com>',
to: "your_email@example.com",
subject: "Hello ✔",
text: "Hello world?",
html: "<b>Hello world?</b>",
});
console.log("Message sent:", info.messageId);
})();
Setting up the Cluster
You can skip this part if you already have a running cluster.
We will use Linode to host a cluster, but you can use any other cloud provider – just follow the respective steps.
Install the tools we need:
Authenticate yourself with your cloud provider:
linode-cli configure
After authentication, create a cluster:
linode-cli lke cluster-create \
--label test-cluster \
--k8s_version 1.33 \
--node_pools.type g6-standard-1 \
--node_pools.count 1
After the cluster starts, get the cluster ID and IP:
linode-cli lke clusters-list
Download kube config for kubectl:
linode-cli lke kubeconfig-view CLUSTER_ID --text --no-headers | base64 -d > ~/.kube/linode-config
export KUBECONFIG=~/.kube/linode-config
Great, the cluster is running and accessible with kubectl. Test it:
kubectl get nodes
If you haven’t yet, you need to get yourself a domain. Pick a name and register it on whichever domain registrar you want.
I will use ‘expendabledomain.com’. Replace it with yours.
Set reverse dns record:
linode-cli networking ip-update CLUSTER_IP --rdns mail.expendabledomain.com
Let’s create public and private keys for DKIM first.
You can create RSA key pairs with a lot of tools but I prefer using opendkim because I’m lazy. Follow or use your preferred method to create one.
Install opendkim for your system and then:
opendkim-genkey -t -s mail -d expendabledomain.com
You will see two new files have been created. A .private one and a .txt file.
We will use the private key in our postfix server and the .txt one to create a record in our dns.
Let’s move to setting up our email server.
We will use this postfix chart as our server.
Postfix is a fast and secure mail server that will let us send our system messages to our users. It has extensive customization while simultaneously offering quick setup.
Download the chart and copy it to your project.

The values.yaml file contains every bit of configuration that we need to set.
In the config section we will set the following:
config:
general:
ALLOWED_SENDER_DOMAINS: "expendabledomain.com"
DKIM_SELECTOR: "mail"
ALLOWED_SENDER_DOMAINS is pretty self explanatory, it just specifies which domains are allowed to send email through the Postfix server. Set it to your domain.
DKIM_SELECTOR defines which public key should be used to verify a digitally signed email message. We can have multiple DKIM keys present at all times to aid with key rotation or sending multiple types of mails (system, marketing).
We are going to need just one in this case.
config:
postfix:
myhostname: "mail.expendabledomain.com"
mydomain: "expendabledomain.com"
smtpd_recipient_restrictions: "permit_mynetworks, reject_unauth_destination"
‘myhostname’ is a specific hostname that the SMTP server will use to identify itself
Emails sent will show ‘mail.expendabledomain.com’ as the party they were received from.
‘mydomain’: defines the local internet domain name and is used by Postfix to determine what domains it considers “local.”
‘smtpd_recipient_restrictions’: due to in-built spam protection in Postfix you will need to specify sender domains, the domains you are using to send your emails from, otherwise Postfix will refuse to start.
config:
opendkim:
Selector: "mail"
Domain: "expendabledomain.com"
KeyFile: "/etc/opendkim/keys/expendabledomain.com.private"
Pretty self explanatory as well, setting the selector, domain and location of the private key for the server to setup DKIM signing.
The keyfile will point to the mounted file we set here:
mountSecret:
enabled: true
path: /etc/opendkim/keys
data:
expendabledomain.com.private: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDhhm5eVNpYWOzX/hmXIsjFArA3o7H0yswPNEI=
-----END PRIVATE KEY-----
Just copy your DKIM private key here.
Keep in mind that this is an example and you should never commit sensitive data with your code in production. Pull secrets from an external secret management system (azure key vault, aws secrets manager, hashicorp vault etc) and mount them when pods are deployed.
The last configuration we need is to set
persistence:
enabled: false
We don’t need persistence for our use case now, but in prod you should definitely enable it.
Persistence ensures that if your container goes down, any emails that were in the process of being delivered won’t be lost and will continue processing when the container starts back up.
Now that we have configured postfix, it is ready to be deployed to our cluster.
helm install postfix ./postfix
You can check service’s status with:
kubectl get pods
All we need to do is creating the proper dns records in our domain.
Create an address record in your domain with your cluster’s ip address we got earlier.
NAME TYPE VALUE
mail.expendabledomain.com A <CLUSTER_IP>
Create a text record for spf:
NAME TYPE VALUE
expendabledomain.com TXT "v=spf1 ip4:<CLUSTER_IP> ~all"
Then for DKIM with your public key:
NAME
mail._domainkey.expendabledomain.com
TYPE
TXT
VALUE
"v=DKIM1; h=sha256; k=rsa; " "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4YZuXlTaWFjs1+55mqY1owGSOpwdwfbip3Jq2aN+xWG33ErMrAMr6XWnaBz4HmsJwbqlAML9nglb7/fOSH" "QlIY+0uoZMWTOmNWalkTK/+vRjpMohjVFi9K+0f+14msRv0Uk8/mn8fgIDtSqGU/9XvSiSf/QQIDAQAB"
Lastly a DMARC record to tie everything together:
NAME TYPE VALUE
_dmarc.expendabledomain.com TXT "v=DMARC1; p=none; rua=mailto:dmarc@expendabledomain.com"
This is a basic DMARC record that lets us monitor our email flow, in production you will have to set a stricter configuration that will fit your needs.
With all this our testing ground is set, all that’s left is to see it working.
We will need to forward the email server’s port in our cluster for our nodejs app to be able to communicate with it.
kubectl port-forward postfix-mail-0 9090:587
Don’t forget to change the ‘to’ email address in the index.js file to be able to check the inbox.
Let’s test it out!
npm run test
You should see the email successfully arriving into your inbox:
Open the mail and check out its contents to see that we have successfully set all the requirements we needed.


And that’s it! You have successfully created your own email server!
Final Thoughts
Before jumping head-first into hosting your own email service, you should carefully consider your needs and weigh your options.
Using third-party services removes the burden of configuration and management.
Although, it comes with costs and limitations (I would hate not being able to send password reset emails for my users just because I accidentally hit a monthly limit).
On the other hand, if you have small use cases (like sending authentication or system update messages) and a flexible architecture, hosting your own solution can be viable.
You have to configure it once, yes, but management is minimal afterwards, and there are no third-party limitations or monthly fees.
Hope you enjoyed setting up your own email server and gained some new tools for your arsenal.


