Pipeline Perfection: Mastering Jenkins Pipelines with Docker

Jenkins is the Swiss army knife of CI/CD — open-source, extensible, and trusted by developers worldwide. Whether managing a microservices fleet or deploying a monolith, Jenkins Pipelines provide a robust, flexible way to automate builds, tests, and deployments. This blog will walk you through building Jenkins Pipelines from scratch and show how to supercharge your pipelines using Docker.


What Is a Jenkins Pipeline?

A Jenkins Pipeline is a user-defined model for a continuous delivery pipeline. It's written in a domain-specific language (DSL) based on Groovy and is stored as code — often inside your repository as a Jenkinsfile.

Why Jenkins Pipelines?

  • As Code: Maintain, review, and version pipelines like application code.
  • Complex Workflow Support: Parallel steps, conditional logic, and stages.
  • Plugin Ecosystem: Extend pipeline functionality with 1,800+ plugins.
  • Scalable Execution: Run builds across distributed Jenkins agents.

Jenkins + Docker: The DevOps Power Couple

Docker and Jenkins together streamline DevOps pipelines — isolating builds, managing environments, and simplifying deployments.

Benefits of Dockerized Jenkins Pipelines:

  • Clean builds every time with ephemeral containers
  • Environment parity between dev/staging/prod
  • Simplified deployment via Docker Compose and Docker Registry
  • Secure secrets management via Jenkins credentials

Setting Up Jenkins with Docker

Let's set up Jenkins using Docker for local or on-prem environments.

Jenkins Setup

To set up Jenkins, create two files in the root directory of your server:

  • jenkins-setup.yml
  • Dockerfile

Copy the jenkins-setup.yml and Dockerfile content into each respective file.

📝 Note: Make sure both files are placed directly in the root directory of your project—not inside any subfolders.

This setup ensures Jenkins can be easily built and run using Docker, making it simple to integrate with your CI/CD pipeline.

jenkins-setup.yml - Docker Compose File

version: '3.8'

services:
  docker:
    image: docker:dind
    container_name: jenkins-docker
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: /certs
    networks:
      jenkins:
        aliases:
          - docker
    volumes:
      - jenkins-docker-certs:/certs/client
      - jenkins-data:/var/jenkins_home
    ports:
      - "2376:2376"

  jenkins:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: jenkins
    restart: on-failure
    networks:
      - jenkins
    environment:
      TZ: Asia/Kolkata
      DOCKER_HOST: tcp://docker:2376
      DOCKER_CERT_PATH: /certs/client
      DOCKER_TLS_VERIFY: '1'
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - jenkins-data:/var/jenkins_home
      - jenkins-docker-certs:/certs/client:ro
      - /etc/localtime:/etc/localtime:ro   # Optional but ensures accurate system time sync

volumes:
  jenkins-docker-certs:
  jenkins-data:

networks:
  jenkins:

Dockerfile for Jenkins with Docker CLI Support

FROM jenkins/jenkins:2.492.3-jdk17

USER root

RUN apt-get update && apt-get install -y \
    lsb-release \
    ca-certificates \
    curl && \
    install -m 0755 -d /etc/apt/keyrings && \
    curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && \
    chmod a+r /etc/apt/keyrings/docker.asc && \
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
    https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" \
    | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
    apt-get update && apt-get install -y docker-ce-cli && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

USER jenkins

RUN jenkins-plugin-cli --plugins "blueocean docker-workflow"

Run Jenkins

docker compose -f jenkins-setup.yml up -d

Access Jenkins at http://localhost:8080 (or your server IP).


Your First Jenkins Pipeline

Option 1: Pipeline Script (Inline)

Best for quick and simple automation jobs.

pipeline {
  agent any
  stages {
    stage('Build') {
      steps { echo 'Building...' }
    }
    stage('Test') {
      steps { echo 'Testing...' }
    }
    stage('Deploy') {
      steps { echo 'Deploying...' }
    }
  }
}

Option 2: Pipeline Script from SCM

Best for projects under version control — especially for teams.

pipeline {
  agent any
  stages {
    stage('Checkout') {
      steps { checkout scm }
    }
    stage('Build') {
      steps { echo 'Build from SCM pipeline' }
    }
  }
}

Real-World Example: Dockerized Jenkins CI/CD Pipeline

Let’s look at a production-ready Jenkinsfile used for building and deploying a Dockerized Node.js API:

Highlights:

  • GitHub Webhooks
  • Conditional Build Trigger
  • Secure Credential Handling
  • Docker Build & Push
  • Remote SSH Deployment
pipeline {
    agent any
    environment {
        REGISTRY_URL = <registry-url>
        IMAGE_NAME = <image-name>
    }
    stages {
        stage('Checkout Code') {
            steps {
                checkout scm
            }
        }
        
        stage('Build & Push Docker Image') {
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
                        sh """
                            echo "${DOCKER_PASS}" | docker login ${REGISTRY_URL} -u "${DOCKER_USER}" --password-stdin
                            docker build -t ${REGISTRY_URL}/${IMAGE_NAME}:latest .
                            docker push ${REGISTRY_URL}/${IMAGE_NAME}:latest
                            docker logout ${REGISTRY_URL}
                        """
                    }
                }
            }
        }
        stage('Deploy') {
            steps {
                sshagent(['jenkins-ssh-key']) {
                    sh """
                        ssh -o StrictHostKeyChecking=no <vm-username>@<vm-ip> '
                        cd /home/ubuntu/docker-compose/apps &&
                        docker compose -f ${IMAGE_NAME}.yml pull &&
                        docker compose -f ${IMAGE_NAME}.yml down &&
                        docker compose -f ${IMAGE_NAME}.yml up -d
                        '
                    """
                }
            }
        }
    }
	post {
        always {
            script {
                // Set up date format and time zone (Asia/Kolkata = IST)
                def now = new Date()
                def formatter = new java.text.SimpleDateFormat('EEE, d MMM yyyy hh:mm:ss a z')
                formatter.setTimeZone(TimeZone.getTimeZone('<your-preferred-timezone>'))
                def formattedDate = formatter.format(now)

                // Send build status email
                emailext(
                    to: 'devteam@example.com',
                    subject: "📣 Jenkins Build: ${currentBuild.fullDisplayName} - ${currentBuild.currentResult}",
                    body: """
                        <p>Build Status - <span style="font-weight: bold; color: ${currentBuild.currentResult == 'SUCCESS' ? 'green' : 'red'};">${currentBuild.currentResult}</span></p>
                        <p>Project - <span style="font-weight: bold; color: #ff9900;">${env.JOB_NAME}</span></p>
                        <p>Build Number - <span style="font-weight: bold; color: #ffad33;">#${env.BUILD_NUMBER}</span></p>
                        <p>Build Details - <a href="${env.BUILD_URL}" style="font-weight: bold; color: #668cff;">Click here</a></p>
                        <p>Timestamp - <span style="font-weight: bold; color: #00e68a;">${formattedDate}</span></p>
                    """,
                    mimeType: 'text/html'
                )
            }
        }
    }
}

📌 Tip: Use withCredentials and sshagent blocks to manage secrets and SSH keys securely via Jenkins Credentials.

Jenkins for Next.js Projects (Frontend)

Front-end frameworks like Next.js need a .env at build time. Here’s how to inject it securely via Jenkins:

pipeline {
    agent any
    environment {
        REGISTRY_URL = <registry-url>
        IMAGE_NAME = <image-name>
    }
    stages {
        stage('Checkout Code') {
            steps {
                checkout scm
            }
        }

		stage('Inject .env') {
            steps {
                withCredentials([file(credentialsId: '<env-name-saved-in-jenkins-credentials>', variable: 'ENV_FILE')]) {
                    sh '''
                    echo "Fixing permissions and copying .env file..."
                    chmod -R 777 .
                    cp $ENV_FILE .env
                    '''
                }
            }
        }
        
        stage('Build & Push Docker Image') {
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
                        sh """
                            echo "${DOCKER_PASS}" | docker login ${REGISTRY_URL} -u "${DOCKER_USER}" --password-stdin
                            docker build -t ${REGISTRY_URL}/${IMAGE_NAME}:latest .
                            docker push ${REGISTRY_URL}/${IMAGE_NAME}:latest
                            docker logout ${REGISTRY_URL}
                        """
                    }
                }
            }
        }
        stage('Deploy') {
            steps {
                sshagent(['jenkins-ssh-key']) {
                    sh """
                        ssh -o StrictHostKeyChecking=no <vm-username>@<vm-ip> '
                        cd /home/ubuntu/docker-compose/apps &&
                        docker compose -f ${IMAGE_NAME}.yml pull &&
                        docker compose -f ${IMAGE_NAME}.yml down &&
                        docker compose -f ${IMAGE_NAME}.yml up -d
                        '
                    """
                }
            }
        }
    }
	post {
        always {
            script {
                // Set up date format and time zone (Asia/Kolkata = IST)
                def now = new Date()
                def formatter = new java.text.SimpleDateFormat('EEE, d MMM yyyy hh:mm:ss a z')
                formatter.setTimeZone(TimeZone.getTimeZone('<your-preferred-timezone>'))
                def formattedDate = formatter.format(now)

                // Send build status email
                emailext(
                    to: 'devteam@example.com',
                    subject: "📣 Jenkins Build: ${currentBuild.fullDisplayName} - ${currentBuild.currentResult}",
                    body: """
                        <p>Build Status - <span style="font-weight: bold; color: ${currentBuild.currentResult == 'SUCCESS' ? 'green' : 'red'};">${currentBuild.currentResult}</span></p>
                        <p>Project - <span style="font-weight: bold; color: #ff9900;">${env.JOB_NAME}</span></p>
                        <p>Build Number - <span style="font-weight: bold; color: #ffad33;">#${env.BUILD_NUMBER}</span></p>
                        <p>Build Details - <a href="${env.BUILD_URL}" style="font-weight: bold; color: #668cff;">Click here</a></p>
                        <p>Timestamp - <span style="font-weight: bold; color: #00e68a;">${formattedDate}</span></p>
                    """,
                    mimeType: 'text/html'
                )
            }
        }
    }
}


Pro Tip: Secure Credentials Management

Store sensitive data like API tokens, Docker credentials, and .env files in:

Dashboard → Manage Jenkins → Credentials

Use:

  • Username with Password for Docker or GitHub
  • Secret file for .env files
  • SSH Username with private key for SSH deploys

Final Thoughts

Jenkins Pipelines bring clarity, consistency, and control to your CI/CD workflows — and when combined with Docker, they’re a force multiplier. Whether you’re automating builds or deploying microservices to production, Jenkins gives you the tools to build confidently and deploy efficiently.