Jenkins Pipeline: Declarative CI/CD Pipelines (2026)

Jenkins remains the dominant self-hosted CI/CD platform for enterprises that need fine-grained control over their build infrastructure. The declarative pipeline syntax — defined in a Jenkinsfile checked into your repository — gives you version-controlled, auditable build pipelines with a clean DSL. This guide covers the full declarative syntax, Docker integration, shared libraries, and when to choose Jenkins over GitHub Actions.

Declarative Jenkinsfile Syntax

A declarative Jenkinsfile has a fixed top-level structure: pipeline block containing agent, environment, options, stages, and post.

// Jenkinsfile — complete Java + Docker application pipeline
pipeline {
    agent any  // run on any available Jenkins agent

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 30, unit: 'MINUTES')
        disableConcurrentBuilds()
        timestamps()
    }

    environment {
        APP_NAME    = 'my-java-app'
        DOCKER_REPO = 'myorg/my-java-app'
        JAVA_HOME   = tool 'JDK-21'  // Jenkins tool name
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git log --oneline -5'
            }
        }

        stage('Build') {
            steps {
                sh './mvnw clean package -DskipTests'
            }
        }

        stage('Test') {
            steps {
                sh './mvnw test'
            }
            post {
                always {
                    junit 'target/surefire-reports/**/*.xml'
                    jacoco execPattern: 'target/jacoco.exec'
                }
            }
        }

        stage('Docker Build & Push') {
            when {
                anyOf {
                    branch 'main'
                    branch 'release/*'
                }
            }
            steps {
                script {
                    def imageTag = "${DOCKER_REPO}:${BUILD_NUMBER}"
                    docker.withRegistry('https://registry.hub.docker.com', 'dockerhub-creds') {
                        def image = docker.build(imageTag)
                        image.push()
                        image.push('latest')
                    }
                }
            }
        }

        stage('Deploy to Staging') {
            when { branch 'main' }
            steps {
                withCredentials([sshUserPrivateKey(credentialsId: 'staging-ssh-key',
                                                   keyFileVariable: 'SSH_KEY')]) {
                    sh '''
                        ssh -i $SSH_KEY -o StrictHostKeyChecking=no deploy@staging.example.com \
                          "docker pull ${DOCKER_REPO}:${BUILD_NUMBER} && \
                           docker stop app || true && \
                           docker run -d --name app -p 8080:8080 ${DOCKER_REPO}:${BUILD_NUMBER}"
                    '''
                }
            }
        }
    }

    post {
        success {
            slackSend channel: '#deployments',
                      color: 'good',
                      message: "Build ${BUILD_NUMBER} succeeded: ${BUILD_URL}"
        }
        failure {
            slackSend channel: '#deployments',
                      color: 'danger',
                      message: "Build ${BUILD_NUMBER} FAILED: ${BUILD_URL}"
            emailext subject: "Jenkins Build Failed: ${JOB_NAME} #${BUILD_NUMBER}",
                     body: "Check console: ${BUILD_URL}",
                     to: 'team@example.com'
        }
        always {
            cleanWs()  // clean workspace after every build
        }
    }
}

Common Steps

The most frequently used built-in pipeline steps:

// Shell command
sh 'mvn clean install'
sh '''
    echo "Multi-line"
    ./gradlew build
'''

// Capture output
def version = sh(script: './mvnw help:evaluate -Dexpression=project.version -q -DforceStdout',
                 returnStdout: true).trim()
echo "Building version: ${version}"

// Source checkout
checkout scm        // checkout the triggering branch
checkout([          // explicit checkout
    $class: 'GitSCM',
    branches: [[name: '*/main']],
    userRemoteConfigs: [[url: 'https://github.com/myorg/myrepo.git',
                         credentialsId: 'github-creds']]
])

// Archive artifacts
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true

// Stash / unstash between stages
stash includes: 'target/*.jar', name: 'app-jar'
unstash 'app-jar'

// Input (manual approval gate)
input message: 'Deploy to production?', ok: 'Deploy'

Parallel Stages for Faster CI

Parallel stages run simultaneously on separate agents or executors, cutting pipeline duration significantly for independent steps like running different test suites:

stages {
    stage('Parallel Tests') {
        parallel {
            stage('Unit Tests') {
                agent { label 'linux' }
                steps {
                    sh './mvnw test -Dtest="**/unit/**"'
                }
                post {
                    always { junit 'target/surefire-reports/unit/*.xml' }
                }
            }

            stage('Integration Tests') {
                agent { label 'linux' }
                steps {
                    sh './mvnw verify -Dtest="**/integration/**" -DfailIfNoTests=false'
                }
                post {
                    always { junit 'target/failsafe-reports/*.xml' }
                }
            }

            stage('Static Analysis') {
                agent { label 'linux' }
                steps {
                    sh './mvnw checkstyle:check pmd:check spotbugs:check'
                }
            }
        }
    }
}
Tip: Use failFast: true in the parallel block to abort all parallel branches as soon as one fails: parallel(failFast: true, 'Unit Tests': { ... }, 'Integration Tests': { ... }). This prevents wasted agent time when a fast-failing test catches a build-breaking change.

Post Block

The post block runs after the pipeline (or a stage) completes. Conditions:

  • always — runs regardless of outcome
  • success — runs only on success
  • failure — runs only on failure
  • unstable — runs when build is unstable (test failures but no compile errors)
  • changed — runs when build result differs from the previous build (failure → success or vice versa)
  • aborted — runs when build was manually aborted
post {
    always {
        junit allowEmptyResults: true, testResults: 'target/surefire-reports/**/*.xml'
        publishHTML(target: [allowMissing: false, alwaysLinkToLastBuild: true,
                             keepAll: true, reportDir: 'target/site/jacoco',
                             reportFiles: 'index.html', reportName: 'Coverage Report'])
        cleanWs()
    }
    changed {
        echo "Build result changed: ${currentBuild.currentResult}"
    }
    failure {
        archiveArtifacts artifacts: 'target/surefire-reports/**', allowEmptyArchive: true
    }
}

Credentials and Environment Variables

Never hardcode secrets in a Jenkinsfile. Use the Credentials plugin and the withCredentials step:

// Username/password credential
withCredentials([usernamePassword(credentialsId: 'nexus-creds',
                                  usernameVariable: 'NEXUS_USER',
                                  passwordVariable: 'NEXUS_PASS')]) {
    sh 'mvn deploy -s settings.xml -Dnexus.user=$NEXUS_USER -Dnexus.pass=$NEXUS_PASS'
}

// Secret text (API key, token)
withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_TOKEN')]) {
    sh './mvnw sonar:sonar -Dsonar.token=$SONAR_TOKEN'
}

// SSH private key
withCredentials([sshUserPrivateKey(credentialsId: 'deploy-key',
                                   keyFileVariable: 'KEY_FILE',
                                   usernameVariable: 'REMOTE_USER')]) {
    sh 'scp -i $KEY_FILE target/app.jar $REMOTE_USER@prod.example.com:/opt/app/'
}

// Credentials in environment block (available as env vars for entire pipeline)
environment {
    DOCKER_CREDS = credentials('dockerhub-creds')
    // Sets DOCKER_CREDS_USR and DOCKER_CREDS_PSW automatically
}

Docker Agent in Pipelines

Running build steps inside a Docker container ensures reproducible builds without installing tools on Jenkins agents:

pipeline {
    agent none  // no default agent — each stage declares its own

    stages {
        stage('Build') {
            agent {
                docker {
                    image 'maven:3.9-eclipse-temurin-21'
                    args '-v $HOME/.m2:/root/.m2'  // mount Maven cache
                    reuseNode true
                }
            }
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Test') {
            agent {
                docker {
                    image 'maven:3.9-eclipse-temurin-21'
                    args '-v $HOME/.m2:/root/.m2 --network host'
                }
            }
            steps {
                sh 'mvn test'
            }
        }

        stage('Docker Build') {
            agent { label 'docker-host' }  // agent with Docker daemon
            steps {
                script {
                    // docker.image().inside() runs steps inside the container
                    docker.image('alpine:3.19').inside {
                        sh 'echo "Running inside Alpine"'
                    }
                    // Build a new image from Dockerfile
                    def app = docker.build("myapp:${BUILD_NUMBER}")
                    app.push()
                }
            }
        }
    }
}

Shared Libraries

Shared libraries let you extract common pipeline logic into a separate Git repository and reuse it across all your Jenkinsfiles:

// vars/mavenBuild.groovy (in the shared library repo)
def call(Map config = [:]) {
    def javaVersion = config.javaVersion ?: '21'
    def goals = config.goals ?: 'clean package'

    pipeline {
        agent { docker { image "maven:3.9-eclipse-temurin-${javaVersion}" } }
        stages {
            stage('Build') {
                steps { sh "mvn ${goals}" }
            }
            stage('Test') {
                steps { sh 'mvn test' }
                post { always { junit 'target/surefire-reports/**/*.xml' } }
            }
        }
    }
}
// vars/deployToK8s.groovy
def call(String namespace, String imageTag) {
    withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
        sh """
            kubectl set image deployment/myapp \
              myapp=myorg/myapp:${imageTag} \
              -n ${namespace}
            kubectl rollout status deployment/myapp -n ${namespace} --timeout=120s
        """
    }
}
// Jenkinsfile in application repo — uses the shared library
@Library('my-shared-lib@main') _

pipeline {
    agent any
    stages {
        stage('Build and Test') {
            steps {
                mavenBuild(javaVersion: '21', goals: 'clean verify')
            }
        }
        stage('Deploy') {
            when { branch 'main' }
            steps {
                deployToK8s('production', "${BUILD_NUMBER}")
            }
        }
    }
}

Multibranch Pipeline

Multibranch Pipeline jobs automatically discover branches and pull requests in your repository and create a child pipeline job for each. Configure branch filters to avoid building every feature branch:

// Jenkinsfile branch behavior with when conditions
pipeline {
    agent any
    stages {
        stage('Build') {
            steps { sh './mvnw clean package' }
        }

        stage('Integration Tests') {
            // Only run on main and release branches (skip on short-lived feature branches)
            when {
                anyOf {
                    branch 'main'
                    branch pattern: 'release/.*', comparator: 'REGEXP'
                    changeRequest()  // also run on PRs
                }
            }
            steps { sh './mvnw verify' }
        }

        stage('Deploy') {
            when { branch 'main' }
            steps { sh './deploy.sh production' }
        }
    }
}

Jenkins vs GitHub Actions

AspectJenkinsGitHub Actions
HostingSelf-hosted (your infra)Cloud (GitHub-hosted) or self-hosted runners
CostFree (OSS) + infra costFree for public repos; minutes-based for private
ConfigurationJenkinsfile (Groovy DSL)YAML workflow files
Plugin ecosystem1,800+ plugins15,000+ marketplace actions
Matrix buildsParallel stages (manual config)Built-in matrix strategy
Secrets managementCredentials pluginGitHub Secrets (repo/org level)
Audit & complianceStrong (full control)Good (GitHub audit log)
Best forEnterprises, complex pipelines, air-gapped environmentsOpen source, GitHub-native teams, simple CI

FAQ

What is the difference between declarative and scripted pipelines?
Declarative pipelines use a structured, opinionated syntax with a pipeline { } wrapper — easier to read and validate. Scripted pipelines are free-form Groovy inside a node { } block — more flexible but harder to maintain. Use declarative for all new pipelines; scripted is only needed for advanced dynamic pipeline generation.
How do I trigger a Jenkins pipeline from a GitHub webhook?
In the Jenkins job, enable "GitHub hook trigger for GITScm polling" under Build Triggers. In GitHub, add a webhook pointing to https://your-jenkins.example.com/github-webhook/ with content type application/json and select "Push events" and "Pull request events". The Jenkins GitHub plugin handles the rest.
How can I speed up Maven builds in Jenkins?
Mount the Maven local repository from the host into the Docker container: args '-v $HOME/.m2:/root/.m2'. This caches downloaded dependencies between builds. Also use -T 1C for parallel module builds and the Incremental Build plugin to skip unchanged modules.
What is the Blue Ocean UI?
Blue Ocean is a Jenkins UI plugin that renders pipeline stages as a visual graph with clear pass/fail indicators per stage. It is especially useful for parallel stages, showing the timeline of each branch. Install via Manage Jenkins → Plugins → Blue Ocean.
Can I run Jenkins pipelines on Kubernetes?
Yes. The Kubernetes plugin for Jenkins (kubernetes-plugin) spins up a fresh pod for each build and tears it down when the build completes. Configure a pod template with the containers your build needs (e.g., maven + docker-in-docker). This provides excellent isolation and scales horizontally with your cluster.