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, or20.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 theversiondynamically. - 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 nameddolpa/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.
whencondition:- Pushes only if:
- Branch name starts with
releaseormain. - OR the
PUSHparameter is set totrue.
- Branch name starts with
- Pushes only if:
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:
mainbranch → version =latest.feature/*branch → version =<feature-name>-SNAPSHOT.hotfix/*branch → version =<hotfix-name>-SNAPSHOT.release/*branch → version = the release version number.
Summary
- The pipeline builds and optionally pushes a Docker image for Ubuntu.
- Dynamic Versioning: The image tag (
version) depends on the branch name. - The pipeline allows manual control to push images via the
PUSHparameter. - Structure:
Get VersionBuild Docker ImageRun Tests(placeholder)Push Image(conditionally).
- 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:
- Navigate to Jenkins Dashboard
- Click on New Item.
- Select Multi-Branch Pipeline and provide a name.
- Source Code Management Configuration
- Under Branch Sources, select Git.
- Provide the repository URL.
- Configure credentials if needed.
- 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.
- 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:
- Make changes to the Dockerfile or Jenkinsfile.
- Jenkins will detect the new branch and automatically create a pipeline job for it.
- 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
- Branch Not Detected
- Go to the Scan Repository Log and verify that Jenkins is scanning the correct branches.
- Docker Build Fails
- Verify the Dockerfile syntax and dependencies.
- Ensure the Jenkins agent has access to the Docker daemon.
- 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! 🚀