Building on Jenkins and deploying to a Docker container

4 min read 4 min read

I was surprised to discover how difficult it was to find relevant documentation and examples for something that "everybody" is supposed to do these days; building something in Jenkins and deploying the artifacts as a docker container. This was something that came up with a customer a few days ago, and I decided to learn how to do it by using a very simple C++ project, a SOCKS proxy server, as a testbed.

What I wanted to do was to compile the project, put the executable into a Docker container, test it, and publish the container to Docker Hub.

It took me 5 whole hours, and 21 iterations in Jenkins, to build the container. It took 36 iterations before I had everything right. (In another project that I am familiar with, it took even more time and about 80 iterations to get it right). Thats a lot of time wasted there, and it tells a tale about documentation that is not as good as it could be. I am not a novice when it comes do devops, as I have extensive experience for 30 years with development, build and operations. Now, to be fair, I am not an expert on Jenkins. I use it almost daily, but creating a new project from scratch is not something I do frequently.

So, lets see how to do this, as simple as possible. Since this task was just something I did to get familiar with how to solve this particular problem, I didn't add any additional complexity. The C++ code is built on my main Jenkins instance, the container use the same OS as the Jenkins machine (to get the dynamic library dependencies right). Neither Jenkins nor the builds are containerized.

The project I want to containerize is shinysocks. The relevant files are in the ci folder.

The container has a simple Dockerfile

FROM ubuntu:xenial MAINTAINER Jarle Aase <jgaa@jgaa.com> # In case you need proxy RUN apt-get -q update &&\ apt-get -y -q --no-install-recommends upgrade &&\ apt-get -y -q install libboost-system1.58.0 libboost-program-options1.58.0 \ libboost-filesystem1.58.0 libboost-coroutine1.58.0 libboost-log1.58.0 \ libboost-thread1.58.0 libboost-context1.58.0 && \ apt-get -y -q autoremove &&\ apt-get -y -q clean &&\ mkdir /etc/shinysocks COPY ci/shinysocks.conf /etc/shinysocks COPY build/shinysocks /usr/bin/shinysocks # Standard SOCKS port EXPOSE 1080 USER nobody # Default command CMD ["/usr/bin/shinysocks", "-c", "/etc/shinysocks/shinysocks.conf"]

Here I use the stock Ubuntu Xenial Docker image from Docker Hub and add some boost libraries required by the application that we will run in the container. I create a /etc/shinysocks directory, and copy an appropriate configuration file there from the ci folder. Then I copy the binary application shinysocks to /usr/bin . Finally, I declare that the container use TCP port 1080, switch to user nobody and define the default command to run when the container is started. The application will be started as user nobody for enhanced security.

Then we have the Jenkinsfile. I prefer to check in the Jenkinsfile with the source code, and just tell Jenkins where he will find that file.

#!/usr/bin/env groovy pipeline { agent { label 'master' } stages { stage ('Build') { steps { sh 'pwd' sh 'ls -la' sh 'rm -rf build' sh 'mkdir build' sh 'cd build && cmake -DCMAKE_BUILD_TYPE=Release .. && make' } } stage('Build Container') { steps { sh "docker build -t jgaafromnorth/shinysocks:v${env.BUILD_ID} -f ci/Dockerfile ." sh "docker tag jgaafromnorth/shinysocks:v${env.BUILD_ID} jgaafromnorth/shinysocks:latest" } } stage('Test Container') { steps { sh "docker run -d --rm -p 1080:1080 --name shinysocks-test jgaafromnorth/shinysocks:v${env.BUILD_ID}" sh "timeout 5 curl -x socks5://localhost:1080 https://google.com/" } } stage('Push to Docker Hub') { steps { withDockerRegistry([ credentialsId: "8cb91394-2af2-4477-8db8-8c0e13605900", url: "" ]) { sh "docker push jgaafromnorth/shinysocks:v${env.BUILD_ID}" sh 'docker push jgaafromnorth/shinysocks:latest' } } } } post { always { deleteDir() sh "docker stop shinysocks-test" } } }

The agent line is just to tell Jenkins to build this project on the main Jenkins instance (and not on my Mac Mini or Windows build machine).

I prefer the declarative variant of Jenkins pipelines.

The file should be easy to read and understand. When Jenkins builds a pipeline project with a Jenkinsfile, it will checkout the sources, in this case from github, before it starts processing the Jenkinsfile. So therefore there is no need for a checkout section.

The first step is to build the shinysocks server application with cmake. Cmake takes care of all the build steps, and if successful, we will end up with a binary file named shinysocks in the build folder. If you look at the Dockerfile above, it copies that file into the container.

The next step is to build the container. Despite many fancy plugins to Jenkins, I think the simplest approach is to use the command-line for this. Especially when we need to copy files from the Jenkins Workdir to the container. (I wasted quite some time trying to get the Docker plugins to do it). Note that I use env.BUILD_ID to name the container. This is a build number for the project that Jenkins increment for each build.

For the testing I could have used plugins, but the command-line approach is quite simple and easy to read for other developers. The test here is naive, just a curl command to see that the proxy works, - what some people would call an integration test. (If this was a popular project with ongoing development, I would probably have added unit tests and a suite of integration tests - but shinysocks was something I wrote in a few hours to solve an urgent problem I faced. Tests for such projects are just a waste of time.

The last step is to send the container to Docker Hub. In your own projects you many use another home for your containers - there are plenty of alternatives.

When I push the container, I use the Docker Pipeline plugin to deal with the authentication and actual location of the repository. I have created a free account on Docker Hub, and created a public repository for shinysocks there. If you look at the Jenkinsfile, there is a credentialsId with an uuid. This confused me, as the Docker Pipeline documentation, and the blogs I found with examples, gave no explanation to what this id refer to. It appears that Jenkins has a credentials section, and that you can add credentials there specifically for Docker Hub. If you add a username and password for Docker Hub, and don't specify an id yourself for the credentials, Jenkins will assign a uuid. This id is what the credentialsId refer to. For my use it's perfect - as I can put the secrets in the Jenkins credentials store - and refer to the random (and not at all secret) id in the public Jenkinsfile.

Finally, I add a post-build step to delete the workdir (to save space on the server disk) and stop the running container I used for testing (if the build process got that far). I do it in the post section to make sure that the container is actually stopped, no matter how the build broke (or succeeded).

So that's it. Mostly for my own reference, but may be these notes proves useful for someone else as well.