Mailu is a container based mail service software stack.
At it’s core, it comprises of Postfix (SMTP), Dovecot (IMAP/POP) and Nginx (proxy) along with some optional extras. It’s all linked into a central container called admin which acts as a central authority. Officially it runs on Docker and Kubernetes (via Helm).
I don’t currently use Docker or Kubernetes for my own services but I have been using Mailu to host some mail services. Instead, I use Podman which is more or less interchangable with Docker but has some differences. This means I’d been running Mailu with some custom setup steps and it was working quite happily.
At various points, I documented my attempts at running it on Podman on a Github Issue. However, elsewhere on the Mailu issue tracker, something bad was happening. A user running on an unsupported setup raised a ticket and sniped at the developers, which results in the developers pushing this commit, which states it’s updating Roundcube but also includes the following:
def test_unsupported():
import codecs
if os.path.isfile(codecs.decode('/.qbpxrerai', 'rot13')) or os.environ.get(codecs.decode('V_XABJ_ZL_FRGHC_QBRFAG_SVG_ERDHVERZRAGF_NAQ_JBAG_SVYR_VFFHRF_JVGUBHG_CNGPURF', 'rot13'), None):
return
print('Your system is not supported. Please start by reading the documentation and then http://www.catb.org/~esr/faqs/smart-questions.html')
while True:
time.sleep(5)
For your convenience I’ll decode it for you, the rot13 encoded text is checking for the file /.dockerenv
to check if it’s running in docker, and if it’s not it looks for the environment variable I_KNOW_MY_SETUP_DOESNT_FIT_REQUIREMENTS_AND_WONT_FILE_ISSUES_WITHOUT_PATCHES
. If it’s not found, it prints an error (to stdout) and loops infinitely in a call to sleep()
.
The intent is that unsupported setups need to set an environment variable to acknowledge that they’re not running a supported setup. However if you recall, I mentioned that one of their supported platforms was Kubernetes. Kubernetes may use containerd
under the hood but it’s not going to create a .dockerenv
file in the container. As such, some of their kubernetes users found that when they upgraded to 2.0.29
their admin containers silently hung. It was silent, because the error was printed rather than written like other log lines.
This broke a few users setups (including mine) and left them confused as no error appeared to be reported. Infact, brining this to their attention led to them hurridly patching their Helm charts after initially denying it was an issue. However, while I disagree with their approach I do think they have a point. I decided since I was re-tooling my own setup anyway, to look at a better solution to running it on Podman. My previous attempts had always been just manually creating a 1:1 mapping from the services/volumes/networks in docker-compose.yml
and making their equivalent in Pods and later Quadlets. These were specific to my own setup and could only really serve as a base for others to crib from.
I started by finding a tool that would let me parse a docker-compose.yml
file in Go, and I found the compose-spec.io library in golang which did exactly what I needed. I then used the Mailu docker-compose.yml template which is used to generate Mailu setups and create Go text templates to programmatically create a set of quadlet units to match an arbitrary docker-compose.yml
for Mailu. After ironing out some issues, I’m currently using mail setup generated from it and I’ve tested initialising a few others with success.
You can review my mailu-quadlet tool code here, there’s also a container image for it available. In terms of operation, it’s relatively simple. Just generate your setup on the Mailu site then download your provided docker-compose.yml
and mailu.env
to a directory on your host.
wget https://[...]/docker-compose.yml
wget https://[...]/mailu.env
Now run the container, mounting the directory containing the docker-compose.yml
and mailu.env
file to /data
inside the container (which it uses by default).
podman run -v $(pwd):/data:z ghcr.io/cyberworm-uk/mailu-quadlet --uuid example
It should now list a series of filenames ending in .container
, .volume
and .network
which are the associated unit files for the resources it needs. You’ll notice they’re all prefixed with the --uuid
you provided. If one isn’t provided a random one will be generated.
The .env
file will need to be copied to /etc/mailu
as this is currently where the generated files expect to find it and these values are required for the services to operate correctly.
sudo mkdir -p /etc/mailu
sudo cp example.env /etc/mailu
Next the unit files will need to be moved to where the Quadlet service expects to find them. Assuming they’re going to be rootful containers (the front
container running Nginx requires binding to privileged ports) this would be /etc/containers/systemd/
.
sudo cp *.network *.container *.volume /etc/containers/systemd
sudo systemctl daemon-reload
# by default these will start on next boot
# alternatively we can start them manually, e.g.
for CONT in *.container; do sudo systemctl start ${CONT::-10}; done
You should now see your Mailu containers running under podman.
$ podman run ghcr.io/cyberworm-uk/mailu-quadlet:latest -help
Usage of /cli:
-compose string
docker-compose.yml file for mailu (default "docker-compose.yml")
-envfile string
mailu.env file for mailu (default "mailu.env")
-uuid string
optional custom uuid to use for generated
$ go doc github.com/cyberworm-uk/mailuquadlet.Mailu
package mailuquadlet // import "github.com/cyberworm-uk/mailuquadlet"
type Mailu struct {
// Has unexported fields.
}
func NewMailu(compose, env string) *Mailu
func (m *Mailu) Export()
func (m *Mailu) Init(compose, env string)
func (m *Mailu) Uuid(id string)