Skip to content

Jenkins

First PublishedByAtif Alam

Jenkins is the original open-source CI/CD server — first released in 2011 (as Hudson). It’s self-hosted, extremely flexible, and has the largest plugin ecosystem of any CI/CD tool. While newer platforms (GitHub Actions, GitLab CI) are simpler to set up, Jenkins remains widely used in enterprises and legacy environments.

Jenkins Controller (primary server)
├── Manages configuration, UI, scheduling
├── Stores pipeline definitions, build history
├── Agent: linux-docker
│ └── Executes jobs in Docker containers
├── Agent: linux-large (bare metal)
│ └── Executes resource-heavy builds
└── Agent: windows
└── Executes .NET builds
ComponentWhat It Does
ControllerThe primary Jenkins server — manages jobs, schedules builds, serves the UI
Agent (node)A machine that executes build jobs (can be permanent or ephemeral)
ExecutorA thread on an agent that runs a single build (an agent can have multiple executors)
Job / ProjectA configured build task (freestyle or pipeline)
BuildA single execution of a job

Modern Jenkins uses Jenkinsfile — a pipeline definition stored in the repository. There are two syntaxes:

// Jenkinsfile (Declarative)
pipeline {
agent any // Run on any available agent
environment {
APP_NAME = 'myapp'
REGISTRY = 'myregistry.com'
}
options {
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Build') {
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Test') {
parallel { // Run test jobs in parallel
stage('Unit Tests') {
steps {
sh 'npm test -- --coverage'
}
post {
always {
junit 'test-results/*.xml'
publishHTML([reportDir: 'coverage', reportFiles: 'index.html', reportName: 'Coverage'])
}
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
}
}
stage('Docker Build') {
steps {
script {
docker.build("${REGISTRY}/${APP_NAME}:${env.BUILD_NUMBER}")
}
}
}
stage('Deploy Staging') {
when {
branch 'main' // Only deploy from main
}
steps {
sh './deploy.sh staging'
}
}
stage('Deploy Production') {
when {
branch 'main'
}
input {
message 'Deploy to production?'
ok 'Deploy'
submitter 'admin,release-team'
}
steps {
sh './deploy.sh production'
}
}
}
post {
success {
slackSend(color: 'good', message: "Build succeeded: ${env.JOB_NAME} #${env.BUILD_NUMBER}")
}
failure {
slackSend(color: 'danger', message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}")
}
always {
cleanWs() // Clean workspace after build
}
}
}

The older, more flexible syntax — full Groovy programming:

// Jenkinsfile (Scripted)
node('linux') {
try {
stage('Checkout') {
checkout scm
}
stage('Build') {
sh 'npm ci && npm run build'
}
stage('Test') {
sh 'npm test'
}
if (env.BRANCH_NAME == 'main') {
stage('Deploy') {
sh './deploy.sh production'
}
}
} catch (e) {
slackSend(color: 'danger', message: "Build failed: ${env.JOB_NAME}")
throw e
} finally {
cleanWs()
}
}
DeclarativeScripted
SyntaxStructured, opinionatedFree-form Groovy
EaseEasier to read and writeMore flexible but verbose
ValidationValidates before runningFails at runtime
Best forMost pipelinesComplex conditional logic
pipeline {
agent none // No default agent (set per stage)
stages {
stage('Build') {
agent {
docker {
image 'node:20-alpine'
args '-v /tmp/.npm:/root/.npm' // Cache npm
}
}
steps {
sh 'npm ci && npm run build'
}
}
stage('Deploy') {
agent { label 'production' } // Run on agent with "production" label
steps {
sh './deploy.sh'
}
}
}
}
Agent TypeWhat It Does
anyRun on any available agent
noneDon’t allocate an agent (set per stage)
label 'name'Run on an agent with a specific label
docker { image '...' }Run inside a Docker container
kubernetes { ... }Run as a Kubernetes pod (with the K8s plugin)
stage('Deploy Production') {
when {
branch 'main' // Only on main branch
not { changeRequest() } // Not on PRs
environment name: 'DEPLOY', value: 'true'
}
steps {
sh './deploy.sh production'
}
}
stage('Approve') {
input {
message 'Deploy to production?'
ok 'Yes, deploy'
submitter 'release-team' // Who can approve
parameters {
choice(name: 'TARGET', choices: ['staging', 'production'], description: 'Environment')
}
}
steps {
echo "Deploying to ${TARGET}"
}
}
stage('Tests') {
parallel {
stage('Unit') {
agent { docker { image 'node:20' } }
steps { sh 'npm test' }
}
stage('Integration') {
agent { docker { image 'node:20' } }
steps { sh 'npm run test:integration' }
}
stage('E2E') {
agent { label 'e2e-runner' }
steps { sh 'npm run test:e2e' }
}
}
}
post {
always {
junit '**/test-results/*.xml' // Always collect test results
cleanWs()
}
success {
slackSend(message: "Build passed")
}
failure {
slackSend(message: "Build FAILED")
mail to: 'team@example.com', subject: "Build failed: ${env.JOB_NAME}"
}
unstable {
echo 'Tests have failures'
}
}

Machines registered with the controller permanently:

Jenkins Controller
├── Agent: build-server-1 (4 executors, label: linux)
├── Agent: build-server-2 (4 executors, label: linux)
└── Agent: windows-server (2 executors, label: windows)

Run each build in a fresh Docker container:

agent {
docker {
image 'maven:3.9-eclipse-temurin-21'
args '-v $HOME/.m2:/root/.m2' // Mount Maven cache
}
}

The Kubernetes plugin launches a pod for each build — auto-scales, clean environment:

agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:20-alpine
command: [cat]
tty: true
- name: docker
image: docker:24-dind
securityContext:
privileged: true
'''
}
}

Jenkins has 1,800+ plugins. Common ones:

PluginWhat It Does
PipelineJenkinsfile support (installed by default)
GitGit SCM integration
Docker PipelineBuild/run Docker containers in pipeline
KubernetesRun agents as Kubernetes pods
Blue OceanModern UI for pipelines
Credentials BindingInject secrets into builds
Slack NotificationSend build notifications to Slack
JUnitParse and display test results
CoberturaCode coverage reports
GitHub / GitLabWebhook triggers, status checks
Multibranch PipelineAuto-discover branches and PRs
Shared LibrariesReusable pipeline code
Role-Based AuthorizationFine-grained access control
// Install via CLI
jenkins-cli install-plugin docker-workflow kubernetes slack
// Or use a plugins.txt file (Docker image setup)
// plugins.txt
pipeline-model-definition:2.2.0
docker-workflow:1.30
kubernetes:1.31.0

Jenkins stores secrets in a built-in credential store:

pipeline {
environment {
AWS_CREDS = credentials('aws-access-key') // Username + password
DOCKER_TOKEN = credentials('docker-hub-token') // Secret text
SSH_KEY = credentials('deploy-ssh-key') // SSH key
}
stages {
stage('Deploy') {
steps {
// AWS_CREDS_USR and AWS_CREDS_PSW are auto-set
sh 'aws s3 sync dist/ s3://mybucket --region us-east-1'
}
}
}
}
Credential TypeUse Case
Username with passwordDocker registry, API credentials
Secret textAPI tokens, single-value secrets
SSH keyGit checkout, server access
CertificateTLS client certificates
FileKubeconfig, service account JSON

Shared libraries let you write reusable pipeline code that multiple projects can use:

# Repository: myorg/jenkins-shared-library
vars/
├── buildDockerImage.groovy
├── deployToK8s.groovy
└── notifySlack.groovy
src/
└── com/myorg/Utils.groovy
vars/buildDockerImage.groovy
def call(String imageName, String tag = env.BUILD_NUMBER) {
sh "docker build -t ${imageName}:${tag} ."
sh "docker push ${imageName}:${tag}"
}
// Jenkinsfile — using the shared library
@Library('myorg-shared-library') _
pipeline {
agent any
stages {
stage('Build') {
steps {
buildDockerImage('myregistry/myapp')
}
}
stage('Deploy') {
steps {
deployToK8s('myapp', 'production')
}
}
}
post {
always {
notifySlack(currentBuild.result)
}
}
}

Multibranch Pipeline automatically discovers branches and PRs, creating a pipeline for each:

Multibranch Pipeline: myapp
├── main (Jenkinsfile found, pipeline running)
├── feature/search (Jenkinsfile found, pipeline running)
├── PR-42 (Jenkinsfile found, pipeline running)
└── develop (Jenkinsfile found, pipeline running)

Configure in the Jenkins UI or via Job DSL. Jenkins scans the repository periodically (or on webhook) and creates/removes pipelines automatically.

FeatureJenkinsGitHub ActionsGitLab CI
HostingSelf-hosted onlyCloud + self-hostedCloud + self-hosted
ConfigGroovy (Jenkinsfile)YAMLYAML
SetupInstall + configure + maintainZero setupZero setup
Plugins1,800+ (community)Marketplace actionsBuilt-in + includes
AgentsPermanent, Docker, K8sGitHub-hosted VMs, self-hostedShared, group, project runners
UIClassic (aging) + Blue OceanModern, integrated with GitHubModern, integrated with GitLab
MaintenanceYou manage upgrades, security, backupsManaged by GitHubManaged by GitLab
FlexibilityExtremely high (Groovy = full programming)High (YAML + actions)High (YAML + includes)
CostFree (but infrastructure + ops cost)Free tier + per-minute pricingFree tier + per-minute pricing
ScenarioWhy Jenkins
Existing investmentLarge Jenkins infrastructure already in place
Complex pipelinesNeed full Groovy programming (conditional logic, API calls, custom flows)
Strict complianceMust self-host everything, no SaaS
Plugin dependencyRely on Jenkins-specific plugins with no equivalent
Multi-SCMCode in Bitbucket, GitHub, GitLab, SVN simultaneously
SignalAlternative
Spending more time maintaining Jenkins than building featuresGitHub Actions / GitLab CI
Jenkins server is a single point of failureCloud-hosted CI/CD
Developers avoid CI because the UI is confusingModern CI/CD with PR integration
Plugin compatibility issues on every upgradePlatform with built-in features

Modern Jenkins deployments use Helm and Configuration as Code (JCasC):

Terminal window
# Install Jenkins via Helm
helm install jenkins jenkinsci/jenkins \
--namespace jenkins --create-namespace \
--set controller.installPlugins='{kubernetes,workflow-aggregator,git,slack}' \
--set controller.JCasC.configScripts.welcome-message='jenkins: {systemMessage: "Jenkins on K8s"}'

JCasC configures Jenkins entirely via YAML — no manual UI setup:

# jenkins.yaml (JCasC)
jenkins:
systemMessage: "Jenkins on Kubernetes"
numExecutors: 0 # All builds on agents, not controller
securityRealm:
ldap:
configurations:
- server: ldap.example.com
authorizationStrategy:
roleBased:
roles:
global:
- name: admin
permissions: [Overall/Administer]
unclassified:
slackNotifier:
teamDomain: myteam
tokenCredentialId: slack-token
  • Jenkins is self-hosted, extremely flexible, and has the largest plugin ecosystem.
  • Declarative Pipeline (Jenkinsfile) is the recommended syntax — structured, validated, readable.
  • Use Docker or Kubernetes agents for clean, reproducible builds.
  • Shared libraries enable reusable pipeline code across projects.
  • Multibranch Pipeline auto-discovers branches and PRs.
  • Jenkins requires ongoing maintenance (upgrades, security, backups) — unlike managed platforms.
  • Consider migrating to GitHub Actions or GitLab CI if Jenkins maintenance overhead exceeds its benefits.
  • For new Jenkins setups, use Helm + JCasC for infrastructure-as-code deployment.