Skip to main content

πŸ› οΈ Jenkins Pipeline Fundamentals

GitHub Actions lives inside GitHub. Jenkins lives wherever you put it. As the self-hosted, plugin-rich veteran of CI/CD, Jenkins gives you total control over your build infrastructure β€” at the cost of running it yourself. This lesson teaches you to define pipelines as code in a Jenkinsfile, from a three-stage starter to a real deployment to Kubernetes.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Describe Jenkins's controller/agent architecture and where builds actually run
  • Choose between Declarative and Scripted pipeline syntax and justify the choice
  • Write a Jenkinsfile with stages, steps, environment variables, and post actions
  • Run stages inside Docker agents and in parallel to speed pipelines up
  • Use credentials, when conditions, and input approval gates for safe deployments

Estimated Time: 45–55 minutes  β€’  Difficulty: Intermediate

Hands-on: Author a Jenkinsfile that builds with Maven, tests in parallel, and gates production behind manual approval.

In This Lesson

Why Jenkins

Jenkins is an open-source automation server and one of the oldest, most battle-tested CI/CD tools around. If GitHub Actions is a specialized power tool bolted onto GitHub, Jenkins is the fully-stocked workshop: a standalone application you host yourself, with a plugin ecosystem numbering in the thousands and the ability to connect to almost any source control, cloud, or on-prem system.

That self-hosted nature is its defining trade-off. You get complete control over the hardware, the network, and the software β€” invaluable in regulated industries, air-gapped networks, or shops with unusual build needs β€” but you also own the installation, upgrades, and security.

AspectJenkinsGitHub Actions
HostingSelf-hosted (your infrastructure)Cloud-hosted by GitHub (or self-hosted runners)
Setup effortHigher β€” install, configure, maintainLower β€” built into GitHub
ExtensibilityThousands of pluginsMarketplace actions
Source controlAlmost any SCMGitHub-centric
Pipeline languageGroovy (a Jenkinsfile)YAML
Infrastructure controlCompleteLimited to runners
πŸ’‘ It's not either/or. Many organizations use both: GitHub Actions for lightweight repo checks and Jenkins for heavyweight, compliance-driven, multi-environment deployments. Knowing both makes you portable across teams.

Controller & Agents

Jenkins splits work between a controller and one or more agents. The controller is the brain β€” it schedules builds, serves the web UI and REST API, and manages plugins β€” but it should do as little actual building as possible. The heavy lifting happens on agents (also called nodes), which keeps the controller responsive and lets you scale out by adding more agents.

Jenkins controller and agent architecture A central controller schedules work and serves the UI, delegating build, test, and deploy jobs to separate agent machines. Controller schedules Β· UI Β· API Β· plugins Agent 1 build jobs Agent 2 test jobs Agent 3 deploy jobs
Figure 1 β€” The controller orchestrates; agents execute. Adding agents scales your build capacity horizontally without touching the controller.
  • Controller β€” schedules jobs, hosts the UI/API, manages plugins and configuration.
  • Agent (node) β€” a worker that runs the actual build/test/deploy steps in a workspace directory.
  • Job / Pipeline β€” the unit of automation; modern Jenkins favors pipelines over old-style freestyle jobs.
  • Plugin β€” an extension adding integrations (Git, Docker, Slack, Kubernetes, and thousands more).

You can run the whole thing from a container to try it out. The official image mounts a persistent volume for its home directory and (optionally) the host Docker socket so pipelines can build images:

# Quick start with Docker
docker run -d --name jenkins \
  -p 8080:8080 -p 50000:50000 \
  -v jenkins_home:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --restart unless-stopped \
  jenkins/jenkins:lts

# Then open http://localhost:8080 and complete the setup wizard.
# The initial admin password:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

Pipeline as Code

Rather than clicking through the UI to configure a chain of jobs, a Jenkins Pipeline describes the entire delivery process as code in a Jenkinsfile checked into your repository. This makes your pipeline versioned, reviewable, and reproducible β€” the same "pipeline as code" principle behind GitHub Actions' YAML.

Jenkins offers two syntaxes for that file:

graph TD A[Jenkins Pipeline] --> B[Declarative] A --> C[Scripted] B --> B1[Structured, opinionated] B --> B2[Easier to learn] B --> B3[Recommended default] C --> C1[Full Groovy language] C --> C2[Maximum flexibility] C --> C3[Steeper learning curve]

The core vocabulary is the same for both:

πŸ“– Pipeline Vocabulary

Pipeline: the whole workflow definition. Agent: where it runs. Stage: a named phase (Build, Test, Deploy) shown as a column in the UI. Step: a single task inside a stage. Jenkinsfile: the text file holding it all, stored in SCM.

Here's a minimal Declarative pipeline β€” three stages plus a post block that always runs:

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                echo 'Building…'
                sh 'npm ci'
                sh 'npm run build'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing…'
                sh 'npm test'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying…'
                sh 'npm run deploy'
            }
        }
    }

    post {
        always  { echo 'Pipeline finished.' }
        success { echo 'All green βœ…' }
        failure { echo 'Something broke ❌' }
    }
}

To run it, create a new Pipeline item in Jenkins and point it at the Jenkinsfile in your SCM β€” Jenkins fetches and executes it on every triggered build.

Declarative Pipeline in Depth

Declarative Pipeline gives you a fixed set of well-defined sections. That rigidity is a feature: it's predictable, easy to read, and hard to misuse.

pipeline {
    agent { ... }        // where to run
    options { ... }      // pipeline-wide options (timeouts, retries)
    environment { ... }  // environment variables
    parameters { ... }   // build parameters
    tools { ... }        // auto-installed tools on PATH
    triggers { ... }     // what triggers the pipeline
    stages { ... }       // the actual work
    post { ... }         // cleanup / notifications
}

Agents

The agent directive says where a pipeline β€” or an individual stage β€” executes. Running inside a Docker image is especially powerful: each build gets a clean, pinned toolchain with no "works on this agent only" drift.

agent any                           // any available agent
agent { label 'linux-fast' }        // an agent with this label
agent {                             // a fresh Docker container per run
    docker {
        image 'node:20-alpine'
        args '-v /tmp:/tmp'
    }
}
agent none                          // no global agent; set it per stage

Environment & credentials

Never hardcode secrets. Store them in Jenkins's credentials manager and pull them in with the credentials() helper, which also creates _USR and _PSW companions for username/password pairs:

environment {
    CI = 'true'
    // Injected from the Jenkins credentials store, never printed in logs
    REGISTRY_CREDS = credentials('docker-registry')
    // -> REGISTRY_CREDS_USR and REGISTRY_CREDS_PSW are available too
}

Stages, steps & conditions

Stages structure the pipeline; the when directive controls whether a stage runs at all β€” perfect for branch-specific deploys:

stages {
    stage('Test') {
        steps {
            sh 'npm test'
            junit 'test-results/*.xml'   // publish test reports
        }
    }
    stage('Deploy to Production') {
        when {
            branch 'main'                 // only on main
        }
        steps {
            sh './deploy.sh production'
        }
    }
}

Post actions

The post section runs at the end of a pipeline or stage, branching on the result. Use it for cleanup and notifications so they happen whether the build passed or failed:

post {
    always  { cleanWs() }                // clean the workspace every time
    success { echo 'Build succeeded.' }
    failure {
        slackSend channel: '#builds',
                  color: 'danger',
                  message: "Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
    }
    fixed   { echo 'Back to green!' }     // failure -> success transition
}

Parallel, Docker & Approvals

Run stages in parallel

Independent work should run at the same time. Wrap sibling stages in parallel to cut wall-clock time:

stage('Test') {
    parallel {
        stage('Unit')        { steps { sh 'npm run test:unit' } }
        stage('Integration') { steps { sh 'npm run test:integration' } }
        stage('E2E')         { steps { sh 'npm run test:e2e' } }
    }
}
graph TD A[Test stage] --> B[Unit] A --> C[Integration] A --> D[E2E] B --> E[Next stage] C --> E D --> E

Pause for human approval

Continuous Delivery keeps a human in the loop before production. The input step pauses the pipeline until an authorized person approves:

stage('Approve Production') {
    steps {
        input message: 'Deploy to production?',
              ok: 'Ship it',
              submitter: 'devops,leads'
    }
}

Pass files between stages/agents

Because parallel stages and different agents don't share a workspace, use stash/unstash to carry build artifacts across:

stage('Build') {
    steps {
        sh 'npm run build'
        stash includes: 'dist/**', name: 'app-build'
    }
}
stage('Deploy') {
    agent { label 'deploy' }
    steps {
        unstash 'app-build'
        sh 'rsync -avz dist/ server:/var/www/html/'
    }
}

πŸ’‘ Shared libraries keep pipelines DRY

When many repos repeat the same logic, extract it into a shared library and call a function from your Jenkinsfile β€” the Jenkins equivalent of a reusable workflow.

@Library('my-shared-library') _
pipeline {
    agent any
    stages {
        stage('Build') { steps { buildNodeApp(nodeVersion: '20') } }
    }
}

Scripted Pipeline

Declarative is the recommended default, but Scripted Pipeline exists for cases that need the full power of the Groovy language β€” dynamic logic, loops, and custom flow control the declarative sections can't express.

node('linux') {
    checkout scm
    def nodeVersion = '20'

    stage('Build') {
        try {
            sh 'npm ci'
            sh 'npm run build'
        } catch (err) {
            currentBuild.result = 'FAILURE'
            throw err
        }
    }

    stage('Test') {
        if (currentBuild.result != 'FAILURE') {
            sh 'npm test'
        }
    }

    stage('Deploy') {
        if (env.BRANCH_NAME == 'main' && currentBuild.result != 'FAILURE') {
            sh 'npm run deploy'
        }
    }
}
FeatureDeclarativeScripted
SyntaxStructured DSLFull Groovy
Flow controlLimited (when, parallel)if/else, loops, functions
Error handlingpost sectionstry/catch
Learning curveGentlerSteeper (needs Groovy)
Best forMost pipelinesComplex, dynamic workflows

βœ… Rule of thumb

Start Declarative. Reach for Scripted only when you hit a wall Declarative genuinely can't handle β€” and even then, consider whether a shared library would be cleaner.

Hands-on Exercise

πŸ‹οΈ Build a Real Deployment Jenkinsfile

Objective: Write a Declarative pipeline for a Java web app that builds with Maven, runs unit and integration tests in parallel, builds and pushes a Docker image, deploys to a staging Kubernetes namespace, then gates production behind manual approval.

Requirements:

  1. Build stage runs Maven inside a Docker agent and stashes the jar.
  2. Test stage runs unit + integration tests in parallel.
  3. An image is built and pushed using credentials from the Jenkins store.
  4. Staging deploys automatically; production waits for input approval.
πŸ’‘ Hint

Use agent none at the top and set per-stage agents. Put DOCKER_CREDS = credentials('docker-registry') in environment, then reference ${DOCKER_CREDS_USR} / ${DOCKER_CREDS_PSW}. The production gate is just a stage whose first step is input.

βœ… Example solution
pipeline {
    agent none

    environment {
        REGISTRY   = 'registry.example.com'
        IMAGE      = 'java-webapp'
        TAG        = "${env.BUILD_NUMBER}"
        DOCKER_CREDS = credentials('docker-registry')
    }

    stages {
        stage('Build') {
            agent { docker { image 'maven:3.9-eclipse-temurin-21'; args '-v $HOME/.m2:/root/.m2' } }
            steps {
                sh 'mvn -B -DskipTests clean package'
                stash includes: 'target/*.jar, Dockerfile', name: 'app'
            }
        }

        stage('Test') {
            parallel {
                stage('Unit') {
                    agent { docker { image 'maven:3.9-eclipse-temurin-21'; args '-v $HOME/.m2:/root/.m2' } }
                    steps { sh 'mvn test'; junit 'target/surefire-reports/*.xml' }
                }
                stage('Integration') {
                    agent { docker { image 'maven:3.9-eclipse-temurin-21'; args '-v $HOME/.m2:/root/.m2' } }
                    steps { sh 'mvn verify -DskipUnitTests'; junit 'target/failsafe-reports/*.xml' }
                }
            }
        }

        stage('Image') {
            agent { label 'docker' }
            steps {
                unstash 'app'
                sh '''
                    docker build -t $REGISTRY/$IMAGE:$TAG .
                    echo "$DOCKER_CREDS_PSW" | docker login $REGISTRY -u "$DOCKER_CREDS_USR" --password-stdin
                    docker push $REGISTRY/$IMAGE:$TAG
                '''
            }
        }

        stage('Deploy staging') {
            agent { label 'deploy' }
            steps {
                sh 'kubectl set image deployment/webapp webapp=$REGISTRY/$IMAGE:$TAG -n staging'
                sh 'kubectl rollout status deployment/webapp -n staging'
            }
        }

        stage('Approve production') {
            steps { input message: 'Deploy to production?', ok: 'Approve' }
        }

        stage('Deploy production') {
            agent { label 'deploy' }
            steps {
                sh 'kubectl set image deployment/webapp webapp=$REGISTRY/$IMAGE:$TAG -n production'
                sh 'kubectl rollout status deployment/webapp -n production'
            }
        }
    }

    post {
        success { slackSend channel: '#deploys', color: 'good',   message: "Deployed $IMAGE:$TAG βœ…" }
        failure { slackSend channel: '#deploys', color: 'danger', message: "Pipeline failed ❌" }
    }
}

Notice how each concept from the lesson appears: Docker agents, parallel tests, credentials, stash/unstash across agents, an input approval gate, and result-based post notifications.

🎯 Quick Quiz

Question 1: In Jenkins's architecture, where should the actual build and test work run?

Question 2: Which pipeline syntax should you reach for by default, and why?

Question 3: How do you keep a human approval step before deploying to production?

Best Practices

βœ… Do

  • Keep the Jenkinsfile in SCM alongside the code it builds.
  • Prefer Declarative; move complex logic into shared libraries.
  • Give each stage a single, clearly-named responsibility.
  • Use Docker agents for clean, pinned, reproducible toolchains.
  • Store all secrets in the credentials manager and set timeouts to catch hung builds.
  • Run independent work in parallel and clean workspaces with cleanWs().

⚠️ Don't

  • Hardcode passwords, tokens, or keys anywhere in the pipeline.
  • Run heavy builds on the controller.
  • Archive everything β€” be selective with archiveArtifacts.
  • Reach for Scripted before Declarative has genuinely failed you.
  • Let workspaces accumulate and fill the disk.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Jenkins is self-hosted and endlessly extensible β€” total control in exchange for running it yourself.
  • A controller schedules and serves; agents do the work and let you scale out.
  • Pipelines are code in a Jenkinsfile; Declarative is the readable default, Scripted the flexible escape hatch.
  • Use Docker agents, parallel stages, credentials, when conditions, and input gates to build safe, fast pipelines.
  • stash/unstash carry artifacts across agents; post handles cleanup and notifications.

πŸ“š Further Reading

πŸš€ What's Next?

Your pipelines keep building and pushing Docker images β€” so it's time to get serious about Docker itself. Next we'll cover Docker in Production: multi-stage builds, small secure images, and running containers reliably under real load.

πŸŽ‰ Two CI/CD tools down!

You can now build pipelines in both GitHub Actions and Jenkins. That covers the vast majority of teams you'll meet.