Serving email on Kubernetes with Mailu

How I set up mailu on Kubernetes.

vados

20 minute read

tl;dr - Setting up Mailu on Kubernetes was pretty simple, once TLS and Ingress are all set up. It’s just a matter of configuring the ingress controller, adding the right ingress resources, and making the right resource configuration for Mailu. I encounter some (mostly self-inflicted) issues along the way, but you can find the resource config that worked for me at the end.

Up until now on every VPS that I’ve purchased/used, I’ve manually set up Postfix and Dovecot and all the related services on the machine, navigating documentation, setting up additional users, adding virutal mailboxes, etc. While I valued the experience (though most of it was reading thorugh documentation/guides), it felt kind of dirty. Why did it take so much finangling to get to a mail set up that wasn’t bad? Why wasn’t there some easy-to-use interface to configure Postfix with some intelligble and clear settings names? I don’t fault the Postfix project for being what it is (it’s a tremendous piece of software that’s been offered to me completely for free), but I often longed for a higher level wrapper around postfix + dovecot combo that I could trust.

There are lots of ways I could automate/manage this little bit of complexity, but this nail is looking like a perfect fit for the containers + Kubernetes hammer. At the high level, I probably just need to run some docker containers (like postfix and dovecot and whatever else) and set aside some hostPath volumes for Kubernetes to use, and make sure everything is hooked up the right way.

There are a few options for “pre-packaged postfix” (if you can call it that), here are a few of them:

NOTE If you’re looking for more awesome self-hosted stuff, the awesome-selfhosted repo is pretty amazing.

These options vary somewhat – mailinabox from what I can tell basically just installs everything and does it well enough that you don’t have to touch it again. mailcow and mailu offer what mailinabox offers plus a web UI to manage your configuration and do things to the postfix installation – managing accounts, etc.

Mailu (previously known as Freeposte.io) was actually one of the first solutions I came across and with their emphasis on use of containers I was pretty happy to choose them.

Creating the Kubernetes resource configuration

Unfortunately, the README instructions for Mailu only mentioned Docker Compose, but luckily a brave soul posted their kubernetes resource configuration so I had a place to start. Here’s the configuration in case it gets lost to time:

apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: default
  labels:
    app: mailu
    role: mail
    tier: backend
spec:
  selector:
    app: mailu
    role: mail
    tier: backend
  ports:
  - name: redis
    port: 6379
    protocol: TCP

---

apiVersion: v1
kind: Service
metadata:
  name: antispam
  namespace: default
  labels:
    app: mailu
    role: mail
    tier: backend
spec:
  selector:
    app: mailu
    role: mail
    tier: backend
  ports:
  - name: antispam
    port: 11333
    protocol: TCP

---

apiVersion: v1
kind: Service
metadata:
  name: antivirus
  namespace: default
  labels:
    app: mailu
    role: mail
    tier: backend
spec:
  selector:
    app: mailu
    role: mail
    tier: backend
  ports:
  - name: antivirus
    port: 3310
    protocol: TCP

---

apiVersion: v1
kind: Service
metadata:
  name: imap
  namespace: default
  labels:
    app: mailu
    role: mail
    tier: backend
spec:
  selector:
    app: mailu
    role: mail
    tier: backend
  ports:
  - name: imap-auth
    port: 2102
    protocol: TCP
  - name: imap-transport
    port: 2525
    protocol: TCP

---

apiVersion: v1
kind: Service
metadata:
  name: mailu
  namespace: default
  labels:
    app: mailu
    role: mail
    tier: backend
spec:
  selector:
    app: mailu
    role: mail
    tier: backend
  ports:
  - name: http
    port: 80
    protocol: TCP
  - name: imap-default
    port: 143
    protocol: TCP
  - name: imap-ssl
    port: 993
    protocol: TCP
  - name: sieve
    port: 4190
    protocol: TCP
  - name: smtp
    port: 25
    protocol: TCP
  - name: smtp-ssl
    port: 465
    protocol: TCP
  - name: smtp-starttls
    port: 587
    protocol: TCP

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mailu
  namespace: default
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mailu
        role: mail
        tier: backend
    spec:
      containers:
      - name: admin
        image: mailu/admin:stable
        imagePullPolicy: Always
        env:
          - name  : DOMAIN
            value : example.com
          - name  : HOSTNAME
            value : mail.example.com
          - name  : POSTMASTER
            value : admin
          - name  : SECRET_KEY
            value : ChangeMeChangeMe
          - name  : DEBUG
            value : "True"
        volumeMounts:
          - name: maildata
            mountPath: /data
          - name: dkim
            mountPath: /dkim
          - name: certs
            mountPath: /certs
            readOnly: true
#          - name: docker
#            mountPath: /var/run/docker.sock
#            readOnly: true
        ports:
          - name: http
            containerPort: 80
            protocol: TCP
      - name: redis
        image: redis:latest
        imagePullPolicy: Always
        volumeMounts:
          - mountPath: /data
            name: redisdata
        ports:
          - containerPort: 6379
            name: redis
            protocol: TCP
      - name: imap
        image: mailu/dovecot:stable
        imagePullPolicy: Always
        env:
          - name  : DOMAIN
            value : example.com
          - name  : HOSTNAME
            value : mail.example.com
          - name  : POSTMASTER
            value : admin
        volumeMounts:
          - mountPath: /data
            name: maildata
          - mountPath: /mail
            name: mailstate
          - mountPath: /overrides
            name: overrides
          - mountPath: /certs
            name: certs
            readOnly: true
        ports:
          - containerPort: 2102
            name: imap-auth
            protocol: TCP
          - containerPort: 2525
            name: imap-transport
            protocol: TCP
          - containerPort: 143
            name: imap-default
            protocol: TCP
          - containerPort: 993
            name: imap-ssl
            protocol: TCP
          - containerPort: 4190
            name: sieve
            protocol: TCP
      - name: smtp
        image: mailu/postfix:stable
        imagePullPolicy: Always
        env:
          - name  : DOMAIN
            value : example.com
          - name  : HOSTNAME
            value : mail.example.com
          - name  : MESSAGE_SIZE_LIMIT
            value : "50000000"
          - name  : RELAYHOST
            value : ""
        volumeMounts:
          - mountPath: /data
            name: maildata
          - mountPath: /overrides
            name: overrides
          - mountPath: /certs
            name: certs
            readOnly: true
        ports:
          - name: smtp
            containerPort: 25
            protocol: TCP
          - name: smtp-ssl
            containerPort: 465
            protocol: TCP
          - name: smtp-starttls
            containerPort: 587
            protocol: TCP
      - name: milter
        image: mailu/rmilter:stable
        imagePullPolicy: Always
        ports:
          - name: milter
            containerPort: 9900
            protocol: TCP
        volumeMounts:
          - name: maildata
            mountPath: /data
          - name: dkim
            mountPath: /dkim
          - name: overrides
            mountPath: /overrides
          - name: certs
            mountPath: /certs
            readOnly: true
      - name: antispam
        image: mailu/rspamd:stable
        imagePullPolicy: Always
        ports:
          - name: antispam
            containerPort: 11333
            protocol: TCP
        volumeMounts:
          - name: filter
            mountPath: /var/lib/rspamd
      - name: antivirus
        image: mailu/clamav:stable
        imagePullPolicy: Always
        ports:
          - name: antivirus
            containerPort: 3310
            protocol: TCP
        volumeMounts:
          - name: filter
            mountPath: /data
      volumes:
        - name: redisdata
          emptyDir: {}
        - name: maildata
          emptyDir: {}
        - name: mailstate
          emptyDir: {}
        - name: overrides
          emptyDir: {}
        - name: dkim
          emptyDir: {}
        - name: filter
          emptyDir: {}
        - name: certs
          secret:
            items:
              - key: tls.crt
                path: cert.pem
              - key: tls.key
                path: key.pem
            secretName: mail-example-com-letsencrypt-ssl
#        - name: docker
#          hostPath:
#            path: /var/run/docker.sock
      imagePullSecrets:
        - name: myregistrykey

My first step was to copy this resource configuration that was posted and try my best to understand it – this meant going through all the resources, trying to figure out what they were for and what they were meant to accomplish, figuring how the milter might speak to the mailu/postfix image, etc. Outside of removing the lines around the registry pull key (since the official docker repo is fine for this), the configuration was largely usable.

Next step was to create directories for all the important information, along with updating the name of the secret that it was going to use for certs. Since I already set up TLS/SSL certs, I knew the secret name I wanted to use was letsencrypt-certs-all. Note that the SSL setup is required, the imap container will fail to start if it can’t load the cert files.

After changing all the information, I was able to create the resources successfully. I am definitely happy about how easy it was – the container hype (and kubernetes hype) are paying off wonderfully.

Connecting to and configuring Mailu

Now that the container seems to be running properly (at least according to kubectl get pods and kubectl describe pod), it’s time to kubectl port-forward into the mailu admin container and start checking out/configuring things. Once port forwarding in I was greeted with the login screen:

Mailu login page

But a bit of a problem – I don’t remember creating a user…

Looking at the Mailu setup guide showed me that there was more setup I needed to do, particularly running docker-compose run --rm admin python manage.py admin root example.net password. Of course, that needs to be run from inside the mailu admin container, so you can either kubectl exec that command directly or kubectl exec -it <container> -- /bin/bash to shell in and do it there.

Once I’m logged in, I can see the main Mailu interface:

Mailu main page

Great success! One thing that immediately sticks out to me is the Mail Domains page – one of the things I’ve found somewhat difficult was managing virtual domains in Postfix, it’s awesome that they’ve offered a UI for that (though maybe that’s a pretty obvious feature a mail administrator would want).

Making sure Mailu is accessible from the outside world

Now that the admin interface is working properly, I need to add a few things to make sure it’s accessible, and the mail server itself is accessible from the outside world via SMTP/SMTPS, IMAPS, and the associated ports. You guessed it, it’s time to set up some Kubernetes Ingress Resources!

Making the Admin interface acecssible

Here’s my first attempt at the ingress for the admin interface:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: mailu-admin-ing
spec:
  tls:
- hosts:
  - "mail.example.com"
  secretName: letsencrypt-certs-all
rules:
- host: "mail.example.com"
  http:
    paths:
  - path: "/admin"
    backend:
      serviceName: mailu
      servicePort: 80

First quick step was to test the ingress to make sure it worked, and it did!

After checking the admin interface I also ran upon the DKIM generation features that Mailu comes with and was super impressed! Yet another thing I don’t have to go through the trouble of setting up on my own.

Making the actual mail server accessible to the outside world

The NGINX Ingress controller I’ve been using up until this point is (obviously) using NGINX under the covers, so making sure email gets to the postfix service/pod/underlying docker container means that I need to forward email traffic through NGINX. I’ve actually never dealt with trying to put email traffic through NGINX before, so this prompted some internet searching on how to do email stuff with NGINX – I assumed there might be a bunch of different configuration variables/keywords I needed to use.

Turns out it’s not so big a deal, since it’s all TCP in the end, I just needed the general TCP ingress/loadbalancing mentioned in the README for the NGINX ingress controller.

This meant updating my Kubernetes system-level resource spec for the NGINX ingress controller:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-ingress-conf
  namespace: kube-system
  labels:
    k8s-app: nginx-ingress-controller
data:
  587: "default/mailu:587"
  993: "default/mailu:993"
  465: "default/mailu:465"

After applying the the updated Kubernetes ConfigMap however, using nmap I could see that the port still wasn’t open/listening from my home computer. This small issue boiled down to making sure the right ports were exposed on the actual containers (for some reason they weren’t), so one more change to the Mailu resource config and I was good to go.

At this point now, the Admin interface as well as the actual mail server (the containers that are running postfix, etc) are accessible from the outside world. Now it’s time to actually try sending emails and doing stuff.

SIDETRACK: I wonder what my utilization looks like

If you’ve followed along with this blog you’ll know that I’ve only recently discovered the wonders and value of dedicated hosting (over VPS providers) – so I was curious to see just how much of the massive server Iv’e rented is now in use. To access my cAdvisor I ran a quick kubectl port-forward kube-apiserver-<port of your server> 5000:4194 -n kube-system.

I found that about 30% of the RAM was being used, and just about no CPU, so I was pretty happy to see that. At this point theres a bunch of things running on the server at this point (all under Kubernetes), but I was glad to see that I wasn’t anywhere close to maxing it out. If you haven’t considered dedicated hosting, check out the Hetzner robot auction – it’s pretty awesome.

Trying to log in with Thunderbird

So now that the postfix server is reachable from the world wide web, it’s time to see if I can actually log in and use my mail server for what I want it for: sending emails.

Mozilla Thunderbird is one of the greatest pieces of software ever written, in my humble oopinion, and my go-to choice for an email client. Despite how great it is, it has a pretty annoying UI bug/feature that I almost always get stuck on whenever I do new manual configurations, so I ran into that again here, and after some head-scratching and finding that bug report again, I was able to get past it and make a manual account. Basically, if you put in every single setting (nothing can be on “auto”), then Thunderbird will let you by with a manual configuration. BTW, auto configuration is actually a file that Thunderbird is just expecting to find, there’s even a Mailu issue about it.

Even with the configuration set up in Thunderbird, I would consistently fail to log in to my account, and couldn’t figure out why. My testing methodology was this:

  1. kubectl logs -f mailu-3284661181-zwb76 imap (open this in a console and watch for errors)
  2. Delete/recreate user in the Mailu admin console

After a while I realized that I was trying to log in using a system user when what I was supposed to be using was the user as configured in Mailu – i.e. my “username” was “user@example.com”, NOT just “user”. Felt pretty dumb, but was glad to be past this minor hiccup.

Update DNS provider with changed DKIM and DNS entries

Since everything was semi-working at this point I started porting all my DNS configuration over to point at this new mail server (so mail.example.com might have bene pointed at somewhere else but now I was pointing it at the server running Kubernetes). Along with this it’s important to update your DKIM-related DNS entries to what Mailu has generated as well, so your emails have a better chance of ending up in Inboxes and not spam folders.

After doing this, it was time to do the most important thing: send an actual email.

Sending an email

After trying to send an email from Thunderbird I got an error – par for the course with me setting up anything. Weirdly enough, Here’s what the postfix container logs looked like:

postfix/smtpd[191]: warning: SASL: Connect to inet:imap:2102 failed: Operation timed out
postfix/smtpd[191]: fatal: no SASL authentication mechanisms
postfix/master[145]: warning: process /usr/lib/postfix/smtpd pid 191 exit status 1
postfix/master[145]: warning: /usr/lib/postfix/smtpd: bad command startup -- throttling

Uh oh, Sending emails is broken

Seeing the errors above, I was wondering if there was some setup that I failed to do… Why would I have absolutely no SASL authentication mechanisms? Isn’t that what Mailu was offering? I logged in already for Thunderbird, I wonder why sending email wasn’t working. Then I had a thought.. is receiving mail even working? I tried to send myself something from my gmail account and here’s what I saw:

postfix/anvil[189]: statistics: max connection rate 1/60s for (submission:172.17.0.4) at Aug 16 21:03:50
postfix/anvil[189]: statistics: max connection count 1 for (submission:172.17.0.4) at Aug 16 21:03:50
postfix/anvil[189]: statistics: max cache size 1 at Aug 16 21:03:50

That output looks just fine, nothign particularly error-related there, but there was nothing in my mailbox. Now it’s pretty clear that not everything is set up right. One of the first things I noticed in the logs was that it couldn’t access the milter at port 9900… Maybe I’m having more connection problems than just the milter?

Uh oh, Sending & receiving emails (everything?) is broken

Welp, looks like in actuality, neither sending nor receiving emails is working properly.

One of the best things that I’ve figured out while working with Kubernetes is if you can’t figure out what’s going wrong, jump into the container and poke around! A quick kubectl exec -it <pod> -- /bin/bash and I was into the actual postfix container, poking around and looking at the configurations in /etc/postfix. To be honest, going through the Postfix documentation lots of times and touching on the SASL README more than once also helped me to understand what I was/wasn’t looking for here.

After some looking at the configuration, and looking to more logs, I realized that imap:2102 (which was in the configuration) was actually timing out/inaccessible from inside the postfix container itself. The imap service is supposed to expose dovecot – pretty big issue if it’s unreachable, maybe I am receiving emails (I don’t see any DAEMON errors in gmail), but they’re just not getting served up through IMAP properly?

Now it’s time to figure out why the imap service isn’t working properly. After checking kubectl describe svc imap, it looks like the service has endpoints, has an IP, and is accessible from a tutum/curl container (a container I routinely spin up just to curl things and make sure they’re accessible). So weirdly enough, now I’m convinced the imap service IS working properly and is accessible, but postfix just can’t access it for some reason.

I tried one more change – updating /etc/postfix/main.cf and changing the smtpd_sasl_path to the IP of the service, and then to the endpoint of the actualmailu` container. After doing that, the errors changed:

postfix/smtpd[206]: connect from unknown[172.17.0.4]
postfix/smtpd[206]: SSL_accept error from unknown[172.17.0.4]: lost connection
postfix/smtpd[206]: lost connection after CONNECT from unknown[172.17.0.4]
postfix/smtpd[206]: disconnect from unknown[172.17.0.4] commands=0/0
postfix/smtpd[206]: connect from unknown[172.17.0.4]
postfix/smtpd[206]: fatal: host/service milter/9900 not found: Name does not resolve
postfix/master[146]: warning: process /usr/lib/postfix/smtpd pid 206 exit status 1

NOW it’s complaining about not finding the milter – which makes me think that things got better. I figured the milter might have had very similar issues so now I try the same trick for the milter as well (using the IP of the container) and find that it works, and the errors change AGAIN:

postfix/smtpd[224]: warning: unknown[172.17.0.4]: SASL LOGIN authentication failed: UGFzc3dvcmQ6
postfix/smtpd[224]: warning: unknown[172.17.0.4]: SASL PLAIN authentication failed: UGFzc3dvcmQ6
postfix/smtpd[224]: warning: unknown[172.17.0.4]: SASL LOGIN authentication failed: UGFzc3dvcmQ6
postfix/smtpd[224]: warning: unknown[172.17.0.4]: SASL PLAIN authentication failed: UGFzc3dvcmQ6
postfix/smtpd[224]: warning: unknown[172.17.0.4]: SASL LOGIN authentication failed: UGFzc3dvcmQ6

Now it’s telling me that my login was failing – not that an SASL method was missing and not that the mitler was inaccessible. Fixing this ended up requiring going back to my Thunderbird configuration and changing the username from user to user@example.com – that was just a bit of an error on my part, forgetting that the username needed to be the full email address. Once I got that fixed, sending emails was finally working!

Here’s where things get a little fuzzy – eventually I was able to replace the smtpd_sasl_path configuration variable with inet:imap:2102 and it started working properly. I’m not sure exactly what happened to make the service that should have been working to start doing the right thing, but I didn’t leave any notes so I can’t write about it here :(.

The final configuration

Here’s the final massive configuration that worked for me:

---

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: mailu-admin-ing
  labels:
    app: mailu
    role: mail
    tier: backend
spec:
  tls:
  - hosts:
    - "mail.example.com"
    secretName: letsencrypt-certs-all
  rules:
  - host: "mail.example.com"
    http:
      paths:
      - path: "/admin"
        backend:
          serviceName: mailu-admin
          servicePort: 80

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mailu-redis
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mailu-redis
        role: mail
        tier: backend
    spec:
      containers:
      - name: redis
        image: redis:latest
        imagePullPolicy: Always
        volumeMounts:
          - mountPath: /data
            name: redisdata
        ports:
          - containerPort: 6379
            name: redis
            protocol: TCP
      volumes:
        - name: redisdata
          hostPath:
            path: /var/data/mailu/redisdata

---

apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app: mailu-redis
    role: mail
    tier: backend
spec:
  selector:
    app: mailu
    role: mail
    tier: backend
  ports:
  - name: redis
    port: 6379
    protocol: TCP

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mailu-imap
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mailu-imap
        role: mail
        tier: backend
    spec:
      containers:
      - name: imap
        image: mailu/dovecot:stable
        imagePullPolicy: Always
        env:
          - name  : DOMAIN
            value : example.com
          - name  : HOSTNAME
            value : mail.example.com
          - name  : POSTMASTER
            value : admin
        volumeMounts:
          - mountPath: /data
            name: maildata
          - mountPath: /mail
            name: mailstate
          - mountPath: /overrides
            name: overrides
          - mountPath: /certs
            name: certs
            readOnly: true
        ports:
          - containerPort: 2102
          - containerPort: 2525
          - containerPort: 143
          - containerPort: 993
          - containerPort: 4190
      volumes:
        - name: maildata
          hostPath:
            path: /var/data/mailu/maildata
        - name: mailstate
          hostPath:
            path: /var/data/mailu/mailstate
        - name: overrides
          hostPath:
            path: /var/data/mailu/overrides
        - name: certs
          secret:
            items:
              - key: tls.crt
                path: cert.pem
              - key: tls.key
                path: key.pem
            secretName: letsencrypt-certs-all

---

apiVersion: v1
kind: Service
metadata:
  name: imap
  labels:
    app: mailu
    role: mail
    tier: backend
spec:
  selector:
    app: mailu-imap
    role: mail
    tier: backend
  ports:
  ports:
  - name: imap-auth
    port: 2102
    protocol: TCP
  - name: imap-transport
    port: 2525
    protocol: TCP
  - name: imap-default
    port: 143
    protocol: TCP
  - name: imap-ssl
    port: 993
    protocol: TCP
  - name: sieve
    port: 4190
    protocol: TCP

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mailu-smtp
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mailu-smtp
        role: mail
        tier: backend
    spec:
      containers:
      - name: smtp
        image: mailu/postfix:stable
        imagePullPolicy: Always
        env:
          - name  : DOMAIN
            value : example.com
          - name  : HOSTNAME
            value : mail.example.com
          - name  : MESSAGE_SIZE_LIMIT
            value : "50000000"
          - name  : RELAYHOST
            value : ""
        volumeMounts:
          - mountPath: /data
            name: maildata
          - mountPath: /overrides
            name: overrides
          - mountPath: /certs
            name: certs
            readOnly: true
        ports:
          - name: smtp
            containerPort: 25
            protocol: TCP
          - name: smtp-ssl
            containerPort: 465
            protocol: TCP
          - name: smtp-starttls
            containerPort: 587
            protocol: TCP
      volumes:
        - name: maildata
          hostPath:
            path: /var/data/mailu/maildata
        - name: overrides
          hostPath:
            path: /var/data/mailu/overrides
        - name: certs
          secret:
            items:
              - key: tls.crt
                path: cert.pem
              - key: tls.key
                path: key.pem
            secretName: letsencrypt-certs-all

---

apiVersion: v1
kind: Service
metadata:
  name: smtp
  labels:
    app: mailu
    role: mail
    tier: backend
spec:
  selector:
    app: mailu-smtp
    role: mail
    tier: backend
  ports:
  - name: smtp
    port: 25
    protocol: TCP
  - name: smtp-ssl
    port: 465
    protocol: TCP
  - name: smtp-starttls
    port: 587
    protocol: TCP

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mailu-milter
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mailu-milter
        role: mail
        tier: backend
    spec:
      containers:
      - name: milter
        image: mailu/rmilter:stable
        imagePullPolicy: Always
        ports:
          - name: milter
            containerPort: 9900
            protocol: TCP
        volumeMounts:
          - name: maildata
            mountPath: /data
          - name: dkim
            mountPath: /dkim
          - name: overrides
            mountPath: /overrides
          - name: certs
            mountPath: /certs
            readOnly: true
      volumes:
        - name: maildata
          hostPath:
            path: /var/data/mailu/maildata
        - name: overrides
          hostPath:
            path: /var/data/mailu/overrides
        - name: dkim
          hostPath:
            path: /var/data/mailu/dkim
        - name: certs
          secret:
            items:
              - key: tls.crt
                path: cert.pem
              - key: tls.key
                path: key.pem
            secretName: letsencrypt-certs-all

---

apiVersion: v1
kind: Service
metadata:
  name: milter
  labels:
    app: mailu-milter
    role: mail
    tier: backend
spec:
  selector:
    app: mailu-milter
    role: mail
    tier: backend
  ports:
  - name: milter
    port: 9900
    protocol: TCP

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mailu-security
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mailu-security
        role: mail
        tier: backend
    spec:
      containers:
      - name: antispam
        image: mailu/rspamd:stable
        imagePullPolicy: Always
        ports:
          - name: antispam
            containerPort: 11333
            protocol: TCP
        volumeMounts:
          - name: filter
            mountPath: /var/lib/rspamd
      - name: antivirus
        image: mailu/clamav:stable
        imagePullPolicy: Always
        ports:
          - name: antivirus
            containerPort: 3310
            protocol: TCP
        volumeMounts:
          - name: filter
            mountPath: /data
      volumes:
        - name: filter
          hostPath:
            path: /var/data/mailu/filter

---

apiVersion: v1
kind: Service
metadata:
  name: antispam
  labels:
    app: mailu-antispam
    role: mail
    tier: backend
spec:
  selector:
    app: mailu-security
    role: mail
    tier: backend
  ports:
  - name: antispam
    port: 11333
    protocol: TCP

---

apiVersion: v1
kind: Service
metadata:
  name: antivirus
  labels:
    app: mailu-antivirus
    role: mail
    tier: backend
spec:
  selector:
    app: mailu-security
    role: mail
    tier: backend
  ports:
  - name: antivirus
    port: 3310
    protocol: TCP

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mailu-admin
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mailu-admin
        role: mail
        tier: backend
    spec:
      containers:
      - name: admin
        image: mailu/admin:stable
        imagePullPolicy: Always
        env:
          - name  : DOMAIN
            value : example.com
          - name  : HOSTNAME
            value : mail.example.com
          - name  : POSTMASTER
            value : core
          - name  : SECRET_KEY
            value : pleasereplacethiswithsomethingelse
          - name  : DEBUG
            value : "True"
        volumeMounts:
          - name: maildata
            mountPath: /data
          - name: dkim
            mountPath: /dkim
          - name: certs
            mountPath: /certs
            readOnly: true
          # - name: docker
          #   mountPath: /var/run/docker.sock
          #   readOnly: true
        ports:
          - name: http
            containerPort: 80
            protocol: TCP
      volumes:
        - name: maildata
          hostPath:
            path: /var/data/mailu/maildata
        - name: dkim
          hostPath:
            path: /var/data/mailu/dkim
        - name: certs
          secret:
            items:
              - key: tls.crt
                path: cert.pem
              - key: tls.key
                path: key.pem
            secretName: letsencrypt-certs-all
        # - name: docker
        #   hostPath:
        #     path: /var/run/docker.sock

---

apiVersion: v1
kind: Service
metadata:
  name: mailu-admin
  labels:
    app: mailu-admin
    role: mail
    tier: backend
spec:
  selector:
    app: mailu-admin
    role: mail
    tier: backend
  ports:
  - name: http
    port: 80
    protocol: TCP

After getting all fo this working, I also found some time to contribute the configuration to the Mailu team which felt pretty great because I always say “oh maybe I’ll contribute” but never do.

Wrapping up

After all this wandering and ups/downs, I’ve finally got a pretty ergonomic email set up now, with nice easy-to-use web UI management provided by Mailu. I was pretty happy with myself at this point so I called it a day here (and I still use this set up to serve my emails).

I hope you find it easier to set up with this guide (at the very least with the resource configuration)!

Did you find this read beneficial? Send me questions/comments/clarifciations.
Want my expertise on your team/project? Send me interesting opportunities!