With the upcoming end-of-life for Bamboo Cloud, I’m in the market for a new build server setup. For this1 experiment, I’m returning to an old favourite – Jenkins – paired with a potential new favourite – Docker. In this post, I describe how I’ve set up a Jenkins server in a Docker container, using the Multibranch Pipeline plugin to automatically configure a simple build2.
I’m looking to set up a Jenkins server via Docker to address some key points:
- I want most of the configuration for the server to be under version control.
- I want the ability to run the build server locally on my machine when I’m experimenting with new features or configurations
- I want to easily be able to set up a build server in a new environment (e.g. on a local server, or in a cloud environment such as AWS)
Docker addresses these points pretty well, as far as I can see. I’m no Docker expert, though – this is literally the first thing I’ve tried to do with this. But it’s a good starting point.
For this article, I want to get a Jenkins server, managed with Docker, to the point where I can easily set up and teardown a container locally. For reference, I’m using Docker for Mac 1.12 and Jenkins 2.7.1 (the stable versions at the time of writing), and much of the information here came from a superb set of articles from Maxfield Steward of RiotGames.com and the documentation for the Official Jenkins Docker Image.
Steps
Go and install Docker
You will need to go and get Docker. That’s a given. I used Docker for Mac, because I’m a Mac user. Your mileage may vary.
You’ll also want Docker Compose, if you follow these instructions. Docker Compose makes working with multiple related containers easier. It comes as part of the install bundle for the Mac and Windows versions, but needs to be obtained separately for Linux.
Create Docker Files
Following the tutorial from RiotGames, I created a working directory with the following layout:
- jenkins_docker
- .env
- docker-compose.yml
- jenkins_data/
- Dockerfile
- jenkins_master/
- Dockerfile
- conf/
- plugins.txt
- jenkins_nginx/
- Dockerfile
- conf/
- jenkins.conf
- nginx.conf
This setup creates:
- a ‘data volume container’ to hold the Jenkins Home directory and the Jenkins logs directory.
- a container for the Jenkins Server itself
- a container for nginx, to be used as a reverse proxy in front of Jenkins.
Let’s go through each file, starting with the Dockerfiles
data/Dockerfile
# Needs to match the linux version used in jenkins-master | |
FROM debian:jessie | |
MAINTAINER Robert Watkins | |
# user 1000 must match the user id for the jenkins user in jenkins-master | |
RUN useradd -d "/var/jenkins_home" -u 1000 -m -s /bin/bash jenkins | |
RUN mkdir -p /var/log/jenkins | |
RUN chown -R jenkins:jenkins /var/log/jenkins | |
VOLUME ["/var/log/jenkins", "/var/jenkins_home"] | |
USER jenkins | |
CMD ["echo", "Data container for Jenkins"] |
This creates a container to manage the data volumes – one of the recommended best practices for using Docker and Jenkins. By moving the Jenkins Home directory into a volume, I can easily create, destroy, and recreate the main Jenkins container – something that needs to be done when upgrading the image.
This container will hold the ‘unmanaged’ Jenkins content – things I set up through the UI (such as security & users) – as well as the build artefacts.
master/Dockerfile
FROM jenkins:2.7.1 | |
MAINTAINER Robert Watkins | |
USER root | |
RUN mkdir /var/log/jenkins | |
RUN mkdir /var/cache/jenkins | |
RUN chown -R jenkins:jenkins /var/log/jenkins | |
RUN chown -R jenkins:jenkins /var/cache/jenkins | |
USER jenkins | |
ENV JAVA_OPTS="-Xmx4096m" | |
# install plugins; the plugins.txt file can be exported from Jenkins like this: | |
# JENKINS_HOST=username:password@myhost.com:port | |
# curl -sSL "http://$JENKINS_HOST/pluginManager/api/xml?depth=1&xpath=/*/*/shortName|/*/*/version&wrapper=plugins" | perl -pe 's/.*?<shortName>([\w-]+).*?<version>([^<]+)()(<\/\w+>)+/\1 \2\n/g'|sed 's/ /:/' > jenkins-master/conf/plugins.txt | |
COPY conf/plugins.txt /var/jenkins_home/plugins.txt | |
RUN /usr/local/bin/plugins.sh /var/jenkins_home/plugins.txt | |
# "For 2.x-derived images, you may also want to" - we're 2.0 dervied, so we want this | |
RUN echo 2.0 > /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state | |
# Put the log file into the log directory, which will be in the data volume | |
# Move the WAR out of the persisted jenkins data dir | |
ENV JENKINS_OPTS="--logfile=/var/log/jenkins/jenkins.log --webroot=/var/cache/jenkins/war" |
This is my configuration of the Jenkins server. It’s pretty close to the example from the RiotGames tutorial, but I’ve added the section between lines 13 and 18, where I select which the plugins to be installed. If you omit this, Jenkins will prompt to install plugins for you on startup. (Actually, even with this, it prompts to install – but these will already be selected and available).
My personal plugins list, at this time, consists of the common plugins, plus the Bitbucket Branch Source Plugin, because we use Bitbucket. This plugin will allow me to create new jobs for Jenkins without having to configure Jenkins itself – an important step in keeping as much Jenkins configuration in source control as possible.
If you’re not sure what plugins you wish to use, I’d advice commenting these lines out for now, then generating a plugins.txt file and enabling the lines again later.
nginx/Dockerfile
This is a basic nginx configuration, based on the RiotGames tutorial. Rather than embed it directly, I direct interested readers to the relevant gist.
docker-compose.yml
data: | |
build: jenkins-data | |
master: | |
build: jenkins-master | |
volumes: | |
- .:/backup | |
volumes_from: | |
- data | |
ports: | |
- "50000:50000" | |
nginx: | |
build: jenkins-nginx | |
ports: | |
- "${HTTP_PORT}:80" # Need to make this configurable, as we'll want 80:80 in 'prod' environments | |
links: | |
- master:jenkins-master |
The docker-compose.yml
file ties these three containers together. The data
container provides the volumes used by the master
container to store Jenkins data, whilst the nginx
container provides a suitable reverse proxy.
One point to note is that it’s possible to change which port on the machine running the Docket container is used to expose nginx. The default – specified in a .env
file – should be 80, but that’s not suitable for a lot of developer machines (such as my own), so we need to override it.
Lastly, note that the master
container mounts the present working directory as /backup
– this is used later to make it easy to extract data from the container.
Build and Start The Containers
We can now use Docker-Compose to build the containers, and then start them.
$ docker-compose build $ docker-compose up -d
At this point, the containers should be running, and you should be able to go [http://localhost/] to see your Jenkins instance (add a port to the URL as needed). When you get there, you’ll be asked to enter the initial administration password – which will be inside your container. So how do you see it? With docker-compose exec
!
$ docker-compose exec master cat /var/jenkins_home/secrets/initialAdminPassword <randomly generated password here>
Once you’ve entered the initial admin password, you will, as previously mentioned, be prompted to install plugins. After that, you’ll need to make your first user, and your first job.
Backing up Jenkins Data
An important step is to know how to back up and restore your Jenkins data. The exact way to do this depends on how you’ve configured your Docker containers – assuming you’ve used a data volume container like I did, you can issue a command to extract data for backup purposes:
$ docker-compose stop $ docker-compose run --rm master tar cvfz /backup/backups.tgz /var/jenkins_home $ docker-compose up -d
If you need to restore this data, you run the exec in reverse:
$ docker-compose stop $ docker-compose run --rm master bash -c "cd /var/jenkins_home && tar xvfz /backup/backup.tgz --strip 1" $ docker-compose up -d
The server that you run the Docker containers on can then be configured to perform the backups on a regular basis via a cron task or similar.
Deploying to a Production Server
This step I haven’t yet gotten around to; I’ve still got several months to get off Bamboo Cloud, and I haven’t decided exactly where I’ll host the Jenkins server (assuming that’s what I end up with).
One likely place, however, will be on Amazon’s OpsWorks infrastructure – we use that for our production servers now, so putting a new instance up there will be straight forward. It will require building a new Chef recipe for deployment, but that’s reasonably straightforward.
Where to from here?
At this point, if you’ve been following along, you’ve got a suite of Docker containers that you can use to run a Jenkins server. I’d suggest committing the files you’ve created along the way to version control, so you can keep track of the changes you’re making.
After that – create some jobs in Jenkins! Get building. Along those lines, my next post in this series will be about what’s involved in getting a multi-branch pipeline configured for Maven. Until then, happy reading.
- For a previous, less successful, experiment, see my post on Bitbucket Pipelines ↩
- More complex build scenarios will come later, in subsequent blog posts. ↩
What if the uid 1000 is occupied and cannot be swapped?
UUID 1000 is what is used by the Jenkins Docker container; if you can’t live with that, you’ll need to change it there. Fortunately, you can do that at build time, using the –build-arg parameter (e.g. –build-arg uid=someothervalue).
For the full list of configurable parameters, see the source code for the Jenkins Docker container