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:

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:

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:

kubectl logs -f mailu-3284661181-zwb76 imap (open this in a console and watch for errors) 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.

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 actual mailu` 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)!