Skip to content
DevOps devops cicd 6 min read

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.

ApproachHow you define itVersion controlWhen to use
Freestyle job (clicking in UI)Web form on the Jenkins serverNo history, lives only on the serverTiny one-off jobs, quick experiments
Scripted pipelineA Jenkinsfile written in Groovy codeYes, in GitComplex logic, loops, advanced control flow
Declarative pipelineA Jenkinsfile with a fixed, readable structureYes, in GitAlmost 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.
  • agentwhere 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:

  • sh runs a shell command on the Ubuntu agent. The ''' triple quotes let you write a multi-line shell script.
  • npm ci is a clean install from package-lock.json. It is faster and more reliable than npm install in automated builds.
  • checkout scm pulls 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 on main.

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 $SECRET or 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 Jenkinsfile in 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 archiveArtifacts so you can download and inspect exactly what was built.
Last updated June 15, 2026
Was this helpful?