Creating a Template Jenkinsfile for Building Docker Images Using Multi-Branch Pipeline Jobs

Learn how to create a reusable Jenkinsfile for Docker image builds. I’ll show you how to set it up with multi-branch pipelines so Jenkins can auto-detect branches, run isolated builds, and streamline your workflow.

Creating a Template Jenkinsfile for Building Docker Images Using Multi-Branch Pipeline Jobs

Introduction

When working with Docker images, consistency and automation are key. To simplify my future Docker image builds, I decided to create a reusable Jenkinsfile template. This Jenkinsfile will handle the necessary steps for building Docker images and is designed to work seamlessly with Jenkins’ Multi-Branch Pipeline jobs.

In this post, I’ll guide you through creating a Jenkinsfile for Docker builds, configuring it to work with multi-branch pipelines, and explaining how Jenkins can automatically detect branches, run isolated builds, and streamline your development process.

1. Why Use a Jenkinsfile for Building Docker Images?

Manually configuring Jenkins jobs for Docker builds can quickly become repetitive and error-prone. By using a Jenkinsfile:

  • Reusable Automation: Define all build steps in code and reuse them across projects.
  • Consistency: Ensure every Docker image is built using the same steps, eliminating environment discrepancies.
  • Version Control: Store the Jenkinsfile alongside your code in Git, so changes to the build process are tracked.
  • Multi-Branch Pipeline Support: Jenkins automatically detects branches in your repository and executes builds independently.

2. Preparing the Project Structure

To start, we need a repository that contains a Dockerfile. The Dockerfile does not have to be in the root directory—it can reside in any folder. For simplicity, I’ll keep both the Dockerfile and the Jenkinsfile together in a single folder:

/<root folder>
    └── infra
        └── docker
            ├── Dockerfile  
            └── Jenkinsfile
 

If your Dockerfile is located in a subdirectory, don’t worry—I’ll show you how to adjust the Jenkinsfile to handle this.

3. Understanding Multi-Branch Pipeline Jobs

Jenkins’ Multi-Branch Pipeline is a powerful feature that enables you to:

  • Automatically scan a repository for all branches.
  • Detect if a Jenkinsfile exists in each branch.
  • Create separate pipeline jobs for each branch.

This feature is particularly useful for testing changes in feature branches before merging them into the main branch. Developers can test their builds in isolation, reducing the risk of breaking the main branch.

4. Creating the Jenkinsfile

Here’s a basic Jenkinsfile template to build a Docker image:

def customImage

pipeline {
    // Agent
    agent any

    // Environment
    environment {
        OS_VERSION='24.04'
    }

    // Parameters
    parameters {
        choice(
            name: 'Version: ',
            choices: ['24.04',
                      '22.04',
                      '20.04'],
            description: 'Ubuntu Version'
        )
        booleanParam(
            name: 'PUSH',
            defaultValue: false,
            description: 'Push Docker image to Docker Hub'
        )
    }

    // Options
    options {
        timestamps()
        ansiColor('xterm')
    }

    // Job
    stages {

        // Version
        stage('Get Version') {
            steps {

                script {
                    setVersion()
                }

                echo "########"

                echo "GIT_BRANCH: ${GIT_BRANCH}"
                echo "VERSION   : ${params.version}"
                echo "BUILD_ID  : ${env.BUILD_ID}"

                echo "########"
            }
        }

        // Build
        stage('Build Docker Image') {
            steps {
                script {
                    echo 'Building Docker Images ... '
                    echo "Branch Name: ${BRANCH_NAME}"
                    echo "Branch Name timmed: ${ env.BRANCH_NAME.trim() }"

                    dir('infra/docker/ubuntu/') {
                        sh 'ls -la '
                        customImage = docker.build("dolpa/ubuntu:${params.version}")

                    }
                }
            }
        }

        // Test
        stage('Run Tests') {
            steps {
                script {
                    // Run tests inside the Docker container
                    echo "TODO: Test framework for Docker Images isn't implemented yet!!!"
                }
            }
        }

        // Push Image
        stage('Push Image') {
            when {
                // Push Docker Image only if the branch is main or release, or force push
                expression { env.BRANCH_NAME.trim().startsWith("release") || env.BRANCH_NAME.trim().startsWith("main") || parameters.PUSH }
            }
            steps {
                script {
                    //
                    docker.withRegistry('', 'Jenkins (local server) to Docker Hub (dolpa)') {

                        /* Push the container to the custom Registry */
                        customImage.push()

                    }
                }
            }
        }
    }

    post {
        success {
            echo 'Build and tests completed successfully.'
        }
        failure {
            echo 'Build or tests failed.'
        }
    }
}

// Global Functions
def setVersion() {
    branchName = env.BRANCH_NAME.trim()

    if( branchName == "main" ) {
        env.version = "latest"
    }
    else if(branchName.startsWith("feature") ) {
        env.version = branchName.split('/')[1]+"-SNAPSHOT"
    }
    else if( branchName.startsWith("hotfix")) {
        env.version = branchName.split('/')[1]+"-SNAPSHOT"
    }
    else if( branchName.startsWith("release") ) {
        env.version = branchName.split('/')[1]
        env.app_version = version
    }
}

Breaking Down the Jenkinsfile

Here’s a detailed line-by-line explanation of the Jenkins pipeline script:

Pipeline Definition:

pipeline {

Starts the declarative pipeline definition.

Agent:

    agent any

Runs the pipeline on any available Jenkins agent (worker node).

Environment Variables:

    environment {
        OS_VERSION='24.04'
    }

Declares OS_VERSION as an environment variable with a default value of 24.04.

Parameters:

    parameters {

Declares input parameters for the Jenkins pipeline.

Choice Parameter:

        choice(
            name: 'Version: ',
            choices: ['24.04',
                      '22.04',
                      '20.04'],
            description: 'Ubuntu Version'
        )
  • Allows the user to select a specific Ubuntu version (24.04, 22.04, or 20.04).
  • Named Version: for clarity.
  • Default value is 24.04

Boolean Parameter:

        booleanParam(
            name: 'PUSH',
            defaultValue: false,
            description: 'Push Docker image to Docker Hub'
        )
  • Adds a boolean input parameter named PUSH.
  • Default is false.
  • Controls whether the Docker image should be pushed to Docker Hub.

Pipeline Options:

    options {
        timestamps()
        ansiColor('xterm')
    }
  • timestamps() adds timestamps to the build log.
  • ansiColor('xterm') enables colored terminal output.

Stages:

    stages {

Begins defining the stages for the pipeline.

Stage 1: Get Version

        stage('Get Version') {
            steps {
                script {
                    setVersion()
                }
                echo "########"

                echo "GIT_BRANCH: ${GIT_BRANCH}"
                echo "VERSION   : ${params.version}"
                echo "BUILD_ID  : ${env.BUILD_ID}"

                echo "########"
            }
        }
  • Purpose: Determines the application version based on the branch name.
  • Calls the setVersion() function (defined globally) to set the version dynamically.
  • Logs:
    • GIT_BRANCH: Branch name.
    • VERSION: Resolved version value.
    • BUILD_ID: Jenkins build ID.

Stage 2: Build Docker Image

        stage('Build Docker Image') {
            steps {
                script {
                    echo 'Building Docker Images ... '
                    echo "Branch Name: ${BRANCH_NAME}"
                    echo "Branch Name timmed: ${ env.BRANCH_NAME.trim() }"

                    dir('infra/docker/ubuntu/') {
                        sh 'ls -la '
                        customImage = docker.build("dolpa/ubuntu:${params.version}")
                    }
                }
            }
        }
  • Purpose: Builds a Docker image.
  • Logs the current branch name and trims any whitespace from BRANCH_NAME.
  • Uses the docker.build() method to build a Docker image named dolpa/ubuntu:${params.version}.
  • The Dockerfile is expected in infra/docker/ubuntu/.

Stage 3: Run Tests

        stage('Run Tests') {
            steps {
                script {
                    echo "TODO: Test framework for Docker Images isn't implemented yet!!!"
                }
            }
        }
  • Placeholder for running tests inside the Docker container.
  • Currently outputs a "TODO" message.

Stage 4: Push Image

        stage('Push Image') {
            when {
                expression { env.BRANCH_NAME.trim().startsWith("release") || env.BRANCH_NAME.trim().startsWith("main") || parameters.PUSH }
            }
            steps {
                script {
                    docker.withRegistry('', 'Jenkins (local server) to Docker Hub (dolpa)') {
                        customImage.push()
                    }
                }
            }
        }
  • Purpose: Pushes the built Docker image to Docker Hub.
  • when condition:
    • Pushes only if:
      • Branch name starts with release or main.
      • OR the PUSH parameter is set to true.
  • docker.withRegistry: Uses Jenkins credentials (Jenkins (local server) to Docker Hub (dolpa)) for Docker Hub authentication.
  • customImage.push(): Pushes the Docker image.

Post-Build Actions

    post {
        success {
            echo 'Build and tests completed successfully.'
        }
        failure {
            echo 'Build or tests failed.'
        }
    }
  • Success: Logs a success message when the pipeline completes successfully.
  • Failure: Logs a failure message if any stage fails.

Global Function: setVersion()

def setVersion() {
    branchName = env.BRANCH_NAME.trim()

    if( branchName == "main" ) {
        env.version = "latest"
    }
    else if(branchName.startsWith("feature") ) {
        env.version = branchName.split('/')[1]+"-SNAPSHOT"
    }
    else if( branchName.startsWith("hotfix")) {
        env.version = branchName.split('/')[1]+"-SNAPSHOT"
    }
    else if( branchName.startsWith("release") ) {
        env.version = branchName.split('/')[1]
        env.app_version = version
    }
}
  • Purpose: Dynamically determines the version based on the branch name.
  • Logic:
    • main branch → version = latest.
    • feature/* branch → version = <feature-name>-SNAPSHOT.
    • hotfix/* branch → version = <hotfix-name>-SNAPSHOT.
    • release/* branch → version = the release version number.

Summary

  1. The pipeline builds and optionally pushes a Docker image for Ubuntu.
  2. Dynamic Versioning: The image tag (version) depends on the branch name.
  3. The pipeline allows manual control to push images via the PUSH parameter.
  4. Structure:
    • Get Version
    • Build Docker Image
    • Run Tests (placeholder)
    • Push Image (conditionally).
  5. Post-build steps log success or failure.

5. Configuring a Multi-Branch Pipeline Job in Jenkins

Follow these steps to create a Multi-Branch Pipeline job in Jenkins:

  1. Navigate to Jenkins Dashboard
    • Click on New Item.
    • Select Multi-Branch Pipeline and provide a name.
  2. Source Code Management Configuration
    • Under Branch Sources, select Git.
    • Provide the repository URL.
    • Configure credentials if needed.
  3. Scan Branches
    • Jenkins will scan the repository for all branches.
    • If it detects a Jenkinsfile in any branch, it will create a separate pipeline for that branch.
  4. Triggering a Scan
    • After configuring the job, click on Scan Repository Now.
    • Jenkins will scan for branches and create jobs for each one.
    • You can view the scan log to ensure the branches and Jenkinsfile are detected correctly.

6. Testing the Pipeline

To test the Multi-Branch Pipeline:

  1. Make changes to the Dockerfile or Jenkinsfile.
  2. Jenkins will detect the new branch and automatically create a pipeline job for it.
  3. Verify that the build runs successfully:
    • Check the console logs for each stage.
    • Confirm that the Docker image is built and pushed to the registry.

Push the branch to the remote repository:

git push origin feature/test-branch  

Create a new branch in your Git repository:

git checkout -b feature/test-branch  

7. Handling Dockerfile in a Subdirectory

If your Dockerfile is not in the root directory, modify the docker build command in the Jenkinsfile:

sh "docker build -t ${DOCKER_IMAGE} ./path/to/Dockerfile-directory"  

This tells Docker to look for the Dockerfile in the specified subdirectory.

8. Troubleshooting Common Issues

  1. Branch Not Detected
    • Go to the Scan Repository Log and verify that Jenkins is scanning the correct branches.
  2. Docker Build Fails
    • Verify the Dockerfile syntax and dependencies.
    • Ensure the Jenkins agent has access to the Docker daemon.
  3. Docker Push Fails
    • Check Docker registry credentials.
    • Verify that the registry URL is correct.

Conclusion

Using Jenkins Multi-Branch Pipeline jobs with a template Jenkinsfile simplifies the process of building Docker images. By automating the build and push steps, developers can test their changes in isolated branches without affecting the main branch.

The ability to automatically scan branches, detect Jenkinsfiles, and run independent builds makes Jenkins an invaluable tool for CI/CD pipelines. Combine this with Docker, and you have a powerful, reproducible build environment ready for any project.

If you enjoyed this guide, check out my other posts on Jenkins Cloud Configuration with Docker.

Happy building! 🚀

Read next

Building Docker Images as Part of a Jenkins Pipeline

CI/CD are crucial practices in modern software development, allowing teams to deliver high-quality software quickly and reliably. Docker, with its containerization capabilities, has become an essential tool in these processes, particularly when integrated with Jenkins

Setting Up Jenkins with JCasC (Jenkins Configuration as Code)

In today’s software development, managing Jenkins through its GUI can become challenging as the complexity and number of jobs grow. Jenkins Configuration as Code (JCasC) provides a powerful solution to this by allowing Jenkins configuration to be defined and managed via code