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
RUN apt-get update
RUN apt-get install -y cmake
RUN apt-get install -y ninja-build
RUN apt-get install -y build-essential
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.
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
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 .
-t build-linux gives the image a name. In this case I’ve called it
'.' 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
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
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.
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
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.