Skip to content

Jenkins CI/CD: Pipelines, Agents & Deployments

Stand up Jenkins, wire it to GitHub and Bitbucket, and ship declarative pipelines that build, test, and deploy on every push.

JenkinsGroovyGitHubBitbucket
RR

Ranjith R

Linux SysAdmin & Cloud Engineer

Published March 1, 2026Updated May 2, 2026
9 min readIntermediate#Jenkins#CI/CD#Pipeline#Automation
Share
Section 01

Introduction

Jenkins is the automation server that sits at the center of most self-hosted CI/CD setups: it watches your repositories, runs builds and tests the moment code changes, and — when everything passes — pushes that code out to servers, registries, or orchestrators. It is not the newest tool in the CI/CD space, but it remains the most flexible: a huge plugin ecosystem, first-class support for both GitHub and Bitbucket, and a pipeline model expressive enough to encode almost any real-world release process as code.

This guide covers standing up Jenkins from scratch, understanding the controller/agent execution model, configuring credentials and tool installations, writing declarative pipelines, wiring webhooks from GitHub and Bitbucket so builds trigger on every push, and deploying build output to a server or a container registry. It closes with the troubleshooting instincts and interview questions that come from running Jenkins in production rather than just reading about it.

Who this is for

  • Engineers setting up CI/CD for a team that currently ships by hand.
  • DevOps engineers who need to migrate ad-hoc deploy scripts into a real pipeline.
  • Anyone who has used Jenkins as a black box and wants to understand what is actually happening underneath.
Section 02

Prerequisites

Before working through the installation and pipeline sections below, have ready:

  • A Linux VM or cloud instance with at least 2 vCPU / 4GB RAM for the controller.
  • Java 17 or 21 available (Jenkins 2.4xx LTS requires a supported JDK) — or Docker if you'll run the container image instead.
  • A GitHub or Bitbucket repository you control, so you can register webhooks.
  • SSH access to a target deploy server, or credentials for a container registry, for the deployment stages later on.
  • Basic familiarity with Groovy syntax — declarative pipelines borrow just enough of it to read comfortably.

LTS vs weekly releases

Use the LTS (Long-Term Support) line for anything production-facing — it receives the same features as the weekly releases but only after they have soaked for several weeks, with security backports.
Section 03

Theory

What CI/CD actually buys you

Continuous Integration means every push is built and tested automatically, so integration bugs surface within minutes instead of at release time. Continuous Delivery extends that to producing a release-ready artifact on every successful build. Continuous Deployment goes one step further and pushes that artifact to production automatically, with no manual gate. Jenkins can drive any of the three — which one you get depends entirely on what your pipeline does after the tests pass.

The controller / agent model

A Jenkins deployment has one controller(formerly called the "master") that owns the UI, the job configuration, the plugin ecosystem, and the scheduling queue. Actual build work happens on agents(formerly "slaves") — separate machines or containers that connect to the controller and execute the steps of a pipeline. This split matters for two reasons: it keeps untrusted build workloads off the controller, and it lets you run builds with different OSes, tool versions, or hardware (a Windows agent for a .NET build, a GPU agent for ML jobs) from the same controller.

Declarative vs scripted pipeline syntax

Jenkins pipelines are written in Groovy DSL, checked into the repo as a Jenkinsfile. There are two syntaxes:

DeclarativeScripted
Starts with pipeline {, a fixed structure of agent, stages, postStarts with node {, arbitrary Groovy control flow
Easier to read, lint, and restart mid-pipelineFull programming power — loops, closures, try/catch anywhere
Recommended default for almost everythingReach for it only when declarative's structure genuinely can't express the logic

A declarative pipeline is built from stages (logical phases like Build, Test, Deploy, shown as columns in the Jenkins UI), each containing one or more steps (the actual commands). A post block at the pipeline or stage level runs regardless of outcome — the natural place for notifications and cleanup.

Static vs dynamic (cloud) agents

A static agentis a long-lived machine you register once and it stays connected, ready to pick up builds — simple, predictable, but idle capacity when there's no work. A dynamic/cloud agent is provisioned on demand (a Docker container, a Kubernetes pod, an EC2 instance) for a single build and then torn down — better utilization at the cost of slightly higher per-build startup time. The Kubernetes plugin and Docker plugin are the two most common ways to run dynamic agents.

Section 04

Architecture

A typical setup: source pushes from GitHub and Bitbucket trigger the controller via webhook, the controller queues the build and dispatches it to a labeled agent, and the agent does the actual work of building, testing, and shipping to a registry or server.

GitHub/Bitbucket webhooks → controller → labeled agents → registry / server / orchestrator

Why labels matter

Agents are tagged with labels (e.g. linux-docker, windows, gpu). A pipeline's agent {label '...' } directive is how you pin a stage to the hardware or toolchain it actually needs, instead of letting it land on whatever agent happens to be free.
Section 05

Installation

Jenkins ships three practical installation paths. All three end up at the same web UI on port 8080 — pick whichever fits how the rest of your infrastructure is run.

terminal
# Requires a supported JDK (17 or 21) already installed
java -version

# Download the latest LTS WAR
curl -fsSL -O https://get.jenkins.io/war-stable/latest/jenkins.war

# Run it directly (foreground, good for a quick trial)
java -jar jenkins.war --httpPort=8080

# Get the initial admin password
cat /root/.jenkins/secrets/initialAdminPassword

Never expose the controller directly to the internet

Put Jenkins behind a reverse proxy (nginx/Caddy) with TLS and, ideally, an IP allowlist or VPN. The controller has broad access to credentials and deploy targets — treat it like a production secrets store, not a public web app.
Section 06

Configuration

Credentials store

Manage Jenkins → Credentials is an encrypted store, never plain environment variables in a job config. Every credential has a type and an ID — the ID is what pipelines reference:

TypeUsed for
Secret textAPI tokens — GitHub PAT, Slack webhook URL, registry token
Username with passwordRegistry logins, Bitbucket App passwords
SSH username with private keyDeploying to a server over SSH, git-over-ssh checkouts
Secret filekubeconfig files, service account JSON keys

Pipelines pull credentials in with withCredentials or the shorthand environment { ... credentials('id') }block — Jenkins masks the value in the console log automatically as long as it's referenced this way rather than echoed manually:

Jenkinsfile (snippet)
withCredentials([
    usernamePassword(credentialsId: 'registry-creds', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS'),
    sshUserPrivateKey(credentialsId: 'deploy-ssh-key', keyFileVariable: 'SSH_KEY')
]) {
    sh '''
        echo "$REG_PASS" | docker login registry.example.com -u "$REG_USER" --password-stdin
        ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no deploy@prod01 "systemctl restart app"
    '''
}

Never bind credentials to plain env vars

environment { PASS = credentials('my-secret') } is safe because Jenkins masks it in logs. Manually running PASS=$(cat secret.txt) and exporting it yourself is not — it bypasses masking and often ends up in sh -x trace output.

Global tool configuration

Manage Jenkins → Tools lets you register named installations of JDK, Maven, Gradle, Node.js, and Docker — either pointing at a fixed path or letting Jenkins auto-install them per agent on first use. Pipelines then reference tools by name instead of hardcoding paths:

Jenkinsfile (snippet)
pipeline {
    agent any
    tools {
        jdk 'jdk17'
        maven 'maven-3.9'
        nodejs 'node20'
    }
    stages {
        stage('Build') {
            steps {
                sh 'mvn -B -DskipTests package'
            }
        }
    }
}

Agents and nodes

Manage Jenkins → Nodes is where static agents are registered: give the node a name, one or more labels, a remote root directory, and a launch method — usually "Launch agent by connecting it to the controller" (JNLP/inbound, agent initiates the connection, works behind NAT) or "Launch via SSH" (controller connects out to the agent, needs the agent reachable and a credential registered).

terminal (on the agent machine, JNLP mode)
# Download the matching agent.jar from the controller
curl -sO http://jenkins.example.com:8080/jnlpJars/agent.jar

# Connect using the secret shown on the Node's configuration page
java -jar agent.jar -jnlpUrl http://jenkins.example.com:8080/computer/agent-01/jenkins-agent.jnlp \
  -secret <secret-from-ui> -workDir "/home/jenkins/agent"
Section 07

Commands

Jenkins CLI

terminal
# Download the CLI jar from a running controller
curl -sO http://jenkins.example.com:8080/jnlpJars/jenkins-cli.jar

# Authenticate with an API token (Manage Jenkins > Users > Security > API Token)
alias jcli='java -jar jenkins-cli.jar -s http://jenkins.example.com:8080/ -auth admin:$JENKINS_TOKEN'

jcli list-jobs                          # list all jobs
jcli build my-pipeline -f -v            # trigger a build, follow output
jcli console my-pipeline                # last build's console log
jcli get-job my-pipeline > config.xml   # export job config as XML
jcli create-job new-job < config.xml    # create a job from XML
jcli safe-restart                       # restart once builds finish
jcli quiet-down                         # stop accepting new builds
jcli list-plugins

Common Groovy pipeline snippets

Jenkinsfile (snippets)
// Read a value out of a properties/JSON/YAML file
def props = readProperties file: 'version.properties'
echo "Building version ${props.VERSION}"

// Conditional stage execution
stage('Deploy to prod') {
    when { branch 'main' }
    steps { sh './deploy.sh prod' }
}

// Parallel stages
stage('Test matrix') {
    parallel {
        stage('Unit')        { steps { sh 'npm test' } }
        stage('Integration') { steps { sh 'npm run test:int' } }
    }
}

// Retry a flaky step
retry(3) {
    sh 'curl -f https://staging.example.com/health'
}

// Timeout a hung step
timeout(time: 10, unit: 'MINUTES') {
    sh './long-running-migration.sh'
}
Section 08

Examples

A complete declarative pipeline: checkout, install dependencies, run tests, build a Docker image, push it to a registry, then deploy over SSH — the shape almost every service-style app ends up using.

Jenkinsfile
pipeline {
    agent { label 'linux-docker' }

    environment {
        IMAGE = "registry.example.com/acme/api"
        TAG   = "${env.GIT_COMMIT.take(7)}"
    }

    options {
        timestamps()
        buildDiscarder(logRotator(numToKeepStr: '20'))
        disableConcurrentBuilds()
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Install & Unit Test') {
            steps {
                sh 'npm ci'
                sh 'npm run lint'
                sh 'npm test -- --ci --reporters=default --reporters=jest-junit'
            }
            post {
                always {
                    junit 'reports/junit.xml'
                }
            }
        }

        stage('Build Image') {
            steps {
                sh "docker build -t ${IMAGE}:${TAG} -t ${IMAGE}:latest ."
            }
        }

        stage('Push Image') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'registry-creds',
                    usernameVariable: 'REG_USER',
                    passwordVariable: 'REG_PASS'
                )]) {
                    sh '''
                        echo "$REG_PASS" | docker login registry.example.com -u "$REG_USER" --password-stdin
                        docker push $IMAGE:$TAG
                        docker push $IMAGE:latest
                    '''
                }
            }
        }

        stage('Deploy') {
            when { branch 'main' }
            steps {
                sshagent(credentials: ['deploy-ssh-key']) {
                    sh """
                        ssh -o StrictHostKeyChecking=no deploy@prod01 \
                          "docker pull ${IMAGE}:${TAG} && \
                           docker service update --image ${IMAGE}:${TAG} acme_api"
                    """
                }
            }
        }
    }

    post {
        success {
            slackSend(channel: '#builds', message: "${env.JOB_NAME} #${env.BUILD_NUMBER} deployed ${TAG}")
        }
        failure {
            slackSend(channel: '#builds', message: "${env.JOB_NAME} #${env.BUILD_NUMBER} failed")
        }
    }
}
  1. Checkout

    Jenkins checks out the exact commit that triggered the build via checkout scm.
  2. Install & test

    Dependencies install, lint runs, and the unit suite runs with a JUnit-format report so failures show up as annotated test results in the UI.
  3. Build & push image

    A tagged image is built and pushed to the registry, tagged with both the short commit SHA and latest.
  4. Deploy (main only)

    The when { branch 'main' } guard means only merges to main trigger a production rollout, over SSH via sshagent.
Section 10

Real World Example

Scenario: a team wants every push to mainon their GitHub repo to automatically build, test, and deploy to production with zero manual steps — the classic "deploy on push" setup.

  1. Install the GitHub plugin and connect an App/token

    Install the GitHub and GitHub Branch Source plugins, then register either a GitHub App (recommended — scoped, higher rate limits) or a personal access token as a Secret text credential in Manage Jenkins → Credentials.
  2. Add the repository webhook

    In the GitHub repo settings, add a webhook pointing at http://jenkins.example.com/github-webhook/ with content type application/json and the push event selected. GitHub will POST there on every push.
  3. Add the githubPush() trigger to the Jenkinsfile

    Jenkinsfile (snippet)
    pipeline {
        agent { label 'linux-docker' }
        triggers {
            githubPush()
        }
        stages {
            stage('Build & Deploy') {
                steps { sh './ci/build-and-deploy.sh' }
            }
        }
    }
  4. Push and watch it fire

    A push to main hits the webhook, GitHub calls Jenkins, Jenkins matches the payload to the job with githubPush() and queues a build within seconds — no polling interval to wait on.
  5. Do the same for Bitbucket

    Install the Bitbucket Server Integration (for Bitbucket Server) or the built-in Bitbucket Cloud webhook support, add a repository webhook pointing at http://jenkins.example.com/bitbucket-hook/, and swap githubPush() for the BitbucketWebhookTriggerImpl equivalent (available as a trigger checkbox in the job's configuration, or the plugin's pipeline syntax snippet generator).
Troubleshooting

Common Issues

Agent shows offline

Check the agent's log from Manage Jenkins → Nodes → the node → Log. Common causes: the JNLP secret in the connection command expired or was regenerated, a firewall blocks the controller's inbound port (default 50000for TCP agents), or the agent's Java version no longer matches what the controller requires after an upgrade.

Webhook not triggering a build

On GitHub/Bitbucket, check the webhook's "Recent Deliveries" tab first — a non-2xx response there means Jenkins never received it (reverse proxy blocking the path, wrong URL). If the delivery shows success but no build starts, confirm the Jenkinsfile actually has the trigger (githubPush() or the Bitbucket equivalent) and that the job has been built at least once manually so Jenkins has registered the trigger.

Credential permission denied in a pipeline

Credentials are scoped — a credential stored at the folder level isn't visible to a job outside that folder, and Folder-scoped or Global credentials require the pipeline (and the user who last approved the script) to actually have permission to use them under Project-based Matrix Authorization. Check Manage Jenkins → Credentials scope before assuming the credential ID is wrong.

Pipeline stuck in the build queue

Almost always means no connected agent matches the requested agent { label ... }. Check Manage Jenkins → Nodes for an agent with that exact label that is online and has a free executor — an agent at 0 idle executors will queue new builds indefinitely rather than failing loudly.

Controller disk fills up from build artifacts

Old workspaces, archived artifacts, and Docker layers on agents accumulate fast. Set buildDiscarder(logRotator(numToKeepStr: '20'))in every pipeline, enable the Workspace Cleanup plugin's post-build wipe, and run a scheduled docker system prune -f on Docker-based agents.
Best Practices

Best Practices

  • Keep the Jenkinsfile in the application repo (pipeline-as-code), never build steps typed into the job configuration UI.
  • Pin agent labels per stage instead of agent any once you have more than one agent type — it prevents a build silently landing on the wrong toolchain.
  • Set buildDiscarder and a pipeline timeout on every job so a hung or forgotten job doesn't consume executors or disk forever.
  • Use shared libraries (vars/*.groovy in a separate repo) once more than two or three Jenkinsfiles start repeating the same stages.
  • Treat the controller as a critical, backed-up system — back up JENKINS_HOME (jobs, credentials, plugin configs) on a schedule, not just the application servers it deploys to.
  • Prefer declarative syntax by default; drop to scripted only for logic declarative genuinely cannot express.
Security

Security Hardening

  • Credential binding, not environment leakage — always inject secrets via withCredentials/credentials() so Jenkins masks them in console output; never echo or manually export a raw secret value.
  • Matrix/role-based authorization — replace the default "logged-in users can do anything" with Project-based Matrix Authorization or Role-Based Access Control so only the right teams can view/build/configure specific jobs.
  • Script approval sandbox — the Groovy sandbox blocks unapproved dangerous method calls in pipeline scripts; approve individual signatures deliberately under Manage Jenkins → In-process Script Approval rather than disabling the sandbox.
  • Agent-to-controller trust — enable Agent → Controller Access Control so agents (which run less-trusted build code) can't call back into controller-only APIs; never run untrusted PR builds on an agent that also holds deploy credentials.
  • Webhook validation — configure a shared secret on GitHub/Bitbucket webhooks and verify the signature so arbitrary requests to the webhook URL can't trigger builds.
  • Keep plugins current — the plugin manager flags known CVEs; outdated plugins are the most common real-world Jenkins compromise vector.
Interview Questions

Interview Questions

The controller owns the UI, job configuration, plugins, and scheduling; it dispatches actual build execution to agents, which are separate machines or containers that connect to the controller and run the pipeline steps. Separating the two keeps untrusted build workloads off the sensitive controller.
Cheat Sheet

Cheat Sheet

jenkins-cheatsheet.groovy
// --- Minimal declarative skeleton ---
pipeline {
    agent { label 'linux-docker' }
    triggers { githubPush() }
    options { timestamps(); buildDiscarder(logRotator(numToKeepStr: '20')) }
    environment { TAG = "${env.GIT_COMMIT.take(7)}" }
    stages {
        stage('Build')  { steps { sh 'make build' } }
        stage('Test')   { steps { sh 'make test' } }
        stage('Deploy') { when { branch 'main' } steps { sh 'make deploy' } }
    }
    post {
        success { echo 'OK' }
        failure { echo 'FAILED' }
    }
}

// --- Credentials ---
withCredentials([usernamePassword(credentialsId: 'id', usernameVariable: 'U', passwordVariable: 'P')]) { sh '...' }
sshagent(credentials: ['deploy-ssh-key']) { sh 'ssh ...' }

// --- Control flow helpers ---
retry(3) { sh '...' }
timeout(time: 10, unit: 'MINUTES') { sh '...' }
parallel(a: { sh '...' }, b: { sh '...' })
when { branch 'main' }
when { expression { return env.BRANCH_NAME == 'main' } }

// --- Jenkins CLI ---
// java -jar jenkins-cli.jar -s http://host:8080/ -auth user:token list-jobs
// java -jar jenkins-cli.jar -s http://host:8080/ -auth user:token build my-job -f -v
// java -jar jenkins-cli.jar -s http://host:8080/ -auth user:token safe-restart

// --- Service management (package install) ---
// sudo systemctl restart jenkins
// sudo journalctl -u jenkins -f
// sudo tail -f /var/log/jenkins/jenkins.log
Summary

Summary

Jenkins earns its place in a modern toolchain by being unglamorous but completely flexible: a controller that schedules work, agents that execute it in isolation, a credentials store that keeps secrets out of logs, and a declarative Jenkinsfile that turns your entire release process — build, test, package, deploy — into code that lives next to the application it ships. Wire GitHub or Bitbucket webhooks in and that whole process runs on every push with no manual trigger. The fundamentals here — pipeline structure, agent labeling, credential binding, and webhook-driven triggers — carry directly into any CI/CD system you touch afterward, Jenkins or not.

Resources

Resources

  • Official Jenkins documentation and pipeline syntax reference — jenkins.io
  • Jenkins plugin index — plugins.jenkins.io
  • GitHub webhook and Apps documentation — docs.github.com
  • Bitbucket webhooks and integrations documentation — support.atlassian.com
  • Groovy language documentation (pipeline scripting foundation) — groovy-lang.org