Jenkins Is Not Dead: A Field Guide to the CI Server That Keeps Shipping the World's Software

A practical, opinionated guide to what Jenkins is, why it endures, and how to run it without losing your weekends.

Arpan Singh
•
10 min read
Jenkins Is Not Dead: A Field Guide to the CI Server That Keeps Shipping the World's Software

Every few years, someone declares Jenkins dead. Every few years, a quiet majority of engineering teams keep shipping through it: banks, game studios, hardware makers, telecoms, government agencies, and a long tail of startups that inherited one from a previous CTO and never found a reason to leave.

Both things are true at once. Jenkins is old, sometimes cranky, and no longer the default choice for a greenfield project. It is also the most flexible, most battle-tested automation server in the industry, and for a large class of problems it is still the right answer.

This post is for three readers: the engineer meeting Jenkins for the first time, the team lead deciding whether to keep it, and the person who just inherited a controller named jenkins-prod-old-DO-NOT-TOUCH. By the end, you should understand how Jenkins works, where it shines, where it bites, and how to run it like a modern platform instead of a pet.

A short history: from Hudson to Jenkins

Jenkins began life as Hudson, created by Kohsuke Kawaguchi while he was working at Sun Microsystems in the mid-2000s. The idea was simple and a little radical for its time: a build server that was easy to install, easy to extend, and friendly enough that developers would actually use it.

After Oracle acquired Sun, a dispute over the project's name and stewardship led the community to fork the code in early 2011 and rename it Jenkins. The community went with the fork, and Jenkins became the project that carried on. Today it is an open-source project hosted under the Continuous Delivery Foundation, with a regular release cadence and a long-term support (LTS) line aimed at teams that value stability over novelty.

That history matters for one reason: Jenkins was built in an era when the answer to almost any integration question was "write a plugin." That decision explains both its greatest strength and its most famous weakness. We will get to both.

How Jenkins actually works

Strip away the plugins and Jenkins has a small, clean architecture:

  • The controller is the brain. It stores configuration, schedules builds, serves the web UI and API, and keeps build history. It should do very little actual work.

  • Agents are the muscle. They are machines, containers, or pods that connect to the controller and execute builds. Each agent offers one or more executors, which are slots for running jobs.

  • Jobs and pipelines describe what to run. Modern Jenkins is built around pipelines: a definition of your build, test, and deploy process, expressed as code and versioned alongside your application.

  • Plugins extend nearly everything: source control, build tools, notifications, credentials, cloud agents, reporting.

A build request arrives (a push, a pull request, a schedule, a manual click). The controller finds an agent matching the labels the pipeline asked for, sends it the work, streams the logs back, and records the result. That is the whole loop. Everything else is variations on it.

Pipelines as code: the feature that changed everything

In the early days, Jenkins jobs were configured by clicking through forms. Those jobs were impossible to review, impossible to reproduce, and easy to break by accident. The introduction of Pipeline and the Jenkinsfile fixed this by letting you check your delivery process into the same repository as your code.

Here is a realistic declarative pipeline:

pipeline {
  agent { label 'docker' }

  options {
    timeout(time: 30, unit: 'MINUTES')
    disableConcurrentBuilds()
    buildDiscarder(logRotator(numToKeepStr: '30'))
  }

  environment {
    IMAGE = "registry.example.com/shop/api"
  }

  stages {
    stage('Build') {
      steps { sh './gradlew assemble' }
    }

    stage('Verify') {
      parallel {
        stage('Unit tests') {
          steps { sh './gradlew test' }
          post { always { junit 'build/test-results/**/*.xml' } }
        }
        stage('Static analysis') {
          steps { sh './gradlew check -x test' }
        }
      }
    }

    stage('Package') {
      when { branch 'main' }
      steps { sh 'docker build -t $IMAGE:$GIT_COMMIT .' }
    }
  }

  post {
    failure { echo 'Build failed. Notify the team here.' }
  }
}

A few things worth noticing. The timeout and concurrency guard are in the file, not in someone's memory. The when clause means only main builds an image. Parallel stages cut wall-clock time without a plugin or a trick. And because this file lives in Git, every change to your delivery process goes through code review like any other change.

Jenkins supports two syntaxes: declarative (structured, opinionated, the one to start with) and scripted (full Groovy, more powerful, easier to turn into spaghetti). Use declarative by default and drop into a script {} block only when you must.

Why Jenkins endures

If newer tools are slicker, why is Jenkins still everywhere? Five reasons, in rough order of importance.

1. You own it. Jenkins runs wherever you can run Java: a rack in your data center, a cloud VM, an air-gapped network with no internet at all. There is no per-minute billing, no vendor deciding your build queue is lower priority, and your source code and secrets never have to leave your network. For regulated industries, that is not a nice-to-have. It is the requirement.

2. The plugin ecosystem is enormous. Whatever you are integrating with, from a modern Kubernetes cluster to a 15-year-old artifact repository, someone has probably written a plugin for it. That breadth is unmatched.

3. Agents can be anything. Linux containers, Windows servers, macOS build machines, GPU boxes, a Raspberry Pi wired to a piece of hardware on a test bench. If it can run the agent and connect to the controller, Jenkins can schedule work onto it. Hosted CI services struggle to match this for embedded, mobile, game, and hardware-in-the-loop workloads.

4. Pipelines are genuinely expressive. Parallel stages, matrix builds, conditional logic, input gates, retries, and shared libraries that let a platform team package common pipeline logic and reuse it across hundreds of repositories.

5. Two decades of edge cases are already solved. Whatever weird thing you are about to hit, someone has hit it, written a blog post, and filed an issue. That accumulated knowledge has real value.

The honest part: where Jenkins hurts

A world-class post on Jenkins has to admit the pain. If you have run it for any length of time, you know most of this already.

  • Plugin sprawl. The thing that makes Jenkins flexible is also the thing that makes it fragile. Plugins have dependencies, some are poorly maintained, and an unplanned upgrade can break a pipeline in ways that are hard to trace.

  • The pet controller problem. A controller configured by hand over five years becomes a snowflake. Nobody knows exactly how it was built, nobody wants to restart it, and nobody can rebuild it.

  • Groovy surprises. Pipeline code runs through a sandboxed Groovy interpreter with some surprising limitations. Complex logic written in a Jenkinsfile gets painful quickly.

  • Security is your job. Because you self-host, patching, access control, and secrets management are yours to get right. An exposed, unpatched Jenkins is a serious liability, since it holds credentials and can deploy to production.

  • A utilitarian experience. The interface has improved over the years, but it still feels more like a workshop than a showroom.

The good news is that nearly every one of these problems is a practice problem, not a product problem. Teams that run Jenkins well follow a recognizable playbook.

The playbook: running Jenkins like a platform

1. Put everything in Git

Your pipelines belong in a Jenkinsfile. Your controller's own configuration belongs in code too, using the Jenkins Configuration as Code (JCasC) plugin. A minimal example:

jenkins:
  systemMessage: "Managed by code. Please don't click around."
  numExecutors: 0

The goal is simple: if your controller disappeared tonight, you could rebuild it from a repository tomorrow morning. If you cannot, you have a pet, not a platform.

2. Keep the controller idle

Set the controller's executors to zero. Builds run on agents, never on the machine holding your configuration and credentials. This single change improves both stability and security more than almost anything else on this list.

3. Make agents ephemeral

Long-lived agents drift. Someone installs a tool by hand, a workspace fills the disk, and builds start failing mysteriously. Instead, spin up a fresh container or pod for every build and throw it away afterward. With the Kubernetes plugin, a pipeline can declare its own environment:

pipeline {
  agent {
    kubernetes {
      yaml '''
        apiVersion: v1
        kind: Pod
        spec:
          containers:
          - name: node
            image: node:22
            command: ['sleep']
            args: ['infinity']
      '''
      defaultContainer 'node'
    }
  }
  stages {
    stage('Test') {
      steps { sh 'npm ci && npm test' }
    }
  }
}

Every build now starts from a known state, tools are versioned alongside the code that needs them, and capacity scales with demand instead of sitting idle.

4. Let the Jenkinsfile orchestrate, not compute

The most common cause of unmaintainable pipelines is business logic buried in Groovy. Keep the real work in scripts and build tools that you can run on a laptop (make, Gradle, npm scripts, shell scripts), and let the Jenkinsfile decide when and where to call them. If a developer cannot reproduce a CI step locally, debugging turns into an exercise in pushing commits and waiting.

5. Use shared libraries for the boring 80%

When fifty repositories each carry a slightly different copy of the same build logic, every fix has to be made fifty times. A shared library lets a platform team publish a vetted step once. Product teams then write something like standardServiceBuild(name: 'orders') and inherit consistent behavior, security checks, and notifications. Version the library, and let teams pin to a tag so a change does not surprise anyone.

6. Go on a plugin diet

Every plugin is a dependency you have to patch. Before adding one, ask whether a shell command or an existing plugin would do the job. Remove what you no longer use. Prefer plugins that are actively maintained and widely adopted. A leaner plugin set means smaller upgrades, smaller attack surface, and fewer 2 a.m. surprises.

7. Stay on LTS and upgrade on a schedule

The LTS line exists precisely so that production teams have a steadier target. Pick it, then upgrade routinely (monthly or quarterly) rather than once every three years in a panic. Test upgrades in a staging controller built from the same JCasC configuration as production. Small, frequent upgrades are boring. Large, rare ones are outages.

8. Treat security as a first-class feature

A short checklist:

  • Enforce authentication and role-based access control. Never leave a controller open.

  • Store secrets in the Jenkins credentials store or, better, an external secrets manager, and inject them with withCredentials so they are masked in logs.

  • Keep pipeline script approvals tight and review requests rather than rubber-stamping them.

  • Put the controller behind HTTPS and a reverse proxy, and limit who can reach it on the network.

  • Patch promptly when security advisories are published.

withCredentials([string(credentialsId: 'deploy-token', variable: 'TOKEN')]) {
  sh './deploy.sh'   // TOKEN is available, and masked in the console log
}

9. Back up and observe

Back up the controller's home directory and test the restore at least once. Watch queue length, agent availability, and build duration trends. A slowly growing queue is the earliest warning that you need more agent capacity, and a creeping build time is usually a sign that caching or parallelism needs attention.

Designing pipelines that stay fast

A few habits separate pipelines people love from pipelines people tolerate:

  • Fail fast. Run the cheapest, most likely-to-fail checks (lint, compile, unit tests) first.

  • Parallelize independent work. Tests, static analysis, and security scans rarely need to wait on each other.

  • Cache what is expensive. Dependency downloads and build artifacts should not be rebuilt from scratch every time.

  • Always set timeouts. A hung build that holds an agent for six hours costs more than the timeout ever will.

  • Make steps idempotent. Re-running a failed deploy should be safe.

  • Keep feedback under ten minutes for the common path. Beyond that, developers stop waiting and start context-switching.

Jenkins versus the alternatives

The honest answer to "should we use Jenkins?" is "it depends on what you are building and who is going to run it."

Jenkins is a strong choice when:

  • You need to self-host for compliance, data-residency, or air-gapped reasons.

  • Your workloads are unusual: specialized hardware, Windows and macOS fleets, large monorepos, long-running builds, or legacy systems.

  • You have a platform team that can own it and amortize the effort across many product teams.

  • You already have significant investment in Jenkins pipelines and shared libraries.

Something else may be better when:

  • You are a small team on GitHub or GitLab with standard workloads. The CI built into those platforms is simpler, and there is nothing to host or patch.

  • You have no one willing to own the care and feeding of a self-hosted system.

  • You want pipelines tightly integrated with your code host's pull request, security, and permission models out of the box.

There is no shame in either answer. The worst outcome is choosing a tool by fashion and then discovering it does not fit your constraints.

Inherited a messy Jenkins? A 30-day plan

If you have just been handed a controller with a decade of history, do not start by rewriting everything. Start by making it safe, then make it reproducible.

  • Week 1: Inventory and stabilize. List jobs, plugins, agents, and credentials. Take a full backup and prove you can restore it. Identify what is actually still used.

  • Week 2: Patch and isolate. Upgrade to a current LTS in a staging copy first. Set the controller to zero executors and move builds onto agents. Lock down access.

  • Week 3: Capture the config. Export the controller's settings into JCasC and commit it. Remove unused plugins and dead jobs.

  • Week 4: Migrate the important jobs. Convert the most critical freestyle jobs into Jenkinsfiles that live with their code. Extract the repeated parts into a shared library.

At the end of the month you will not have a perfect system, but you will have a controller you can rebuild, upgrade, and reason about. That is the difference between a liability and a platform.

The bottom line

Jenkins is not the newest tool, and it does not try to be. It is a workhorse: endlessly adaptable, proudly self-hosted, and capable of automating nearly anything you can describe. Its reputation for pain comes mostly from years of unmanaged accumulation, not from anything inherent in the design.

Treat it as code, keep the controller light, make agents disposable, upgrade on a schedule, and guard the keys. Do that, and Jenkins stops being the thing everyone is afraid to touch and goes back to being what it was always meant to be: the quiet machinery that turns commits into running software.

The tools will keep changing. The need to build, test, and ship reliably will not. Jenkins has been answering that need for two decades, and with good practices it will keep answering it for a while yet.

Comments (0)

Join the discussion by logging into your account.

No comments yet. Be the first to comment!

Arpan Singh

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Arpan Singh's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.