Jenkins Declarative Pipelines: From Zero to Production CI/CD
Why Declarative Over Scripted
Jenkins supports two pipeline syntaxes. Scripted pipelines are pure Groovy — flexible but hard to read and validate. Declarative pipelines use a structured DSL that Jenkins can lint, visualize, and restart from specific stages. Unless you need advanced Groovy logic, declarative is the right default.
The fundamental structure:
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make build'
}
}
stage('Test') {
steps {
sh 'make test'
}
}
stage('Deploy') {
steps {
sh 'make deploy'
}
}
}
}
Every declarative pipeline needs pipeline {}, at least one agent, and a stages {} block.
Agent Directives
The agent tells Jenkins where to run the pipeline. Options:
// Run on any available node
agent any
// Run in a Docker container
agent {
docker {
image 'node:20-alpine'
args '-v /tmp:/tmp'
}
}
// Run on a node with a specific label
agent {
label 'linux && docker'
}
// No global agent — define per stage
agent none
Using agent none at the top level with per-stage agents is common when different stages need different environments:
pipeline {
agent none
stages {
stage('Build') {
agent { docker { image 'maven:3.9-eclipse-temurin-17' } }
steps {
sh 'mvn package -DskipTests'
}
}
stage('Test') {
agent { docker { image 'maven:3.9-eclipse-temurin-17' } }
steps {
sh 'mvn test'
}
}
stage('Deploy') {
agent { label 'production' }
steps {
sh './deploy.sh'
}
}
}
}
Environment Variables and Credentials
Jenkins exposes many built-in variables (BUILD_NUMBER, BRANCH_NAME, GIT_COMMIT, etc.). Define your own with environment {}:
pipeline {
agent any
environment {
APP_NAME = 'my-service'
REGISTRY = 'registry.example.com'
IMAGE_TAG = "${APP_NAME}:${BUILD_NUMBER}"
}
stages {
stage('Build Image') {
steps {
sh "docker build -t ${IMAGE_TAG} ."
sh "docker push ${IMAGE_TAG}"
}
}
}
}
For secrets, never hardcode — use credentials binding:
environment {
// String credential — injects as env var
API_KEY = credentials('my-api-key')
// Username/password — injects as APP_CREDS_USR and APP_CREDS_PSW
APP_CREDS = credentials('docker-hub-credentials')
}
Or bind inline inside a step:
steps {
withCredentials([
string(credentialsId: 'deploy-token', variable: 'DEPLOY_TOKEN'),
usernamePassword(
credentialsId: 'db-credentials',
usernameVariable: 'DB_USER',
passwordVariable: 'DB_PASS'
)
]) {
sh './deploy.sh'
}
}
Parallel Stages
Run independent stages simultaneously to cut build times:
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
}
stage('Integration Tests') {
steps {
sh 'npm run test:integration'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
}
}
Each parallel branch runs on its own executor. If your agents are containerized, this is nearly free. If they share a node, watch for resource contention.
When Conditions
Skip stages based on conditions:
stage('Deploy to Production') {
when {
branch 'main'
not { changeRequest() }
}
steps {
sh './deploy-prod.sh'
}
}
stage('Deploy to Staging') {
when {
anyOf {
branch 'develop'
branch 'staging'
}
}
steps {
sh './deploy-staging.sh'
}
}
stage('Security Scan') {
when {
expression { return env.RUN_SECURITY_SCAN == 'true' }
}
steps {
sh 'trivy image ${IMAGE_TAG}'
}
}
Common when conditions:
branch 'main'— only on this branchchangeRequest()— only on pull requeststag 'v*'— only on matching tagsenvironment name: 'DEPLOY_ENV', value: 'prod'— env var checkexpression { <groovy> }— arbitrary logic
Post Conditions
Run steps after the pipeline finishes, regardless of outcome:
post {
always {
// Always runs — cleanup, notifications
junit 'target/surefire-reports/**/*.xml'
cleanWs()
}
success {
slackSend(color: 'good', message: "Build ${BUILD_NUMBER} succeeded")
}
failure {
slackSend(color: 'danger', message: "Build ${BUILD_NUMBER} FAILED")
emailext(
subject: "Build Failed: ${JOB_NAME} #${BUILD_NUMBER}",
body: '${BUILD_LOG, maxLines=50}',
to: '[email protected]'
)
}
unstable {
slackSend(color: 'warning', message: "Build ${BUILD_NUMBER} is unstable")
}
}
post can appear at the pipeline level (runs after all stages) or inside a specific stage (runs after that stage).
Input Step: Manual Gates
Add a manual approval gate before production deployments:
stage('Approve Production Deploy') {
when { branch 'main' }
steps {
input(
message: 'Deploy to production?',
ok: 'Deploy',
submitter: 'devops-leads,release-manager',
parameters: [
string(name: 'RELEASE_NOTES', description: 'Notes for this release')
]
)
}
}
The build pauses and waits for a human to click "Deploy" in the Jenkins UI. submitter restricts who can approve. Set a timeout to avoid hanging forever:
timeout(time: 1, unit: 'HOURS') {
input message: 'Deploy to production?'
}
Options Block
Configure pipeline-wide behavior:
pipeline {
options {
timeout(time: 30, unit: 'MINUTES') // Fail if pipeline runs > 30 min
buildDiscarder(logRotator(numToKeepStr: '10')) // Keep last 10 builds
disableConcurrentBuilds() // Don't run multiple builds in parallel
retry(2) // Retry the entire pipeline up to 2 times
timestamps() // Prefix all output with timestamps
skipStagesAfterUnstable() // Don't run later stages if unstable
}
// ...
}
Shared Libraries
For large organizations with many pipelines, shared libraries extract common logic into a reusable package. Structure:
jenkins-shared-library/
vars/
buildAndPush.groovy # Global variable / custom step
deployToK8s.groovy
src/
org/example/
Docker.groovy # Groovy class
Define a global variable (custom step):
// vars/buildAndPush.groovy
def call(String imageName, String tag) {
sh "docker build -t ${imageName}:${tag} ."
sh "docker push ${imageName}:${tag}"
}
Use it in a Jenkinsfile after loading the library:
@Library('jenkins-shared-library@main') _
pipeline {
agent any
stages {
stage('Build') {
steps {
buildAndPush('my-service', env.BUILD_NUMBER)
}
}
}
}
Register the library in Jenkins: Manage Jenkins → System → Global Pipeline Libraries.
Complete Production Jenkinsfile
A realistic pipeline for a containerized Node.js service:
@Library('shared-lib@main') _
pipeline {
agent none
options {
timeout(time: 20, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
environment {
REGISTRY = 'registry.example.com'
APP = 'api-service'
IMAGE = "${REGISTRY}/${APP}"
REGISTRY_CREDS = credentials('registry-credentials')
}
stages {
stage('Checkout') {
agent any
steps {
checkout scm
script {
env.GIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
env.IMAGE_TAG = "${IMAGE}:${GIT_SHORT}"
}
}
}
stage('Test') {
agent { docker { image 'node:20-alpine' } }
parallel {
stage('Unit') {
steps { sh 'npm ci && npm test' }
}
stage('Lint') {
steps { sh 'npm ci && npm run lint' }
}
}
}
stage('Build & Push') {
agent any
steps {
sh "docker login -u ${REGISTRY_CREDS_USR} -p ${REGISTRY_CREDS_PSW} ${REGISTRY}"
sh "docker build -t ${IMAGE_TAG} ."
sh "docker push ${IMAGE_TAG}"
}
}
stage('Deploy Staging') {
agent { label 'staging' }
steps {
sh "kubectl set image deployment/${APP} ${APP}=${IMAGE_TAG} -n staging"
sh "kubectl rollout status deployment/${APP} -n staging --timeout=5m"
}
}
stage('Deploy Production') {
when { branch 'main' }
agent { label 'production' }
steps {
timeout(time: 1, unit: 'HOURS') {
input(message: 'Deploy to production?', submitter: 'devops-leads')
}
sh "kubectl set image deployment/${APP} ${APP}=${IMAGE_TAG} -n production"
sh "kubectl rollout status deployment/${APP} -n production --timeout=10m"
}
}
}
post {
success { slackSend(color: 'good', message: "✅ ${APP} ${GIT_SHORT} deployed") }
failure { slackSend(color: 'danger', message: "❌ ${APP} build failed: ${BUILD_URL}") }
always { cleanWs() }
}
}
Troubleshooting Common Issues
"No such DSL method" — You used a scripted pipeline command inside a declarative block. Wrap it in script {}.
Credentials not masked in logs — Ensure you're using credentials() binding, not manually echoing secret values.
Build hangs at input — Set a timeout() wrapper around input steps.
Docker socket not available — Mount it explicitly: args '-v /var/run/docker.sock:/var/run/docker.sock'.
SCM checkout fails on agents — Ensure the agent node has Git installed and SSH keys configured for your repository.
Was this article helpful?
CI/CD Engineering Lead
Automation evangelist who believes no deployment should require a human. I write pipelines, break pipelines, and write about both. Code-first, always.
Related Articles
Jenkins Installation & Configuration: From Zero to First Pipeline
Install Jenkins on Ubuntu and Docker, configure security settings, manage plugins, and create your first freestyle and pipeline jobs step by step.
Jenkins Kubernetes Agents: Dynamic Build Pods for Scalable CI/CD
Configure Jenkins to dynamically spin up Kubernetes pods as build agents. Covers the Kubernetes plugin, pod templates, JNLP agents, multi-container pods, resource limits, and RBAC — so you never manage static build nodes again.
Jenkins Declarative Pipelines: The Complete Jenkinsfile Guide
Master Jenkins declarative pipelines — stages, steps, post actions, environment variables, credentials, parallel execution, and when conditions.
Jenkins Shared Libraries: Reusable Pipeline Code at Scale
Build and maintain Jenkins shared libraries — directory structure, global vars, custom steps, class-based libraries, testing, and versioning strategies.
Azure DevOps Pipelines: YAML CI/CD from Build to Production
How to build production Azure DevOps YAML pipelines — multi-stage CI/CD, environments, approvals, variable groups, templates, container jobs, and deployment strategies for Kubernetes and App Service.
The Complete Guide to GitHub Actions CI/CD: From Zero to Production-Ready Pipelines
Build production-grade GitHub Actions CI/CD pipelines — from first workflow to reusable workflows, matrix builds, and deployment gates.
More in Jenkins
View all →Fix Jenkins Agent Disconnecting Randomly
Diagnose and fix Jenkins agents that go offline mid-build — resolve network timeouts, JVM heap issues, and SSH connection drops.
Fix Jenkins Pipeline 'Scripts Not Permitted to Use' / 'Script Not Yet Approved'
Resolve Jenkins Groovy sandbox errors by approving script signatures, configuring the script security plugin, and using safe alternatives.
Docker Agents in Jenkins: Reproducible Builds Every Time
Run Jenkins pipeline stages inside Docker containers for clean, reproducible builds — agent configuration, custom images, Docker-in-Docker, and Kaniko alternatives.