angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Sending mail from a Docker container using ISPConfig3 host Postfix MTA

Use the docker0 bridge IP address to connect to Postfix

28 June 2019 Updated 31 August 2019
post main image
Original photo unsplash.com/@alejandroescamilla.

In the endless number of problems you encounter, and solve, when starting to use new technology I was facing a new one: how to send email from my Python Docker app using the ISPConfig host MTA (Mail transfer Agent). I found there are two ways to do this:

  • Send mail from our container to port 25 of the host where the MTA is listening on
  • Write the mail file to a directory on the host and use a script to transfer the mail files to the MTA

Maybe later I will investigate the mail files method, see also links below.

Send mail from our container to port 25 of the host where the MTA is listening on

To do this we must use the Docker bridge called docker0. From the Docker docs:

'By default, the Docker server creates and configures the host system’s docker0 a network interface called docker0, which is an ethernet bridge device. If you don’t specify a different network when starting a container, the container is connected to the bridge and all traffic coming from and going to the container flows over the bridge to the Docker daemon, which handles routing on behalf of the container.

Docker configures docker0 with an IP address, netmask, and IP allocation range. Containers which are connected to the default bridge are allocated IP addresses within this range.'

This means that in the container, when using telnet to check if we can connect to the MTA and can send a test mail, we cannot do:

telnet localhost 25
telnet 127.0.0.1 25

but instead must use the docker bridge IP address:

telnet <docker0 IP address> 25

You can get the docker0 IP address e.g. by running:

ifconfig docker0

Test on local machine (Ubuntu 18.04 desktop)

I decided to test this first on my local machine instead of messing around on production. Assuming you have Docker installed and no MTA, you need the following to do these tests:

  • Check listening ports and applications for port 25. Run one of:
    sudo lsof -i -P -n | grep LISTEN | grep 25
    sudo netstat -tulpn | grep LISTEN | grep 25
  • A working container. Alpine is fine for our purpose. Enter it by running:
    docker run -it alpine /bin/sh
  • Telnet in the Docker container. When in the container, install telnet by running:
    apk add busybox-extras
  • A socket server listening on port 25. I used the following: 
    # description: python 3 socket server printing and echoing received data
    import socket
    import sys
    
    # host: all available interfaces
    host = ''
    # port: set to port 25 for our test
    port = 25
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print('socket created')
    try:
    	s.bind((host, port))
    except socket.error as msg:
    	print('bind failed, msg = {}'.format(msg))
    	sys.exit()
    print('bind done')
    
    s.listen(1)
    print('start listening')
    
    conn, addr = s.accept()
    print('Connection from ', addr)
    while True:
        data = conn.recv(1024)
        print('data = {}'.format(data))
        if not data: 
            break
        conn.sendall(data)
    conn.close()

On my local machine, the docker0 bridge IP address is: 172.17.0.1.

First, we check if port 25 is available (it should be):

sudo lsof -i -P -n | grep LISTEN | grep 25

Open another terminal window, start the echoing socket server:

sudo python3 listen_port.py

This prints some text:

socket created
bind done
start listening

Now port 25 must be in use (by our socket server):

python3    5857            root    3u  IPv4 2925123970      0t0  TCP *:25 (LISTEN)

Open another terminal window, start and enter the Docker container:

docker run -it  alpine /bin/sh

Run 'apk add busybox-extras' to install telnet.

In the Docker container type:

telnet 127.0.0.1 25

You will get the message:

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

Now use the docker0 IP address instead:

telnet 172.17.0.1 25

In the terminal windows of the socket server you should see the message:

Connection from  ('172.17.0.2', 38928)

In the Docker container, type some words, hit Enter, etc. They should be echoed in the Docker container and you should see them in the socket server terminal window.

Be aware that there may be a sequence problem. the order in which you start the socket server and the Docker container. Also when you terminate the socket server and start it again you may get an error:

OSError: [Errno 98] Address already in use

In this case wait some time before starting it again.

So far so good. We can communicate via the docker0 bridge. Let's proceed to the production server.

Implement on production (Debian Stretch + ISPConfig3)

On my production server, the docker0 bridge IP address is: 172.17.0.1

For ISPConfig3 the MTA is Postfix. The configuration is in the file:

/etc/postfix/main.cf

There are two lines in this file that may need changes:

inet_interfaces = all
...
mynetworks = 127.0.0.0/8 [::1]/128

The line with 'inet_interfaces' looks fine, Postfix already listens to all interfaces, no need to change this line. The line with 'mynetworks' must be changed, it must include:

  • The IP address of the docker0 bridge, and,
  • The IP addresses of Docker images (that will be sending mail)

We already have the IP address of the docker0 bridge, 172.17.0.1. To find the IP addresses of our Docker containers we can inspect them, first run:

docker ps

to get the container id, then use inspect, e.g.:

docker inspect c2c44e9bea28

This will give a lot of info and somewhere at the bottom the line something like:

"IPAddress": "172.20.0.2"

To include all Docker addresses I use the subnet 172.16.0.0/12, this gives an IP range: 172.16.0.1 - 172.31.255.254. The mynetworks line then becomes:

mynetworks = 127.0.0.0/8 [::1]/128  172.16.0.0/12

After changing this line in /etc/postfix/main.cf and saving the file we must restart Postfix:

service postfix restart

To check if our changes are working we again use telnet. On the host type:

telnet 127.0.0.1 25

It will print something like:

220 server.example.com ESMTP Postfix (Debian/GNU)

Remember this line! Type quit to stop telnet. Now we start a small Docker container, like we did on the local machine:

docker run -it  alpine /bin/sh

and again add telnet by running 'apk add busybox-extras'. Of course:

telnet 127.0.0.1 25

will fail:

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

But:

telnet 172.17.0.1 25

now should give you the message of the MTA, the same message as on the host:

220 server.example.com ESMTP Postfix (Debian/GNU)

As a final test, we can send a mail to ourselves from the container using sendmail. Get the sendmail options:

sendmail --help

Somewhere there is the line:

-S HOST[:PORT]	Server (default $SMTPHOST or 127.0.0.1)

This means we can change the default address to the one of docker0. To send a mail, type:

sendmail -S 172.17.0.1 yourname@yourdomain.yourextension

Start typing:

To: someone@example.com
Subject: My docker test mail

Hello this is a mail from my docker container. 
Thank you.

Type Ctrl-D to send the mail. You may want to inspect /var/log/mail.log for your mail message. I did and unfortunately there was an error, the message in mail.log:

You cannot send mail from 4449d8888ddd since that domain cannot receive mail

Checking further it appeared that the from address was not fully qualified domain. You can check this by running sendmail with Verbose (-v) option. It showed:

sendmail: send:'MAIL FROM:<root@1f12e2cef814>'

The solution is to add the MAIL FROM option SENDER (-f) on the command line:

sendmail -v -S 172.17.0.1 -f info@example.com yourname@yourdomain.yourextension

Again start typing:

To: yourname@yourdomain.yourextension
From: someone@example.com
Subject: My docker test mail

Hello this is a mail from my docker container. 
Thank you.

Type Ctrl-D to send the mail. Now the mail is sent and should appear in your mailbox (or spam). Mission completed!

Using a local SMTP server for debugging

There is a Python one liner that acts as a SMPT server and can be useful for debugging, the -d flag adds debugging information:

sudo python -m smtpd -d -n -c  DebuggingServer 172.17.0.1:1025

Final remarks

There is an issue with the described method and that is, see also links:

'Postfix must be started after the docker0 inteface has been brought up'.

'Because the Docker network bridge may not yet be ready at system bootup postfix may fail to start because it cannot bind to that address.'

I have not looked into solutions yet but it is scary. If the server is rebooted I must not forget to check if mail can be send. For this reason the 'mail files' method can be a better solution as there no dependencies on docker0 bridge.

Links / credits

Configure sendmail inside a docker container
https://stackoverflow.com/questions/26215021/configure-sendmail-inside-a-docker-container

Send an email from a Docker container through an external MTA with ssmtp
https://www.michelebologna.net/2019/send-an-email-from-a-docker-container/

Sending email from docker through Postfix installed on the host
http://satishgandham.com/2016/12/sending-email-from-docker-through-postfix-installed-on-the-host/

Sending email inside a docker container to hosts smtp running postfix
https://serverfault.com/questions/817353/sending-email-inside-a-docker-container-to-hosts-smtp-running-postfix

Using system postfix as mail relay for docker containers
https://markusbenning.de/blog/2017/08/16/using-system-postfix-as-mail-relay-for-docker-containers.html

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.