The great thing about Docker is that you can put your build environment inside a Docker image and not have to mess around with settings on your computer. Additionally, any computer can build exactly the same just by using the same Docker image. So how do you go about creating a Docker image in the first place?
There is a huge repository of images already built and maintained on hub.docker.com. There is a good chance that there is already an image out there that has the build environment you want. But its still nice to be able to create your own so you know exactly what goes in it.
A Docker image is built by using a Dockerfile. By default the file is simply called Dockerfile
. The syntax is simple and basically a list of instructions that are executed in order within a docker container and the result saved as an image. A Dockerfile starts with a FROM instruction which is the name of an existing Docker image that you want to use as the base. There is a built on one called scratch
that is simply an entirely empty file system. In general you don’t use this one, this is how the operating system base images are created. Typically you’ll use a standard linux base. Every major distribution now provides Docker images on Docker Hub. The image you use for your base has nothing to do with the host operating system you are running on (Note this is not the case for Windows images, but that is a completely separate situation).
I like to start my images with Ubuntu as that is an easy to use distribution and only 73MB (as of 20.04). A very popular one to use is Alpine, but I don’t particularly like it as I’m not familiar with its package manager. I am going to demonstrate building a Docker image that is capable of building the WjCryptLib for Linux x64.
Save the following as Dockerfile
FROM ubuntu:20.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y cmake
RUN apt-get install -y ninja-build
RUN apt-get install -y build-essential
ENV CMAKE_GENERATOR=Ninja
ENV CFLAGS=-Wno-deprecated-declarations
The first line says use the existing image ubuntu:20.04
as the starting base. This will be automatically downloaded from hub.docker.com the first time you try to use it.
Then ENV DEBIAN_FRONTEND=noninteractive
line sets an environment variable. Annoyingly the Ubuntu images don’t already have this set, and if its not set then using apt-get install
on some packages will cause a prompt to come up to ask for timezone information. This prevents the build from jamming.
The next 4 RUN
lines are command line instructions run directly in the temporary container created to build the image. The ubuntu images don’t come with the apt-get cache prepulated, so you have to start with an apt-get update
Finally there are two more environment variables which will be set in the image. One sets the CMAKE generator to automatically be Ninja seen we have install that. The second one add a default C compile flag which is currently needed for building WjCryptLib without warnings.
This Dockerfile is all that is needed. Often you have extra files that you want to put into the image. But in this case all we needed to do from a fresh Ubuntu was to do a few apt-get installs.
To build the image run the following command
docker build -t build-linux .
The -t build-linux
gives the image a name. In this case I’ve called it build-linux
. The '.'
at the end says to use the current working directory as the directory to send to Docker as the build context. This will include any files that are in the dir. In this case there aren’t any.
You will see the output from the execution in the output. When its finished there will be a docker image called build-linux
in your images list in Docker. If you run docker images
you’ll see something like
REPOSITORY TAG IMAGE ID CREATED SIZE
build-linux latest 5e29d818504f 10 seconds ago 413MB
This image is now ready to use to build WjCryptLib. You can downloadWjCryptLib with the following command (assuming you have git installed)
git clone https://github.com/WaterJuice/WjCryptLib.git
This will download the repo to a directory called WjCryptLib
. With in that directory run the following commands to build it.
docker run --rm -v $PWD:$PWD -w $PWD build-linux cmake -H. -Bbuild -DCMAKE_INSTALL_PREFIX=bin
docker run --rm -v $PWD:$PWD -w $PWD build-linux cmake --build build --target install
And that is it!
Now the great thing about Docker is that you don’t need to recreate the Docker image all the time. Once you have made it you can then share it with other people who don’t ever need to worry creating it. They can then use it without any effort. Note you generally would share images by publishing them to Docker hub, in which case the name of the image would have your account name in it.
For example I have the build image wjxx/ubuntu20.04-build
which is similar to the one described here. It is also capable of building WjCryptLib and can be used immediately by simply using it instead of the locally build build-linux
docker run --rm -v $PWD:$PWD -w $PWD wjxx/ubuntu20.04-build cmake -H. -Bbuild -DCMAKE_INSTALL_PREFIX=bin
docker run --rm -v $PWD:$PWD -w $PWD wjxx/ubuntu20.04-build cmake --build build --target install
The docker run --rm -v $PWD:$PWD -w $PWD
is a very common thing to use. So I have an alias for it called drun
so I don’t have to type it in each time.
Now when building I always use --rm
which says to create a temporary container and then throw it away when finished. We have no interest in any file changes that occur within the docker container. The files we are interested in are all done in the volume mounted from the host current directory. In fact you can run it with a readonly file system by adding the flag --read-only
. Another useful flag to add is --network=none
as this build requires no network access you can feel comfortable knowing the container won’t allow it.
The Dockerfile used in this example is simple and can be improved somewhat to make the image a bit smaller. However the following change should only be done once you’ve got your image working otherwise you’ll lose out the benefit of the cache when modifying it.
FROM ubuntu:20.04
ENV \
DEBIAN_FRONTEND=noninteractive \
CMAKE_GENERATOR=Ninja \
CFLAGS=-Wno-deprecated-declarations
RUN set -ex ;\
apt-get update ;\
apt-get install -y cmake ;\
apt-get install -y ninja-build ;\
apt-get install -y build-essential ;\
rm -rf /var/lib/apt/lists/*
Each command in a Dockerfile creates a layer which can be useful, and often not wasteful in space. If all you are doing is adding things to the filesystem them no space is wasted by splitting it over layers. If however you delete files in a command you don’t save any space because each layer is a diff from the previous one. In this Dockerfile the line rm -rf /var/lib/apt/lists/*
has been added at the end. This deletes the apt-get cache that is downloaded with apt-get update
. This can save a bit of space, however it is only useful if it all happens within the one layer. So it is important to have the rm
occur in the same RUN
command as the apt-get update
.
In this case the end image is 384 MB versus the original 413 MB. Whether that is important or not is a matter of opinion.