Nethence Newdoc Olddoc Lab Your IP BBDock  

Dockerizing Postfix

Dockerizing a strong anti-spam Postfix MX


The container is ideally data-independend and possibly load-balance-ready. So the shared Docker volume may also be some NFS or shared disk fs on the Docker host side.

Avoiding conflicts between the container and the Docker host Postfix instances

Public DNS records

You should advertise a public A record, not a CNAME, corresponding to the MX record. See the Postfix guide for further ado. As for Dockerization, this A record should correspond to container’s postfix myhostname, not Docker host’s.

Liberating port 25 & 587

We are going to setup an MX inside a container, therefore the smtp port needs to be freed from the Docker host. You should see e.g. that port 25 and possibly 587 are taken,

netstat -antpe --inet --inet6|grep LISTEN|egrep '25|587'

tweak Postfix on the Docker host so it does NOT listen on ports 25 and 587,

    vi /etc/postfix/

    (comment out port 25 and eventually port 587)

    postfix reload

and check again so ports 25 and 587 are now available.

No Loops, No Loops, No Loop

Note. The system hostname doesn’t really matter (what really matters is postfix myhostname), but for consistency I used the same (as short form) on the docker host. As for the container, its name generates its system hostname and static resolution anyway, which is okay.

The Docker host should differentiate itself from the container to be able to talk to it and avoid loops. The most simple and clean fix is to:

Ref. relayhost maintained brutally

As a preview, the container to be setup will have something like,

myhostname =
myorigin = $mydomain
mydestination = $mydomain

Once the thing is up and running you should pass this acceptance tests,

send a mail to root@domain from the public network
send a mail to root@domain from the docker host as wheeled user
send a mail to root@domain from the docker host as root
send a mail to root@domain from the container

Launching the container

Make sure that either the shared volume doesn’t or does exist whether it’s a fresh installation or not,

sudo ls -alhF /data/postfixprod/

Run the container based on the custom Ubuntu Docker image,

docker ps -a | grep $app
docker run -d --name $app -h $app -p 25:25 -p 587:587 -p 143:143 -v /data/$app:/home custom/ubuntu
docker exec -ti postfixprod bash

Note. in case you need to link some Postfix & Dovecot mappings to MariaDB,

    --link mariadbprod:mariadb \

Note. (optional) also if you like to,

    -v /data/postfixprod.conf:/etc/postfix \
    -v /data/postfixprod.spool:/var/spool/postfix \

And proceed with the Postfix guide.

Logs for Docker host and fail2ban

You might want to make the mail logs available to the Docker host so it can enable fail2ban,

cd /etc/rsyslog.d/
cp -pi 50-default.conf 50-default.conf.dist
vi 50-default.conf

mail.*                          -/home/mail.log
mail.err                        /home/mail.err

make sure the files with the right perms (syslog:adm) are in place so rsyslog will actually be able to write to it,

cd ~/
pkill rsyslogd
mv /var/log/mail.log /home/
mv /var/log/mail.err /home/
tail -F /home/mail.* &
postfix reload

Ready to go

Write the new init for the Docker container to start the daemons,

cd ~/
cat > init.bash <<-EOF
cp -pf /etc/resolv.conf /etc/hosts /etc/services /var/spool/postfix/etc/
#/usr/lib/postfix/sbin/master -w
/usr/sbin/postfix start
#while true; do sleep 120; done
tail -F /var/log/mail.err
chmod +x init.bash

and switch to it,

pkill rsyslogd
postfix stop
[[ ! -f /var/log/mail.err ]] && touch /var/log/mail.err

Note. master without -w if you want it to be the remaining process of the container’s entrypoint (CMD). See man 8 master. I prefer to use a fake init or just tail -F as a remaining process so I can eventually really restart the postfix daemon while keeping the container up and running (e.g. for dovecot).

See the the Postfix guide again to run some checks.

Everything’s fine? Then commit to an image from the Docker host and change the entrypoint against the new init,

docker commit --change='CMD ["/root/init.bash"]' -p postfixprod postfixprod.`date +%s`.ready

or if the good entrypoint already has been applied, simply,

docker commit -p postfixprod postfixprod.`date +%s`.ready


You don’t need to swith to that new container right now, as it’s already up and running. But considering the user db could be kept outside the container (TODO), you would then be able to launch it on other nodes with that special init and just pointing to the same user db and mail storage!

docker ps -a | grep $app
docker run -d --name $app -h $app \
    -p 25:25 -p 587:587 -p 143:143 \
    -v /data/$app:/home \
docker logs $app
docker exec -ti $app ps auxfw
#docker exec -ti $app bash

Finally add some helper to track the MX activity from the Docker host,

vi ~/.screenrc

screen -t "postfixprod" 3 docker exec -ti postfixprod tail -F /var/log/mail.err /var/log/mail.log


Host Integration

Configure the System D service,

cd /etc/systemd/system
vi postfixprod.service

Description=postfixprod container

ExecStart=/usr/bin/docker start -a postfixprod
ExecStop=/usr/bin/docker stop -t 30 postfixprod


systemctl daemon-reload

Eventually restart the shit using System D now,

#docker stop postfixprod
#systemctl start postfixprod
#systemctl status postfixprod

Enable the shit at startup,

systemctl enable postfixprod
systemctl status postfixprod


Home | GitHub | Donate