Relay emails from alpine container using SMTPHOST.

Posted by Orville Bennett on 20 March 2023
Read time: about 3 minutes

I've been making more changes to my form API backend recently, migrating the deployment environment from FreeBSD jails to Docker containers. This isn't a post about that migration though. In the process of migrating I lost access to a fully featured sendmail binary.

If you'd like to see why sendmail specifically is so important to me, feel free to check out this talk I gave last year where I go through why I wrote the API in the first place, and how I tested it (which involves sendmail).

In Alpine Linux, which is what I use for formulate's base docker image1, sendmail is a part of busybox2. This means limited functionality for that version of sendmail, and this affected its ability to relay mail from within the docker container. When I tried using the built-in sendmail I was seeing a message similar to the following:

sendmail: can't connect to remote host (127.0.0.1): Connection refused

The gist of that message is that sendmail is trying to send mail by connecting to localhost, which it's treating as a remote mail server. But there is no mail server listening on localhost.

One thing I could have done was install a separate application to handle relaying mail in the container. In fact, in my FreeBSD jail that is exactly what I did. The sendmail binary exists but does not get used. Instead, I configured opensmtpd to handle mail serving duties. I didn't want my docker image any bigger though, it was already 10.5 MB and I was hoping to get to single digit sizes.3

I found out that instead of a configuration file I could use an environment variable—SMTPHOST—to point to an actual mail server to handle messages. That allowed me to use docker to set this environment variable and get form submissions sending via email again.

docker run -it --rm -e SMTPHOST="mail.host.domain" -p 80:8000 formulate

1

Used as the base docker image primarily for its smaller attack surface.

2

BusyBox combines tiny versions of many common UNIX utilities into a single small executable.

3

I actually started off at 14.4 MB but was able to shave some size off the rust binary by following some tips for creating smaller rust executables from the min-sized-rust repository.