Use Docker to build, test and push your Artifacts

Sue is a software engineer at BetterSoft. She is in charge of starting a new project which includes building up the CI/CD pipeline for the new application her team will create. The company has established some standards and she knows she has to comply with those. The build server the company is using for all the products is a cloud based SaaS offering. The companys’ DevOps team is responsible to manage the build server, specifically its build agents. The other teams in the company are using Maven to build artifacts. Preferably the build artifacts are Docker images and Sues’ plan is use Docker too, to package and run the new application. While mostly leveraging the techniques used by other teams in their respective CI/CD pipelines Sue soon runs into some problems. Her build needs some special configuration of the build agent. Although the DevOps team is very friendly and helpful she has to file a ticket to get the task done, since DevOps is totally overbooked with work for other projects that have a higher priority in the company. After two days the ticket is finally addressed and closed and Sue can continue with her task. But while testing her build process, Sue stumbles across some other hurdles which requires a DevOps engineer to SSH into the build agent and manually solve the issue. Some files have been mysteriously locked and the CI server cannot remove them. Thus any further build is failing.

Does this sound somewhat familiar to you? If yes, then I hope this post is going to give you some tools and patterns on how to get your independence back and fully own the whole CI/CD process end to end. I want to show you how the use of containers can reduce the friction and bring CI (and CD) to the next level.

To read more of my posts about Docker please refer to this index.

What’s wrong with normal CI?

  • we have to specially configure our CI server and its build agents
  • we are dependent on some assistance from Ops or DevOps
  • we cannot easily scale our build agents
  • builds can change the state of the build agent and negatively impact subsequent builds
  • building locally on the developer machine is not identical to building on a build agent of the CI server
  • etc.

Containerize the build

These days our build artifacts most probably will be Docker images. So, if we want to run our build inside a container we need to have the Docker CLI available in the container. If this is the only major requirement and we’re only using some scripts for other build related tasks we can use the official Docker image. To get this image and cache it locally do

docker pull docker

To demonstrate how this is working we can now run an instance of the Docker image like this

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock docker /bin/sh

and we’ll find ourselves in a bash session inside the container. Note how we mount the Docker socket from the host into the container to get access to the Docker engine. We can then execute any Docker command the same way as we are doing it directly on the host, e.g.

docker images

Doing this we should see the list of all the Docker images in the cache of the host. Similarly we can build and run images from within our container that will live and be executed on the host. A sample run command could be

docker run --rm -it busybox echo "Hello World"

which will execute an instance of the busybox container image in the context of the host.

Cool! That was easy enough, we might say … so what? Well it is actually quite important because it really opens us the door to do some more advanced stuff.

Build the Artifact

Let’s say we have a Python-Flask project that we want to build. You can find the code here. The Dockerfile looks like this

FROM python:2.7
RUN mkdir -p /app
WORKDIR /app
COPY requirements.txt /app/
RUN pip install -r requirements.txt
COPY . /app
EXPOSE 5000
ENTRYPOINT python ./app.py

From the root of the project execute this command

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/app -w /app docker docker build -t myapi .

This will build a Docker image called myapi. After the build is done the container is terminated and removed but our image is still around sitting in the cache of the Docker host.

Now, building alone does not do the job. There are a few more tasks that we want to execute. Thus instead of running one command in a container at a time it is much better to run a whole script. Let’s do this and create a new file called builder.sh in the root of the project. To this file we will add all our CI code. We also will want to make this file executable

chmod +x ./builder.sh

So, what’s inside our builder.sh file? To start with we just add the docker build command

docker build -t myapi .

And then we can modify the above Docker command to look like this

docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $(pwd):/app \
  -w /app \
  docker ./builder.sh

This will give us the very same result as before, but now we have the possibility to extend the builder.sh file without changing anything in the docker run command.

Test the Artifact

The first thing we normally want to do is to run some tests against our built artifact. This normally means to run an instance of the built container image and execute a special (test-) script in the container instead of the actual starting command.

$IMAGE_NAME=myapi
docker build -t $IMAGE_NAME
docker run --rm -i --entrypoint test.sh $IMAGE_NAME

You might have noticed that I started to use variables in my script. This makes the whole thing more flexible as we will see further down.

Tag and Push image

Once we have successfully built and tested our artifact we are ready to push it to the repository. In this case I will use Docker hub. But before we can push an image we have to tag it. Let’s add the following snippet to our builder.sh script

$ACCOUNT=gnschenker
$TAG=1.0
docker tag $IMAGE_NAME $ACCOUNT/$IMAGE_NAME:$TAG
docker tag $IMAGE_NAME $ACCOUNT/$IMAGE_NAME:latest

Before we can push the images to the Docker Hub we need to authenticate/login. We can do that directly on our host using docker login and providing username and password. Docker will then store our credentials in $HOME/.docker.config.json. To use these credentials in the container we can map the folder $HOME/.docker to /root/.docker since inside the container we’re executing as root. Thus our modified docker run command will look like this

docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $HOME/.docker:/root/.docker \
  -v $(pwd):/app \
  -w /app docker \
  ./builder.sh

Finally after having taken care of the credentials we can push the images to the repository by adding this snippet to the builder.sh script

docker push $ACCOUNT/$IMAGE_NAME:$TAG
docker push $ACCOUNT/$IMAGE_NAME:latest

and we’re done.

Generalizing for re-use

Wouldn’t it be nice if we could reuse this pattern for all our projects? Sure, we can do that. First we build our own builder image that will already contain the necessary builder script and add environment variables to the container that can be modified when running the container. The Dockerfile for our builder looks like this

FROM docker:1.12
VOLUME /app
ENTRYPOINT /builder.sh

and the builder.sh looks like this

# build artifact
docker build -t $IMAGE /app

# test artifact
if [ -e /app/test/sh ]; then 
  /app/test.sh 
fi

# tag artifact
docker tag $IMAGE $ACCOUNT/$IMAGE:$TAG 
docker tag $IMAGE $ACCOUNT/$IMAGE:latest

# push artifact
docker push $ACCOUNT/$IMAGE:$TAG 
docker push $ACCOUNT/$IMAGE:latest

We can now build this image

docker build -t builder .

To be able to not only use this image locally but also on the CI server we can tag and push the builder image to Docker Hub. In my case this would be achieved with the following commands

docker tag builder gnschenker/builder:0.1
docker tag builder gnschenker/builder:latest

docker push gnschenker/builder:0.1
docker push gnschenker/builder:latest

Once this is done we can create add a file run.sh to our Python project which contains the overly long docker run command to build, test and push our artifact

docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $HOME/.docker:/root/.docker \
  -v $(pwd):/app \
  -e ACCOUNT=gnschenker \
  -e IMAGE=myapi \
  -e TAG=1.1 \
  gnschenker/builder:latest

Note how I pass values for the 3 environment variables ACCOUNT, IMAGE and TAG to the container. They will be used by the builder.sh script.

Once we have done this we can now use the exact same method to build, test and push the artifact on our CI server as we do on our developer machine. In your build process on the CI server just define a task which executes the above Docker run command. The only little change I would suggest is to use the variables of your CI server, e.g. the build number to define the tag for the image. For e.g. Bamboo this could look like this

docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $HOME/.docker:/root/.docker \
  -v $(pwd):/app \
  -e ACCOUNT=gnschenker \
  -e IMAGE=myapi \
  -e TAG=1.${bamboo.buildNumber} \
  gnschenker/builder:latest

Summary

In this post I have shown how we can use a Docker container to build, test and push an artifact of a project. I really only have scratched the surface of what is possible. We can extend our builder.sh script in many ways to account for much more complex and sophisticated CI processes. As a good sample we can examine the Docker Cloud builder.

Using Docker containers to build, test and push artifacts makes our CI process more robust, repeatable and totally side-effect free. It also gives us more autonomy.

About Gabriel Schenker

Gabriel N. Schenker started his career as a physicist. Following his passion and interest in stars and the universe he chose to write his Ph.D. thesis in astrophysics. Soon after this he dedicated all his time to his second passion, writing and architecting software. Gabriel has since been working for over 25 years as a consultant, software architect, trainer, and mentor mainly on the .NET platform. He is currently working as senior software architect at Alien Vault in Austin, Texas. Gabriel is passionate about software development and tries to make the life of developers easier by providing guidelines and frameworks to reduce friction in the software development process. Gabriel is married and father of four children and during his spare time likes hiking in the mountains, cooking and reading.
This entry was posted in CI/CD, containers, docker and tagged , , , , . Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Humm, this does not work

    docker run –rm -it -v /var/run/docker.sock:/var/run/docker.sock docker /bin/bash

    root@svr034:~# docker run –rm -it -v /var/run/docker.sock:/var/run/docker.sock docker /bin/bash
    /usr/local/bin/docker-entrypoint.sh: exec: line 19: /bin/bash: not found
    root@svr034:~# docker run –rm -it -v /var/run/docker.sock:/var/run/docker.sock docker bash
    /usr/local/bin/docker-entrypoint.sh: exec: line 19: bash: not found
    root@svr034:~#

    • gabrielschenker

      Sorry Pascal, my bad. The Docker image does not have bash installed. use /bin/sh instead. I’ll update the post

  • Abhishek Gupta

    Are we executing this command “docker run –rm -it -v /var/run/docker.sock:/var/run/docker.sock docker /bin/sh” because of the reason that our agent docker image is already running as container on docker host, and we are intending to run a build job as another container within that running agent container, so the case here is container within container ( docker within docker). And, that is why we mounted the deamon socket of docker engine running on the host ( the host on which agent container image is running ) into the the new container that we spun up to run the job of build so that when we attach the volume of the app code for build, it builds in the context of the original host, not the build container. Please clarify.

  • Abhishek Gupta

    Is the build agent also contained within container in this case? If it is, where is it? Please respond.