What Is a Jenkins Pipeline?
Jenkins Pipeline lets you define the build, test, and deploy process as code (a Jenkinsfile). Unlike Freestyle jobs that you assemble by clicking in the web UI, you write the pipeline as code and keep it in Git with the rest of the project, so you get version control, code review, and reproducibility. This is called "Pipeline as Code."
There are two ways to write a pipeline.
| Style | Characteristics |
|---|---|
| Declarative | Structured syntax that starts with a pipeline { } block. More readable and easier to validate—this is the recommended approach |
| Scripted | Groovy-based. More flexible, but more complex and a steeper learning curve |
This guide covers Declarative Pipeline, the production standard.
Prerequisites
Jenkins should already be installed, and you need the following plugins.
- Pipeline (included by default)
- Git / GitHub Branch Source
- Credentials Binding (for injecting credentials)
When you create a new job, choose the "Pipeline" or "Multibranch Pipeline" type and set the definition to Pipeline script from SCM. Jenkins will then read the Jenkinsfile at the repository root.
The Most Basic Jenkinsfile
pipeline {
agent any
stages {
stage('Build') {
steps {
echo ' when'
sh 'make build'
}
}
stage('Test') {
steps {
sh 'make test'
}
}
stage('Deploy') {
steps {
sh 'make deploy'
}
}
}
}The core structure looks like this.
| Block | Role |
|---|---|
pipeline | Top-level block that wraps the entire pipeline (required) |
agent | Specifies which node/environment to run on |
stages | Container that holds one or more stages |
stage | A logical step (Build, Test, Deploy, etc.). Visualized in the UI |
steps | The commands that actually run inside a stage |
agent — Specifying the Execution Environment
agent decides where the pipeline (or an individual stage) will run.
// 1)
agent any
// 2)
agent { label 'linux && docker' }
// 3) Docker run
agent {
docker {
image 'node:20-alpine'
args '-v /tmp:/tmp'
}
}
// 4) none, stage set
agent noneUsing agent { docker { ... } } runs each build in a clean container, which reduces "it worked on my machine" problems. The agent node must have Docker installed.
environment — Environment Variables
Declare variables in an environment block. You can scope them to the whole pipeline or to a specific stage.
pipeline {
agent any
environment {
APP_NAME = 'my-service'
BUILD_TAG = "v1.${BUILD_NUMBER}" //
REGISTRY = 'registry.example.com'
}
stages {
stage('Info') {
steps {
sh 'echo ": $APP_NAME:$BUILD_TAG"'
}
}
}
}Built-in environment variables you will use often:
| Variable | Meaning |
|---|---|
BUILD_NUMBER | Sequential build number |
BUILD_ID | Build ID |
JOB_NAME | Job name |
WORKSPACE | Absolute path of the workspace |
GIT_COMMIT | Hash of the checked-out commit |
BRANCH_NAME | (Multibranch) Branch name |
Using Credentials Safely
Never put passwords or tokens in a Jenkinsfile in plaintext. Register them under Manage Jenkins → Credentials and reference them by ID.
environment {
// Secret Text
DOCKER_TOKEN = credentials('docker-registry-token')
}For Username/Password credentials, wrapping them explicitly with withCredentials is the safer approach.
steps {
withCredentials([usernamePassword(
credentialsId: 'docker-login',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS')]) {
sh 'echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin $REGISTRY'
}
}Even if credentials show up in logs, Jenkins automatically masks them as ****. Still, never write code that prints them directly, such as echo "$DOCKER_PASS".
Parallel Execution — parallel
Independent work (for example, a test matrix across OSes or versions) can run at the same time with parallel to cut wall-clock time.
stage('Test Matrix') {
parallel {
stage('Unit') {
steps { sh 'make test-unit' }
}
stage('Integration') {
steps { sh 'make test-integration' }
}
stage('Lint') {
steps { sh 'make lint' }
}
}
}Conditional Execution — when
Run a stage only when a condition is met. Deploys usually run only on the main branch.
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'make deploy'
}
}when supports many conditions, including branch, environment name: 'X', value: 'Y', expression { ... }, and changeset.
post — After the Build
Run cleanup or send notifications regardless of whether the build succeeded or failed.
post {
always {
junit 'reports/**/*.xml' //
cleanWs //
}
success {
echo '✅ '
}
failure {
mail to: 'team@example.com',
subject: "failed: ${JOB_NAME} #${BUILD_NUMBER}",
body: " check: ${BUILD_URL}"
}
}| Condition | When it runs |
|---|---|
always | Always, regardless of the result |
success | Only on success |
failure | Only on failure |
unstable | When the build is unstable (e.g. test failures) |
changed | When the result differs from the previous build |
options & triggers
options {
timeout(time: 30, unit: 'MINUTES') // 30 when
retry(2) // failed when 2 when
disableConcurrentBuilds // when
timestamps //
}
triggers {
cron('H 2 * * *') // 2when
pollSCM('H/15 * * * *') // 15
}The H in cron('H 2 * * *') is a hash. Jenkins automatically assigns a spread-out time per job so they do not all fire at once. Prefer this over a fixed minute (0) in a regular cron expression.
Complete Example — Building a Node App and Deploying with Docker
pipeline {
agent { label 'docker' }
environment {
REGISTRY = 'registry.example.com'
IMAGE = "${REGISTRY}/web-app"
TAG = "${GIT_COMMIT.take(8)}"
}
options {
timeout(time: 20, unit: 'MINUTES')
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Install & Test') {
agent { docker { image 'node:20-alpine'; reuseNode true } }
steps {
sh 'npm ci'
sh 'npm test'
}
}
stage('Build Image') {
steps {
sh "docker build -t ${IMAGE}:${TAG} ."
}
}
stage('Push') {
when { branch 'main' }
steps {
withCredentials([usernamePassword(
credentialsId: 'docker-login',
usernameVariable: 'U', passwordVariable: 'P')]) {
sh 'echo "$P" | docker login -u "$U" --password-stdin $REGISTRY'
sh "docker push ${IMAGE}:${TAG}"
}
}
}
}
post {
always { cleanWs() }
failure { echo ' failed — check ' }
}
}Shared Libraries
If several projects share the same build logic, put a shared library in a separate Git repository and reuse it.
@Library('my-shared-lib') _
pipeline {
agent any
stages {
stage('Build') {
steps {
buildApp(lang: 'node') // vars/buildApp.groovy call
}
}
}
}The library repository is organized as vars/ (global functions), src/ (Groovy classes), and resources/ (static files). Register it under Manage Jenkins → System → Global Pipeline Libraries.
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
Permission error in an sh step | Grant execute permission to the agent user; add the user to the Docker group |
| Pipeline appears stuck | It may be waiting on an input approval. Wrap it with timeout |
| Credentials come through empty | Check for a typo in credentialsId or a folder-scope mismatch |
| Workspace missing on a Docker agent | Add the reuseNode true option |
Catch syntax errors before you waste a build by using Pipeline Syntax → Declarative Directive Generator or jenkins-cli declarative-linter.
Summary
| Topic | Key takeaway |
|---|---|
| Definition style | Prefer Declarative Pipeline (pipeline { }) |
| Required structure | agent → stages → stage → steps |
| Environment variables | environment { }, built-ins BUILD_NUMBER/GIT_COMMIT |
| Secrets | Inject safely with credentials() and withCredentials |
| Speed | Run independent work concurrently with parallel |
| Conditionals | when { branch 'main' } |
| Post-processing | post { always / success / failure } |
| Reuse | Share build logic with a Shared Library |
Declarative Pipeline is the starting point for defining CI/CD as code so you get reproducibility and easier collaboration. Start from the complete example above and adjust the stages to fit your project.
AI tools may assist drafting. Check the evidence in each guide for whether commands were run and which environment was used. Confirm the current docs for your version before applying instructions, and email us about errors. email us
Questions & answers
Ask a question about this guide. We'll review and reply.