Building in containers
October 6, 2022

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.

Share This Post