Send an email from a Docker container through an external MTA with ssmtp

I packaged a standard application (think of it as a standard PHP or <insert your preferred framework here>) into a Docker container. So far, it was working flawlessly, but then a problem arose: send an email from the Docker container (the event is triggered within the container).

As you may know, a good Docker container is a container with only one process running: the naive solution for our case would be to have, in addition to having our PHP process running, another process to manage the email interexchange (an MTA, i.e. Postfix). As we are following the best practices for Docker containers, this path is discouraged.

There are many solutions to this problem.

The common ground for all of the solutions is to rely on ssmtp when sending emails from the container. ssmtp is a simple relayer to deliver local emails to a remote mailhub that will take care of delivering the emails.

Provided that the container distribution ships ssmtp, the installation is straightforward: just add the package during the install phase of the Dockerfile. ssmtp must be configured to relay every email an SMTP host, e.g.:

# cat /etc/ssmtp/ssmtp.conf

# The user that gets all the mails (UID < 1000, usually the admin)
root=postmaster

# The place where the mail goes. The actual machine name is required
# no MX records are consulted. Commonly mailhosts are named mail.domain.com
# The example will fit if you are in domain.com and you mailhub is so named.

# Use SSL/TLS before starting negotiation
UseTLS=Yes
UseSTARTTLS=Yes

# Fill the following with your credentials (if requested) 
AuthUser=postmaster@mycompany.biz
AuthPass=supersecretpassword

# Change or uncomment the following only if you know what you are doing

# Where will the mail seem to come from?
# rewriteDomain=localhost
# The full hostname
# hostname="localhost"
# The address where the mail appears to come from for user authentication.
# rewriteDomain=localhost
# Email 'From header's can override the default domain?
# FromLineOverride=yes

All the three solutions that I am going to illustrate rely on having a custom mailhub that must be configured accordingly.

Let’s review each solution.

An external SMTP relay host

If an external SMTP relay host is available, the solution is to point mailhub option of ssmtp to the external SMTP host.

Another container running the MTA

The proper way to solving this problem would be to run a Docker container just for the MTA itself (personal preference: Postfix). One caveat of this solution: some Linux distributions come with an MTA running out of the box. If the container host is already running an MTA, the container cannot publish the port 25/tcp from the Postfix container [the address is already in use by the MTA running on the host].

By searching on GitHub, a promising and an up-to-date container is the eea.docker.postfix. After you deploy the Postfix container, link every container that needs an MTA to it. E.g.

# docker run --link=postfix-container my-awesome-app-that-needs-an-mta 

The container must configure ssmtp to use postfix-container(or the name defined as the link) in the mailhub option in ssmtp.conf.

Relying on the host MTA

Premise: the Docker daemon exposes an adapter to all the containers running on the same host. This adapter is usually named as the docker0 interface:

# ip a show docker0
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff
    inet 172.17.42.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::11:22ff:fff0:3344/64 scope link 
       valid_lft forever preferred_lft forever

If the host MTA is listening on the docker0 interface, then the containers can relay email to the host MTA. There is not an extra configuration on the container itself, just configure ssmtp to use the docker0 IP as the mailhub.

EXTRA: HOW TO CONFIGURE POSTFIX TO LISTEN ON DOCKER INTERFACE (LIKE DOCKER0) AS WELL

To use the solution described above, the MTA on the host must be configured to listen on the docker0 inteface as well. In case that the MTA in case is Postfix, the configuration is straightforward:

On the host, open /etc/postfix/main.cf and add the docker0 IP to the inet_interfaces option and add the subnetwork block range of the containers that need to use the host MTA to the mynetwork option:

# cat /etc/postfix/main.cf 

[...]
inet_interfaces = 127.0.0.1, 172.17.42.1 
mynetworks = 127.0.0.0/8 172.17.42.0/24
[...]

If Postfix is set to be started at boot by systemd, we need to take care of the dependency: Docker daemon must be started before the Postfix daemon, as Postfix needs to bind on the docker0 IP address.

In order to express this dependency, and luckily for us, systemd already ships with a service that detects when an interface is up:

# systemctl | grep docker0
  sys-devices-virtual-net-docker0.device                                                                      loaded active plugged   /sys/devices/virtual/net/docker0

Postfix must be started after the docker0 inteface has been brought up, and to express the dependency we must alter every postfix service units (this may vary based on the host distribution):

# systemctl | grep postfix
  postfix.service                                                                                             loaded active exited    Postfix Mail Transport Agent                                                                          
  postfix@-.service                                                                                           loaded active running   Postfix Mail Transport Agent (instance -)    

in this case of both of the Postfix services with:

# systemctl edit postfix.service postfix@-.service

Override the unit service file by declaring the dependency explicitely:

[Unit]
Requires=sys-devices-virtual-net-docker0.device
After=sys-devices-virtual-net-docker0.device

Reload systemd with systemctl daemon-reload and restart Postfix with systemctl restart postfix.

Relying on the host MTA by using host network driver on Docker

When a container is set to use host networking interface, the container can access the host networking and thus its services. If the container host already has an MTA configured, then the containers can use it by just pointing to localhost.The syntax to use host networking interface for the application that needs to use the host MTA is:

# docker run --net=host my-awesome-app-that-needs-an-mta

To configure ssmtp, just point the mailhub to localhost.

NOTE: Using the host networking interface has obviously security drawbacks, because containers do not have their networking containerized by Docker but rather rely on the host networking; this can guarantee to the Docker container to have access to the whole networking stack (in read-only mode) and open low-numbered ports like any other root process. Use this networking option by carefully weigh pro and cons.

Leave a Reply

Your email address will not be published. Required fields are marked *