Envío de correo desde un Docker contenedor utilizando ISPConfig3 hosts Postfix MTA
Utilice la dirección IP de puente docker0 para conectarse a Postfix
En el sinfín de problemas que encuentras y resuelves cuando empiezas a usar una nueva tecnología, me enfrentaba a uno nuevo: cómo enviar correo electrónico desde mi Python Docker aplicación usando el ISPConfig host MTA (Mail transfer Agent). Encontré que hay dos maneras de hacer esto:
- Enviar correo desde nuestro contenedor al port 25 host donde se MTA está escuchando.
- Escriba el archivo de correo en un directorio del host y utilice un script para transferir los archivos de correo a la carpeta MTA
Tal vez más adelante investigaré el método de archivos de correo, ver también los enlaces de abajo.
Enviar correo desde nuestro contenedor al port 25 host donde se MTA está escuchando.
Para ello debemos utilizar el Docker puente llamado docker0. De los Docker médicos:
Por defecto, el Docker servidor crea y configura el docker0 del sistema host una interfaz de red llamada docker0, que es un dispositivo puente ethernet. Si no se especifica una red diferente al iniciar un contenedor, el contenedor se conecta al puente y todo el tráfico que viene y va al contenedor fluye por el puente hacia el , Docker daemonque se encarga del enrutamiento en nombre del contenedor.
Docker configura el docker0 con una dirección IP, netmask, y un rango de asignación de IP. A los contenedores que están conectados al puente por defecto se les asignan direcciones IP dentro de este rango.''.
Esto significa que en el contenedor, cuando se utiliza telnet para comprobar si podemos conectarnos a la MTA y podemos enviar un correo de prueba, no podemos hacerlo:
telnet localhost 25
telnet 127.0.0.1 25
pero en su lugar debe usar la dirección IP del puente de conexión:
telnet <docker0 IP address> 25
Puede obtener la dirección IP del docker0, por ejemplo, ejecutando:
ifconfig docker0
Prueba en máquina local (Ubuntu 18.04 desktop)
Decidí probar esto primero en mi máquina local en lugar de perder el tiempo con la producción. Asumiendo que usted ha Docker instalado y no MTAhay, usted necesita lo siguiente para hacer estas pruebas:
- Compruebe los puertos de escucha y las aplicaciones en busca de port 25... Ejecute una de ellas:
sudo lsof -i -P -n | grep LISTEN | grep 25 sudo netstat -tulpn | grep LISTEN | grep 25
- Un contenedor de trabajo Alpine está bien para nuestro propósito. Introdúzcalo corriendo:
docker run -it alpine /bin/sh
- Telnet en el Docker contenedor. Cuando esté en el contenedor, instale telnet ejecutando:
apk add busybox-extras
- Una socket server escucha en port 25... Usé lo siguiente:
# 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()
En mi máquina local, la dirección IP del docker0 bridge es: 172.17.0.1.
Primero, comprobamos si port 25 está disponible (debería estarlo):
sudo lsof -i -P -n | grep LISTEN | grep 25
Abra otra ventana de terminal, inicie el eco socket server..:
sudo python3 listen_port.py
Esto imprime algo de texto:
socket created
bind done
start listening
Ahora port 25 debe estar en uso (por nuestro socket server):
python3 5857 root 3u IPv4 2925123970 0t0 TCP *:25 (LISTEN)
Abra otra ventana de terminal, inicie e introduzca el Docker contenedor:
docker run -it alpine /bin/sh
Ejecute'apk add busybox-extras' para instalar telnet.
En el tipo de Docker contenedor:
telnet 127.0.0.1 25
Recibirá el mensaje:
telnet: can't connect to remote host (127.0.0.1): Connection refused
Ahora usa la dirección IP del docker0 en su lugar:
telnet 172.17.0.1 25
En las ventanas de los terminales de los socket server terminales debería ver el mensaje:
Connection from ('172.17.0.2', 38928)
En el Docker contenedor, escriba algunas palabras, presione Enter, etc. Deberían resonar en el Docker contenedor y debería verlos en la ventana del socket server terminal.
Tenga en cuenta que puede haber un problema de secuencia, el orden en el que se inicia el socket server y el Docker contenedor. Además, cuando termine el socket server y lo inicie de nuevo, es posible que se produzca un error:
OSError: [Errno 98] Address already in use
En este caso, espere algún tiempo antes de comenzar de nuevo.
Hasta ahora todo bien. Podemos comunicarnos a través del puente docker0. Pasemos al servidor de producción.
Implementar en producción (Debian Tramo + ISPConfig3)
En mi servidor de producción, la dirección IP del docker0 bridge es: 172.17.0.1
Para ISPConfig3 la MTA es Postfix. La configuración está en el archivo:
/etc/postfix/main.cf
Hay dos líneas en este archivo que pueden necesitar cambios:
inet_interfaces = all
...
mynetworks = 127.0.0.0/8 [::1]/128
La línea con ' 'inet_interfacesse ve bien, Postfix ya escucha todas las interfaces, no hay necesidad de cambiar esta línea. La línea con ' 'mynetworksdebe ser cambiada, debe incluir:
- La dirección IP del puente docker0, y,
- Las direcciones IP de las Docker imágenes (que serán las que envíen el correo)
Ya tenemos la dirección IP del puente docker0, 172.17.0.1. Para encontrar las direcciones IP de nuestros Docker contenedores podemos inspeccionarlos, primero ejecute:
docker ps
para obtener la identificación del contenedor, luego use inspect, e.g..:
docker inspect c2c44e9bea28
Esto dará un montón de información y en algún lugar en la parte inferior de la línea algo como:
"IPAddress": "172.20.0.2"
Para incluir todas las Docker direcciones que uso la subred 172.16.0.0.0/12, esto da un rango de IP: 172.16.0.1 - 172.31.255.254. La mynetworks línea se convierte entonces en:
mynetworks = 127.0.0.0/8 [::1]/128 172.16.0.0/12
Después de cambiar esta línea en /etc/postfix/main.cf y guardar el archivo debemos reiniciar Postfix:
service postfix restart
Para comprobar si nuestros cambios están funcionando utilizamos de nuevo telnet. En el tipo de host:
telnet 127.0.0.1 25
Imprimirá algo como:
220 server.example.com ESMTP Postfix (Debian/GNU)
¡Recuerda esta línea! Escriba"quit to stop telnet" (salir para parar) Ahora empezamos un pequeño Docker contenedor, como hicimos en la máquina local:
docker run -it alpine /bin/sh
y de nuevo añadir telnet ejecutando 'apk add busybox-extras'. Por supuesto:
telnet 127.0.0.1 25
fallará:
telnet: can't connect to remote host (127.0.0.1): Connection refused
Pero:
telnet 172.17.0.1 25
ahora debería darle el mensaje de la MTA, el mismo mensaje que en el host:
220 server.example.com ESMTP Postfix (Debian/GNU)
Como prueba final, podemos enviarnos un correo a nosotros mismos desde el contenedor usando sendmail. Obtenga las sendmail opciones:
sendmail --help
En algún lugar está la línea:
-S HOST[:PORT] Server (default $SMTPHOST or 127.0.0.1)
Esto significa que podemos cambiar la dirección por defecto por la de docker0. Para enviar un correo, escriba:
sendmail -S 172.17.0.1 yourname@yourdomain.yourextension
Empieza a escribir:
To: someone@example.com
Subject: My docker test mail
Hello this is a mail from my docker container.
Thank you.
Escriba Ctrl-D para enviar el correo. puede que desee inspeccionar /var/log/mail.log para su mensaje de correo. Lo hice y desafortunadamente hubo un error, el mensaje en mail.log:
You cannot send mail from 4449d8888ddd since that domain cannot receive mail
Además, se comprobó que la dirección del remitente no era un dominio totalmente cualificado. Puede comprobar esto ejecutando sendmail la opción Verbose (-v). Apareció:
sendmail: send:'MAIL FROM:<root@1f12e2cef814>'
La solución es añadir la opción MAIL FROM SENDER (-f) en la línea de comandos:
sendmail -v -S 172.17.0.1 -f info@example.com yourname@yourdomain.yourextension
Empieza a escribir de nuevo:
To: yourname@yourdomain.yourextension
From: someone@example.com
Subject: My docker test mail
Hello this is a mail from my docker container.
Thank you.
Escriba Ctrl-D para enviar el correo. Ahora el correo se envía y debería aparecer en su buzón (o spam). ¡Misión completada!
Uso de un servidor local SMTP para la depuración
Hay un Python liner que actúa como un servidor SMPT y puede ser útil para la depuración, el indicador -d añade información de depuración:
sudo python -m smtpd -d -n -c DebuggingServer 172.17.0.1:1025
Observaciones finales
Existe un problema con el método descrito, es decir, véase también los enlaces:
Postfix debe iniciarse después de que se haya sacado el inteface docker0".
Debido a que el puente de Docker red puede no estar listo en el arranque del sistema, postfix puede fallar al no poder enlazarse a esa dirección.
Todavía no he buscado soluciones, pero me da miedo. Si se reinicia el servidor no debo olvidarme de comprobar si se puede enviar correo. Por esta razón el método'mail files' puede ser una mejor solución ya que no hay dependencias en docker0 bridge.
Enlaces / créditos
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
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow