
Building Exim in a container on Ubuntu
Today we are going to do something a little bit different. We are going to try and create a Docker container that will build the latest version of Exim with our new favourite operating system, Ubuntu, from the source package we download from the Exim website. However, we are going to use the latest version of exim that we download but using the same configuration options and build dependencies as the Ubuntu/Debian team do – this allows us to perhaps get fixes/updates faster than waiting for Ubuntu to publish them, but we are effectively compiling the packages in the same way.
Now if that’s not enough, we are going to use this newly “built from source code” version of Exim and run it inside a new Ubuntu container. This builds on our previous article of How to Run Exim in a Docker Container.
First, let’s create our Dockerfile for the build environment based on Ubuntu 20.04 LTS (jammy), including some of the tools i know i will need from the standard set of packages:
$ cat Dockerfile.builder # build file for exim # Set specific version number and architecture requirements FROM --platform=linux/arm64/v8 ubuntu:jammy LABEL maintainer="[email protected]" LABEL description="exim-blog-post-builder" CMD ["/bin/bash"] # update the apt database and install security updates ARG DEBIAN_FRONTEND="noninteractive" RUN apt update RUN apt upgrade -y # install some standard packages that I know i will need and create a place to build RUN apt install -y bzip2 build-essential fakeroot dpkg-dev RUN mkdir /build $ docker build --quiet -f Dockerfile.builder -t exim-blog-post-builder:0.1 . sha256:784bd3c2586ef5c18e70658e20dba3999f6bcd1e69dca964702c7e5ac8e29a6b
Note: The way Docker builds containers means that if a command has run successfully then that “layer” is considered completed and you can build new layers on top of it. As such, the commands ‘apt update’ and ‘apt upgrade -y’ may not do what you think, nor give you the contents you expect in your container and therefore your container may be running with old updates. If you add the option ‘–no-cache’ to your build command, every layer will be run as though it has never been run before – something you definitely want to do when building new images to ensure you are getting all the updates you can. We don’t in the examples below, but you should!
Now we can see we have a very basic Ubuntu-based container on which we can build things from source, including the necessary tools and compilers:
$ docker run exim-blog-post-builder:0.1 which cc /usr/bin/cc $ docker run exim-blog-post-builder:0.1 ls -al /build total 8 drwxr-xr-x 2 root root 4096 Oct 6 01:33 . drwxr-xr-x 1 root root 4096 Oct 6 01:35 ..
So let’s ready the Exim source to be used, we will download the archive from the Exim website and palce into our local directory and then add it into the container, unzip it, do a very basic config of the build and finally we will compile the code inside the container:
$ wget https://ftp.exim.org/pub/exim/exim4/exim-4.96.tar.bz2 --2022-10-06 15:24:29-- https://ftp.exim.org/pub/exim/exim4/exim-4.96.tar.bz2 Resolving ftp.exim.org (ftp.exim.org)... 37.120.190.30 Connecting to ftp.exim.org (ftp.exim.org)|37.120.190.30|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 2047632 (2.0M) [application/octet-stream] Saving to: ‘exim-4.96.tar.bz2’ exim-4.96.tar.bz2 100%[================================================================================================>] 1.95M 438KB/s in 4.6s 2022-10-06 15:24:35 (438 KB/s) - ‘exim-4.96.tar.bz2’ saved [2047632/2047632] $ cat Dockerfile.builder # build file for exim # Set specific version number and architecture requirements FROM --platform=linux/arm64/v8 ubuntu:jammy LABEL maintainer="[email protected]" LABEL description="exim-blog-post-builder" CMD ["/bin/bash"] # update the apt database and install security updates ARG DEBIAN_FRONTEND="noninteractive" RUN apt update RUN apt upgrade -y # install some standard packages that I know i will need and create a place to build RUN apt install -y bzip2 build-essential fakeroot dpkg-dev libpcre2-dev pcre2-utils RUN mkdir /build # enable source repos RUN sed -i 's|^# deb-src|deb-src|g' /etc/apt/sources.list && apt update # Add the exim user needed to build with RUN /usr/sbin/adduser --ingroup mail --quiet exim # Add downloaded exim.org source to the build directory, download the ubuntu/debian source and install dependencies RUN mkdir -p /build/exim.org /build/ubuntu-src COPY exim-4.*.bz2 /build/exim.org RUN cd /build/exim.org && tar xf exim-4.*.bz2 && rm -f exim-4.*.bz2 && mv exim-4* exim4 RUN cd /build/ubuntu-src && apt-get -y source exim4-daemon-heavy && apt-get -y build-dep exim4-daemon-heavy # Using the source we downloaded, and the Markefile from Ubuntu with some updates, let's ready the source RUN cp /build/ubuntu-src/exim4-*/src/EDITME /build/exim.org/exim4/Local/Makefile # Now customise our Makefile a little, just to enable basic compiling RUN cd /build/exim.org/exim4 && \ sed -i -r 's|^(EXIM_USER=).*$|\1ref:exim|g' Local/Makefile && \ sed -i -r 's|^#\s*(EXIM_GROUP=).*$|\1ref:mail|g' Local/Makefile && \ sed -i -r 's|^#\s*(USE_GNUTLS=)|\1|g' Local/Makefile && \ sed -i -r 's|^#\s*(TLS_LIBS=-lgnutls)|\1|g' Local/Makefile && \ sed -i -r 's|^\s*(PCRE_CONFIG=)(.*)$|PCRE2_CONFIG=\2|g' Local/Makefile && \ echo "EXTRALIBS=-ldl" >> Local/Makefile # Compile the code and install the binaries RUN cd /build/exim.org/exim4 && make RUN cd /build/exim.org/exim4 && make install $ docker build --quiet -f Dockerfile.builder -t exim-blog-post-builder:0.2 . sha256:9bfcbf9d889b9fbe729e4f3afceaca1e83913c0fe0902874d4e261cf3b302999
We now have a basically configured Exim v4.96 (currently 4.95 is shipped with Ubuntu) built more-or-less the same way with the same options as the Ubuntu team.
…but I want to customise this some more. Specifically, I want to add support for IPv6, use the built-in SPF code, lookup against Redis caches, enable content scanning and to be able to extract data from JSON documents, sounds like a lot, but it isn’t…. Here is my new and updated Docker file:
$ cat Dockerfile.builder # build file for exim # Set specific version number and architecture requirements FROM --platform=linux/arm64/v8 ubuntu:jammy LABEL maintainer="[email protected]" LABEL description="exim-blog-post-builder" CMD ["/bin/bash"] # update the apt database and install security updates ARG DEBIAN_FRONTEND="noninteractive" RUN apt update RUN apt upgrade -y # install some standard packages that I know i will need and create a place to build RUN apt install -y bzip2 build-essential fakeroot dpkg-dev libpcre2-dev pcre2-utils libhiredis0.14 libhiredis-dev libspf2-2 libspf2-dev libjansson4 libjansson-dev RUN mkdir /build # enable source repos RUN sed -i 's|^# deb-src|deb-src|g' /etc/apt/sources.list && apt update # Add the exim user needed to build with RUN /usr/sbin/adduser --ingroup mail --quiet exim # Add downloaded exim.org source to the build directory, download the ubuntu/debian source and install dependencies RUN mkdir -p /build/exim.org /build/ubuntu-src COPY exim-4.*.bz2 /build/exim.org RUN cd /build/exim.org && tar xf exim-4.*.bz2 && rm -f exim-4.*.bz2 && mv exim-4* exim4 RUN cd /build/ubuntu-src && apt-get -y source exim4-daemon-heavy && apt-get -y build-dep exim4-daemon-heavy # Using the source we downloaded, and the Markefile from Ubuntu with some updates, let's ready the source RUN cp /build/ubuntu-src/exim4-*/src/EDITME /build/exim.org/exim4/Local/Makefile # Now customise our Makefile a little, just to enable basic compiling RUN cd /build/exim.org/exim4 && \ sed -i -r 's|^(EXIM_USER=).*$|\1ref:exim|g' Local/Makefile && \ sed -i -r 's|^#\s*(EXIM_GROUP=).*$|\1ref:mail|g' Local/Makefile && \ sed -i -r 's|^#\s*(USE_GNUTLS=)|\1|g' Local/Makefile && \ sed -i -r 's|^#\s*(TLS_LIBS=-lgnutls)|\1|g' Local/Makefile && \ sed -i -r 's|^\s*(PCRE_CONFIG=)(.*)$|PCRE2_CONFIG=\2|g' Local/Makefile && \ sed -i -r 's|^#\s*(LOOKUP_JSON=)|\1|g' Local/Makefile && \ sed -i -r 's|^#\s*(LOOKUP_REDIS=)|\1|g' Local/Makefile && \ sed -i -r 's|^#\s*(WITH_CONTENT_SCAN=)|\1|g' Local/Makefile && \ sed -i -r 's|^#\s*(SUPPORT_SPF=)|\1|g' Local/Makefile && \ sed -i -r 's|^#\s*(LDFLAGS \+= -lspf2)|\1|g' Local/Makefile && \ sed -i -r 's|^#\s*(HAVE_IPV6=)|\1|g' Local/Makefile && \ echo "EXTRALIBS=-ldl" >> Local/Makefile && \ echo "LOOKUP_LIBS=-lhiredis -ljansson" >> Local/Makefile # Compile the code and install the binaries RUN cd /build/exim.org/exim4 && make RUN cd /build/exim.org/exim4 && make install $ docker build --quiet -f Dockerfile.builder -t exim-blog-post-builder:0.3 . sha256:a7b19bc3b24d41a3580349128f66dffdad33039489da2da4ca7ab4802e943cca
OK, i have my binaries built and ready to go, let’s get them out of the container and onto the localhost.
$ mkdir exim-binaries 2>/dev/null $ rm -f exim-binaries/* $ docker run --mount src=`pwd`/exim-binaries,target=/exim-binaries,type=bind -it exim-blog-post-builder:0.3 cp -R /usr/exim/ /exim-binaries/ $ ls -la exim-binaries/exim/bin/ total 4064 drwxr-xr-x 18 dave staff 576 6 Oct 17:54 . drwxr-xr-x 4 dave staff 128 6 Oct 17:54 .. -rwxr-xr-x 1 dave staff 11284 6 Oct 17:54 exicyclog -rwxr-xr-x 1 dave staff 10664 6 Oct 17:54 exigrep lrwxr-xr-x 1 dave staff 11 6 Oct 17:54 exim -> exim-4.96-2 -rwxr-xr-x 1 dave staff 1644568 6 Oct 17:54 exim-4.96-2 -rwxr-xr-x 1 dave staff 4820 6 Oct 17:54 exim_checkaccess -rwxr-xr-x 1 dave staff 18984 6 Oct 17:54 exim_dbmbuild -rwxr-xr-x 1 dave staff 29856 6 Oct 17:54 exim_dumpdb -rwxr-xr-x 1 dave staff 38680 6 Oct 17:54 exim_fixdb -rwxr-xr-x 1 dave staff 23048 6 Oct 17:54 exim_lock -rwxr-xr-x 1 dave staff 29984 6 Oct 17:54 exim_tidydb -rwxr-xr-x 1 dave staff 151533 6 Oct 17:54 eximstats -rwxr-xr-x 1 dave staff 8223 6 Oct 17:54 exinext -rwxr-xr-x 1 dave staff 60680 6 Oct 17:54 exipick -rwxr-xr-x 1 dave staff 5558 6 Oct 17:54 exiqgrep -rwxr-xr-x 1 dave staff 5159 6 Oct 17:54 exiqsumm -rwxr-xr-x 1 dave staff 4431 6 Oct 17:54 exiwhat
I have exim binaries compiled, so now let’s create a running Docker image using these binaries. We will use the same method to generate the Exim config as we did in our previous blog post, with a few changes just like we did in the last post.
$ docker run -it new-exim:1.0 /usr/exim/bin/exim -bP > exim-blog-post-new.conf $ egrep -v "^(openssl_options|errors_reply_to|smtp_ratelimit_|syslog_facility|system_filter_user|system_filter_group|smtp_load_reserve|queue_only_load|deliver_queue_load_max|local_interfaces)" exim-blog-post-new.conf > tmpfile; mv tmpfile exim-blog-post-new.conf $ echo "local_interfaces = 0.0.0.0" >> exim-blog-post-new.conf $ cat Dockerfile.new-exim # build file for exim # Set specific version number and architecture requirements FROM --platform=linux/arm64/v8 ubuntu:jammy MAINTAINER [email protected] LABEL description="new-exim" # update the apt database and install security updates ARG DEBIAN_FRONTEND="noninteractive" RUN apt update RUN apt upgrade -y # add the exim user and group RUN /usr/sbin/adduser --system --ingroup mail --disabled-login --quiet exim && \ /usr/sbin/addgroup --system exim # install the libraries I need to run exim with and then cleanup after apt RUN apt install -y libhiredis0.14 libspf2-2 libjansson4 libgnutls-dane0 libgnutls30 \ libc6 libpcre2-posix3 perl-modules-5.34 && \ rm -rf /var/lib/apt/lists/* && \ apt clean # copy in our freshly built exim COPY exim-binaries /usr/ RUN chown -R root.root /usr/exim # rather than map our config in, I will place it inside the container so we have a 'golden image' of sorts COPY exim-blog-post-new.conf /etc/ CMD ["/usr/exim/bin/exim", "-bdf", "-q1h", "-C", "/etc/exim-blog-post-new.conf"] # expose Exim to the network EXPOSE 25 $ docker build --quiet -f Dockerfile.new-exim -t new-exim:1.0 . sha256:daa71246b85df2e49e5f098679769540e6183c85a63adb396aed4eb3a0a63304
So, does it run?
$ docker run -it -p 1025:25 new-exim:1.0 2022-10-06 08:39:00 exim user lost privilege for using -C option
$ telnet localhost 1025 Trying ::1... Connected to localhost. Escape character is '^]'. 550 Administrative prohibition Connection closed by foreign host.
It does, well at least as well at the one in our previous post. So now you can go configure Exim to run exactly how you want.
Let us know if you have any questions, we’ll be glad to help!
Find this useful?
We’re taking ideas for future how-to posts about the behind-the-scenes workings of email services.
If you’d like to make a request, we invite you to drop us a line here.
About atmail
atmail is an email solutions company with more than 20 years of global, white label, email expertise. You can trust us to deliver an email platform that is secure, stable and scalable. We power more than 170 million mailboxes worldwide and offer modern, white-labelled, cloud hosted email with your choice of US or (GDPR compliant) EU data centres. Contact us anytime, here.