Jenkins Declarative Pipelines
Jenkins is a server that automates the boring, repetitive parts of shipping software: building your code, running your tests, and deploying it. A pipeline is the recipe Jenkins follows, written as a series of steps. With declarative pipelines you write that recipe in a single text file called a Jenkinsfile and store it right next to your code in Git. This is called pipeline-as-code, and it means your build process is version-controlled, reviewable, and reproducible just like the rest of your project.
This page assumes you already have Jenkins installed and running on an Ubuntu server (see Jenkins introduction). Here we focus on the Jenkinsfile itself.
Why pipeline-as-code
In the early days, people configured Jenkins jobs by clicking buttons in a web form. That worked, but it had real problems: nobody could review the changes, there was no history of who changed what, and if the Jenkins server died, your job configuration died with it.
Pipeline-as-code fixes all of that. The whole build process lives in one file in your repository.
| Approach | How you define it | Version control | When to use |
|---|---|---|---|
| Freestyle job (clicking in UI) | Web form on the Jenkins server | No history, lives only on the server | Tiny one-off jobs, quick experiments |
| Scripted pipeline | A Jenkinsfile written in Groovy code | Yes, in Git | Complex logic, loops, advanced control flow |
| Declarative pipeline | A Jenkinsfile with a fixed, readable structure | Yes, in Git | Almost everything — start here |
Use declarative pipelines unless you have a strong reason not to. They are easier to read, they catch syntax errors early, and they cover the vast majority of real-world needs. Reach for scripted pipelines only when you truly need custom programming logic.
The shape of a declarative pipeline
Every declarative pipeline has the same skeleton. Learn these five blocks and you understand the whole thing:
pipeline— the outer wrapper. Everything goes inside it.agent— where the work runs (which machine or container Jenkins uses).stages— a group of named phases like “Build”, “Test”, “Deploy”.steps— the actual commands inside each stage.post— actions that run after the stages finish, based on success or failure (great for sending notifications).
A real Jenkinsfile
Here is a complete, working Jenkinsfile for a Node.js web app. It checks out the code, installs dependencies, runs tests, builds the app, and deploys it. Save this as a file named exactly Jenkinsfile (no extension) in the root of your repository.
pipeline {
// 'any' means: run on any available Jenkins machine (agent).
agent any
// Tools Jenkins should make available. These must be configured
// under "Manage Jenkins > Tools" first.
tools {
nodejs 'node-20'
}
// Variables available to every stage below.
environment {
APP_NAME = 'devcraftly-web'
DEPLOY_DIR = '/var/www/devcraftly'
}
// Run the whole pipeline every night at 2am, plus on every push.
triggers {
cron('H 2 * * *')
}
stages {
stage('Checkout') {
steps {
// Pull the latest code from the configured Git repo.
checkout scm
}
}
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Test') {
steps {
sh 'npm test'
}
}
stage('Build') {
steps {
sh 'npm run build'
}
}
stage('Deploy') {
// Only deploy when we are on the main branch.
when {
branch 'main'
}
steps {
// Bind a stored SSH key so we never write secrets in the file.
withCredentials([sshUserPrivateKey(
credentialsId: 'deploy-ssh-key',
keyFileVariable: 'SSH_KEY'
)]) {
sh '''
rsync -avz --delete -e "ssh -i $SSH_KEY -o StrictHostKeyChecking=no" \
dist/ [email protected]:${DEPLOY_DIR}/
'''
}
}
}
}
// Runs after all stages, no matter the result.
post {
success {
echo "${APP_NAME} pipeline succeeded."
}
failure {
echo "${APP_NAME} pipeline FAILED — check the console log."
}
always {
// Save the build output so you can download it later.
archiveArtifacts artifacts: 'dist/**', allowEmptyArchive: true
}
}
}
A few terms explained:
shruns a shell command on the Ubuntu agent. The'''triple quotes let you write a multi-line shell script.npm ciis a clean install frompackage-lock.json. It is faster and more reliable thannpm installin automated builds.checkout scmpulls the same repository and branch that triggered the build. SCM means Source Code Management (Git, in practice).when { branch 'main' }is a guard: the Deploy stage is skipped on feature branches and only runs onmain.
Binding credentials safely
Never paste passwords, API keys, or SSH keys directly into a Jenkinsfile — it lives in Git where everyone can read it. Instead, store secrets in Jenkins and reference them by an ID.
First add the credential in the UI: Manage Jenkins > Credentials > System > Global credentials > Add Credentials. For a deploy key, choose kind “SSH Username with private key” and give it the ID deploy-ssh-key.
Then bind it inside a stage with withCredentials, exactly as shown in the Deploy stage above. Jenkins injects the secret into a temporary environment variable ($SSH_KEY) that only exists for the duration of that block, and it automatically masks the value in the build log.
Jenkins masks credential values in logs, but it cannot mask a secret you print on purpose. Never run
echo $SECRETor pass a secret as a visible command-line argument that gets logged. Treat every secret as if the whole team can see the console output.
For a simple username/password (for example a Docker registry login), use the usernamePassword binding:
steps {
withCredentials([usernamePassword(
credentialsId: 'dockerhub',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh 'echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin'
}
}
Storing and running the pipeline
Commit the Jenkinsfile to your repo so it is versioned with the code:
git add Jenkinsfile
git commit -m "Add Jenkins declarative pipeline"
git push origin main
Now tell Jenkins to use it. In the Jenkins UI, create a new item of type “Multibranch Pipeline” (or “Pipeline” for a single branch), point it at your Git repository URL, and set the script path to Jenkinsfile. Jenkins scans the repo, finds the file, and builds automatically on every push.
When a build runs you will see each stage in the Stage View, and clicking a build shows its console output:
Output:
[Pipeline] stage (Test)
[Pipeline] sh
+ npm test
> [email protected] test
> jest --ci
PASS src/utils.test.js
Tests: 18 passed, 18 total
[Pipeline] stage (Deploy)
Skipping stage Deploy (when branch != main)
Finished: SUCCESS
Best practices
- Keep the
Jenkinsfilein the repository root and treat changes to it like any other code: review them in a pull request. - Use
npm ci(or the equivalent lockfile-based install for your language) so builds are reproducible. - Put secrets in Jenkins Credentials and bind them with
withCredentials— never hard-code them. - Guard your Deploy stage with
when { branch 'main' }so feature branches cannot accidentally deploy to production. - Use the
post { failure { ... } }block to send Slack or email alerts so a broken build never goes unnoticed. - Keep each stage focused on one job (build, test, deploy) — small stages make failures easy to read in the Stage View.
- Archive build artifacts with
archiveArtifactsso you can download and inspect exactly what was built.