Since Jenkins was born in another IT age, pre-Cloud and pre-DevOps (and was called Hudson at this time), it appears not that lightweight and straightforward manageable as we might expect of today’s tools. Still, it’s the most prominent CI tool and deserves attention. Furthermore with recent features (see below) Jenkins seems to catch up with CI tools 2.0

Let’s name some challenging moment with Jenkins

  • Jenkins Plugin hell dependencies management - that are tending to make the system fragile e.g. on updates.
  • XML based configuration, that is not documented well and differs from plugin to plugin
  • Dependency on used tools. I mean Jenkins depends on tools that are used in Jenkins Jobs. E.g. java, go, c++, maven, ant, python.. in different versions, that are tending to live their own lives.

But meanwhile, we have tools and methods to handle these Jenkins problems. In 2017 we simply can run Jenkins in a Docker container and profit from better Isolation and reproducibility of Jenkins installation. Let’s gets hands on it…

Jenkins Docker image

Of course, there is Official Jenkins Container, so we don’t need to start from scratch.

FROM jenkins:2.46.3
...

This can be a beginning of your customized Dockerfile. Version 2.46.3 is current LTS Jenkins version. Next will be probably 2.60.0.

Let’s improve the Jenkins plugin management form begin on. The nice thing about the official Docker image is, that it includes a script that is best for pre-install desired plugins. And this with (automatic) dependency resolution. Here an example:

FROM jenkins:2.46.3
#Install plugins with dependencies
RUN /usr/local/bin/install-plugins.sh workflow-aggregator:2.5 workflow-job:2.11 \
    git \
    maven-plugin m2release checkstyle findbugs jacoco \
    dashboard-view disk-usage jobConfigHistory \ 
    scm-sync-configuration \
...

We see some well know plugins whereat workflow-aggregator and workflow-job are pinned to a specific version. I the case of workflow-job it was needed since the latest version (2.12) is transitively included by workflow-aggregator but requires Jenkins jenkins:2.60> which is not in LTS status now (bug from plugin maintainers I would say). However, version pinning and explicit declaration are there to help.

You need to decide what else you need to be included in your Docker image.

Jenkins Configuration

Another nice feature of the official Jenkins image is the fact that everything that you place to /usr/share/jenkins/ref/ during build time will be placed to /var/jenkins_home on container start.

So just COPY/ADD need files like:

# Jenkins config
COPY configuration/*.groovy /usr/share/jenkins/ref/init.groovy.d/
COPY configuration/log.properties /usr/share/jenkins/ref/log.properties.override

By the way, you can do all pre-configurations you need via groovy scrips. Here e.g. we declare the default java temp directory and disable Jenkins installation Wizard.

import jenkins.*
import hudson.*

Thread.start {
    System.setProperty('jenkins.install.runSetupWizard', "false")
    println "--> Startup Wizard is disabled"

    System.setProperty('java.io.tmpdir','/var/jenkins_home')
    println "--> Setting java.io.tmpdir"
}

E.g. you may want to include or better generate some ssh-keys that can be used for identification into the Docker image but maybe it’s also better to configure such thing later in the UI by placing it in the secure store of Jenkins - decisions you do on yourself.

More likely you want to trust your organization and committing public keys is Ok. So here is an example hot to teach java that is of course included into Jenkins container to trust your root certificates.

COPY ssl/*.pem /etc/pki/java/

#Jenkins container runs with user Jenkins so we need to switch to root temporary.
USER root

# SSL Trust Certs: Importing to java & to system
RUN bash -c 'echo "Yes" | /usr/lib/jvm/java-8-openjdk-amd64/bin/keytool -import -alias my Root -file /etc/pki/java/myrootca.cert.pem -keystore /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/cacerts -storepass secretpass'

#And placing it in the system so e.g. subversion can access your server via SSL
RUN cp /etc/pki/java/myrootca.cert.pem /usr/local/share/ca-certificates/ && update-ca-certificates
#back to user Jenkins
USER jenkins

And here is an example of AWS CLI installation

# AWS CLI 
RUN curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip" && unzip awscli-bundle.zip && ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws

Also, you might want to install some system packages. At this point, you should think twice, because you might run into the problem that you need to rebuild your Jenkins image on changing packet version or you need that packages in different version for different jobs and everything get messy. But if it’s not an issue here is an example for installation of Ansible

#Ansible source list
RUN echo "deb http://ppa.launchpad.net/ansible/ansible/ubuntu trusty main" >> /etc/apt/sources.list && \
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 93C4A3FD7BB9C367

# Install additional packages.
RUN apt-get update && apt-get install -y ansible python-dnspython && rm -rf /var/lib/apt/lists/*

if you’re trusting your job builders you can include sudo package as well and define rules

# Allow Jenkins user to execute  /path/to/my/program as root
RUN bash -c 'echo "jenkins ALL=(ALL) NOPASSWD: /path/to/my/program" >> /etc/sudoers' 

Please consider the risks, do not use keyword ALL instead of a specific program if possible.

Docker client

Next important thing is to have a possibility to build docker images on your Jenkins. Obviously, it can be a good idea to use the same docker host where our Jenkins container runs. First, we need to install the docker client inside of the container.

USER root
RUN curl https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz | tar xvz -C /tmp/ && mv /tmp/docker/docker /usr/bin/docker
USER jenkins

But this requires further actions that we need to do if the container starts. For this, we change the entry point to our custom script, which is capable to execute needed actions before starting Jenkins.

ENTRYPOINT ["/bin/tini", "--", "/usr/local/bin/init.sh"]

to wire docker client from inside of the container to the hosts docker-engine we need to start the Jenkins with volume that is docker engine’s socket (file representation on Linux)

docker run -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock my-jenkins

and in the init.sh we put the following.

#!/usr/bin/env bash
echo "=== Docker INFO ======================================="
if [ -S ${DOCKER_SOCKET} ]; then
    echo "Mapping docker socket"
    sudo chmod 777 /var/run/docker.sock
fi
...

of course, we can do all other need run-time configuration here. To start Jenkins we just include these lines at the end.

echo "==== Finishing prestarting actions ===================="
echo "Starting /usr/local/bin/jenkins.sh as $(whoami) - $(id)"

exec /usr/local/bin/jenkins.sh;

Jenkins Declarative Pipeline

Now we did, but there is more. Take a look on Jenkins Declarative pipelines. Here is one example to illustrate something important.

pipeline {
    agent none 
    stages {
        stage('Example Build') {
            agent { docker 'maven:3-alpine' } 
            steps {
                echo 'Hello, Maven'
                sh 'mvn --version'
            }
        }
        stage('Example Test') {
            agent { docker 'openjdk:8-jre' } 
            steps {
                echo 'Hello, JDK'
                sh 'java -version'
            }
        }
    }
}

Most certainly an attentive reader has already noticed that compilation steps are executed in different docker containers and the workspace is prepared automatically. This is the answer to the complexity of “tool configuration” and is much readable and maintainable than native XML-based job configuration. You don’t need to install maven or java. Docker images are predefined and downloaded automatically. So this pipeline is reusable after upgrades or disasters and on other Jenkins installations. You can put this declaration into s.c. Jenkinsfile and commit it to your SCM.

Worth to mention, Jenkins team is working on Editor for pipelines However I prefer to write it directly, and syntax reference might be helpful here.

Thank you for reading, and thank you for your comments.