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'
}
}
}
}
}
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 outcomesuccess— runs only on successfailure— runs only on failureunstable— 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
| Aspect | Jenkins | GitHub Actions |
|---|---|---|
| Hosting | Self-hosted (your infra) | Cloud (GitHub-hosted) or self-hosted runners |
| Cost | Free (OSS) + infra cost | Free for public repos; minutes-based for private |
| Configuration | Jenkinsfile (Groovy DSL) | YAML workflow files |
| Plugin ecosystem | 1,800+ plugins | 15,000+ marketplace actions |
| Matrix builds | Parallel stages (manual config) | Built-in matrix strategy |
| Secrets management | Credentials plugin | GitHub Secrets (repo/org level) |
| Audit & compliance | Strong (full control) | Good (GitHub audit log) |
| Best for | Enterprises, complex pipelines, air-gapped environments | Open 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 anode { }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 typeapplication/jsonand 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 1Cfor 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.