Automate Docker Image Versioning with Jenkins

Versioning your Docker images is a good practice that will make them easier to deal with, whether they are used only privately or are offered to the public. With each iteration of an image, regression is always a possibility. To prevent the disruption of services for yourself and others, it is always a good idea to maintain a stable versioning system. By using semantic versioning, we are able to categorize our iterations clearly and allow the subscriber to choose which they want.

Versioning with Docker is easy, but pushing several tags manually each time the version changes is arduous. Using continuous integration, we can automate the process so that versioned images are pushed each time we make a change in our repository. For this tutorial, we will use Jenkins as our CI technology, but the solutions here are simple and can be applied to any CI.

  1. Give your Docker image a semantic version.
  2. Using CI, push your image to the latest, major, minor, and patch version tags on each successful build.

Getting Started

For this tutorial, we will be re-purposing the code from How to Dockerize Node.js. You can use the code from GitHub as a starting point.

If you don’t already have a Jenkins instance set up, I suggest following at least the setup steps of this tutorial. Also be sure the Docker Pipeline plugin is installed, although it is generally installed by default.

Lastly, if you are following along with Jenkins, you will need to add your DockerHub credentials as a username/password Jenkins credential with the id my-dockerhub-credentials.

Version Your Image

The first and most obvious step is to give our Docker image a version. Create a file named VERSION at the project’s root directory. This file should contain your image’s semantic version. For instance:

1.0.0

This file should be committed to your repository and updated according to semantic versioning guidelines when your Docker image changes.

Although not required, it is a good idea to copy this file into the image itself. This is so that the version can be determined from inside the image. Add the following line to your Dockerfile.

COPY VERSION /

If you are following along with the example code, the resulting Dockerfile should now look like this:

FROM node:8.15.0-jessie

WORKDIR /home/node/app
COPY package*.json ./
RUN npm install

COPY VERSION /
COPY . ./

EXPOSE 3000

CMD ["npm", "start"]

Automate with Jenkins

Now that we have a versioned image, we need to make Jenkins build and push the image to the appropriate version tags. Create a Jenkinsfile at the project’s root repository with the following contents (replacing my DockerHub username, levipayne, with yours).

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                script {
                    def version = readFile('VERSION')
                    def versions = version.split('\\.')
                    def major = versions[0]
                    def minor = versions[0] + '.' + versions[1]
                    def patch = version.trim()

                    docker.withRegistry('', 'my-dockerhub-credentials') {
                        def image = docker.build('levipayne/blog-docker-versioning:latest')
                        image.push()
                        image.push(major)
                        image.push(minor)
                        image.push(patch)
                    }
                }
            }
        }
    }
}

If you are unfamiliar with Jenkins, this is a basic pipeline definition with only one stage. As everything outside of the stage is boilerplate, we will break down the internals of it one block at a time.

def version = readFile('VERSION')
def versions = version.split('\\.')
def major = versions[0]
def minor = versions[0] + '.' + versions[1]
def patch = version.trim()

This block parses the VERSION file we created and breaks it down into the three parts of a semantic version; major, minor, and patch. For version 1.0.0, major is 1, minor is 1.0, and patch is 1.0.0.

docker.withRegistry('', 'my-dockerhub-credentials') {

This syntax comes courtesy of the Docker Pipeline plugin. Although it is generally used to define a custom registry, we can use it only to authenticate with our DockerHub credentials by leaving the registry parameter blank. This ensures that we are authorized to push tags to our DockerHub repository.

def image = docker.build('levipayne/blog-docker-versioning:latest')
image.push()
image.push(major)
image.push(minor)
image.push(patch)

Lastly, here we build the image and push the tagged images. The first push pushes the latest tag. This tag should always be associated with the latest image, as the name suggests. Also, we push up the major, minor, and patch versions separately.

How it Works

If your Jenkins build was a success, you should have some new tags on DockerHub. Take a look at them, or alternatively, look at mine. You should see tags latest, 1, 1.0, and 1.0.0.

Now, consider what would happen if we updated the version to 1.0.1. You would then see the following versions on DockerHub.

  • 1 (Just updated)
  • 1.0 (Just updated)
  • 1.0.0 (Not updated)
  • 1.0.1 (Just updated)

Those using the 1.0.0 (patch) version would not be affected by these changes. Those using the 1.0 (minor) version will get all patch updates, but not major or minor updates. Those using the 1 version will get all minor and patch updates, but not major updates. Lastly, anyone using the latest tag will be subject to all updates to the image.

Closing Thoughts

Using automated semantic versioning for our Docker images makes it easier for users to subscribe only to iterations they wish to receive. It also preserves previous versions in case of regressions or changes that break backwards-compatibility.

For full example code, see the repository on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.