Jenkins
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 Architecture
Section titled “Jenkins Architecture”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| Component | What It Does |
|---|---|
| Controller | The primary Jenkins server — manages jobs, schedules builds, serves the UI |
| Agent (node) | A machine that executes build jobs (can be permanent or ephemeral) |
| Executor | A thread on an agent that runs a single build (an agent can have multiple executors) |
| Job / Project | A configured build task (freestyle or pipeline) |
| Build | A single execution of a job |
Jenkinsfile (Pipeline as Code)
Section titled “Jenkinsfile (Pipeline as Code)”Modern Jenkins uses Jenkinsfile — a pipeline definition stored in the repository. There are two syntaxes:
Declarative Pipeline (Recommended)
Section titled “Declarative Pipeline (Recommended)”// 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 } }}Scripted Pipeline
Section titled “Scripted Pipeline”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() }}Declarative vs Scripted
Section titled “Declarative vs Scripted”| Declarative | Scripted | |
|---|---|---|
| Syntax | Structured, opinionated | Free-form Groovy |
| Ease | Easier to read and write | More flexible but verbose |
| Validation | Validates before running | Fails at runtime |
| Best for | Most pipelines | Complex conditional logic |
Key Jenkinsfile Features
Section titled “Key Jenkinsfile Features”agent — Where Jobs Run
Section titled “agent — Where Jobs Run”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 Type | What It Does |
|---|---|
any | Run on any available agent |
none | Don’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) |
when — Conditional Execution
Section titled “when — Conditional Execution”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' }}input — Manual Approval
Section titled “input — Manual Approval”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}" }}parallel — Parallel Stages
Section titled “parallel — Parallel Stages”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 — Actions After Build
Section titled “post — Actions After Build”post { always { junit '**/test-results/*.xml' // Always collect test results cleanWs() } success { slackSend(message: "Build passed") } failure { slackSend(message: "Build FAILED") } unstable { echo 'Tests have failures' }}Agents
Section titled “Agents”Permanent Agents
Section titled “Permanent Agents”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)Docker Agents
Section titled “Docker Agents”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 }}Kubernetes Agents (Ephemeral Pods)
Section titled “Kubernetes Agents (Ephemeral Pods)”The Kubernetes plugin launches a pod for each build — auto-scales, clean environment:
agent { kubernetes { yaml '''apiVersion: v1kind: Podspec: containers: - name: node image: node:20-alpine command: [cat] tty: true - name: docker image: docker:24-dind securityContext: privileged: true''' }}Plugins
Section titled “Plugins”Jenkins has 1,800+ plugins. Common ones:
| Plugin | What It Does |
|---|---|
| Pipeline | Jenkinsfile support (installed by default) |
| Git | Git SCM integration |
| Docker Pipeline | Build/run Docker containers in pipeline |
| Kubernetes | Run agents as Kubernetes pods |
| Blue Ocean | Modern UI for pipelines |
| Credentials Binding | Inject secrets into builds |
| Slack Notification | Send build notifications to Slack |
| JUnit | Parse and display test results |
| Cobertura | Code coverage reports |
| GitHub / GitLab | Webhook triggers, status checks |
| Multibranch Pipeline | Auto-discover branches and PRs |
| Shared Libraries | Reusable pipeline code |
| Role-Based Authorization | Fine-grained access control |
Managing Plugins
Section titled “Managing Plugins”// Install via CLIjenkins-cli install-plugin docker-workflow kubernetes slack
// Or use a plugins.txt file (Docker image setup)// plugins.txtpipeline-model-definition:2.2.0docker-workflow:1.30kubernetes:1.31.0Credentials and Secrets
Section titled “Credentials and Secrets”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 Type | Use Case |
|---|---|
| Username with password | Docker registry, API credentials |
| Secret text | API tokens, single-value secrets |
| SSH key | Git checkout, server access |
| Certificate | TLS client certificates |
| File | Kubeconfig, service account JSON |
Shared Libraries
Section titled “Shared Libraries”Shared libraries let you write reusable pipeline code that multiple projects can use:
# Repository: myorg/jenkins-shared-libraryvars/ ├── buildDockerImage.groovy ├── deployToK8s.groovy └── notifySlack.groovysrc/ └── com/myorg/Utils.groovydef 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
Section titled “Multibranch Pipeline”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.
Jenkins vs Modern CI/CD
Section titled “Jenkins vs Modern CI/CD”| Feature | Jenkins | GitHub Actions | GitLab CI |
|---|---|---|---|
| Hosting | Self-hosted only | Cloud + self-hosted | Cloud + self-hosted |
| Config | Groovy (Jenkinsfile) | YAML | YAML |
| Setup | Install + configure + maintain | Zero setup | Zero setup |
| Plugins | 1,800+ (community) | Marketplace actions | Built-in + includes |
| Agents | Permanent, Docker, K8s | GitHub-hosted VMs, self-hosted | Shared, group, project runners |
| UI | Classic (aging) + Blue Ocean | Modern, integrated with GitHub | Modern, integrated with GitLab |
| Maintenance | You manage upgrades, security, backups | Managed by GitHub | Managed by GitLab |
| Flexibility | Extremely high (Groovy = full programming) | High (YAML + actions) | High (YAML + includes) |
| Cost | Free (but infrastructure + ops cost) | Free tier + per-minute pricing | Free tier + per-minute pricing |
When Jenkins Still Makes Sense
Section titled “When Jenkins Still Makes Sense”| Scenario | Why Jenkins |
|---|---|
| Existing investment | Large Jenkins infrastructure already in place |
| Complex pipelines | Need full Groovy programming (conditional logic, API calls, custom flows) |
| Strict compliance | Must self-host everything, no SaaS |
| Plugin dependency | Rely on Jenkins-specific plugins with no equivalent |
| Multi-SCM | Code in Bitbucket, GitHub, GitLab, SVN simultaneously |
When to Consider Migrating
Section titled “When to Consider Migrating”| Signal | Alternative |
|---|---|
| Spending more time maintaining Jenkins than building features | GitHub Actions / GitLab CI |
| Jenkins server is a single point of failure | Cloud-hosted CI/CD |
| Developers avoid CI because the UI is confusing | Modern CI/CD with PR integration |
| Plugin compatibility issues on every upgrade | Platform with built-in features |
Jenkins on Kubernetes (JCasC)
Section titled “Jenkins on Kubernetes (JCasC)”Modern Jenkins deployments use Helm and Configuration as Code (JCasC):
# Install Jenkins via Helmhelm 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-tokenKey Takeaways
Section titled “Key Takeaways”- 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.