Dockerizing Postfix

warning: this guide is mostly obsolete although it contains a few tricks to make Postfix happy-happy in a container


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.

Note that the Docker network is added to mynetworks so e.g. the RBL checks won’t be performed against the MX server itself.

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 on mounted volume

This avoids storage of the logs on the image when commiting and also allows fail2ban to read those from the Docker host.

I am keeping the default rsyslog setup,

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

but I’m moving the whole /var/log/ folder to the mounted volume,

pkill rsyslog
mv /var/log/ /home/
ln -s ../home/log /var/log

To check that everything’s working,

tail -F /var/log/mail.log &
cat /etc/rsyslog.d/postfix.conf
ls -lhF /var/spool/postfix/dev/log
logger -u /var/spool/postfix/dev/log --socket-errors=on test

Ready to go

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

cd /home/
cat > init.bash <<-EOF
cp -pf /etc/resolv.conf /etc/hosts /etc/services /var/spool/postfix/etc/
cp -pf /lib/x86_64-linux-gnu/libnss_* /var/spool/postfix/lib/x86_64-linux-gnu/
#/usr/lib/postfix/sbin/master -w
/usr/sbin/postfix start
tail -F /var/log/mail.err &
while true; do sleep 120; done
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.

You should see Mounts and no Volumes,

docker volume ls
docker inspect postfixprod|less
/Mounts --> should show e.g. /data/postfixprod:/home
/Volumes --> should be none

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

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

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

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

and check the image size (should be between 400 and 700MB, no more as we’re not including the data/Maildirs nor the logs in it),

docker images|grep postfixprod

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 images|grep postfixprod|head -1|awk '{print $1}'`
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

and start the container when the host comes up,

vi /etc/rc.local

echo -n starting postfixprod container and forcing dovecot...
/usr/bin/docker start postfixprod \
    && /usr/bin/docker exec -d postfixprod /usr/sbin/dovecot \
    && echo done


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


Copyright © 2024 Pierre-Philipp Braun