# Jenkins, From Zero

> The CI server that still runs much of enterprise: the Jenkinsfile pipeline-as-code, stages and steps, agents, and the plugin ecosystem for better and worse.


---

# Jenkins, From Zero

You have probably inherited a Jenkins server. Nobody chooses Jenkins fresh anymore, but somebody before you did, and now a wall of blue and red orbs decides whether your code ships. The XML config is a mystery, the plugins are a graveyard, and the one person who understood it left. This guide hands you the mental model so the box stops being a black box.

## How to read this

Read the phases in order. Phase 1 is the model: what Jenkins actually is, why it exists, and the controller/agent shape that explains everything else. Phase 2 is the Jenkinsfile you will read and write every day. Phase 3 is the production reality: plugins, credentials, and the reasons teams both depend on it and curse it. Each phase has runnable-looking examples and a short quiz to check yourself.

## The phases

1. [The mental model: what Jenkins is and why it won't die](01-the-mental-model.md)
2. [The Jenkinsfile: pipeline, agent, stages, steps](02-the-jenkinsfile.md)
3. [Production reality: plugins, credentials, and the tradeoffs](03-production-reality.md)


---

# The mental model: what Jenkins is and why it won't die

Here is the reality you are probably standing in. There is a server somewhere with a name like `ci-prod-01`. It has a web UI from a design era you can date by the gradients. When you push code, something on that server runs your tests and tells you green or red. When it breaks, you open it up and find forty installed plugins, an `Execute shell` box with bash glued into a text field, and zero idea how any of it got there.

That feeling, that the box is unknowable, comes from one missing idea. Once you have the idea, the whole thing collapses into something simple. So let's build the idea first.

## What Jenkins actually is

Strip away the plugins and the UI and Jenkins is one sentence: **a long-running server that watches for events and runs jobs in response.**

That's it. An event is usually "someone pushed to Git" or "it's 2am" or "a human clicked a button." A job is a sequence of shell commands you want run reliably, on a clean machine, every time, with the output recorded. Jenkins sits there forever, waiting, and when the trigger fires it does the work and keeps a permanent record of what happened.

If you have read [/guides/what-cicd-does](/guides/what-cicd-does), this is the CI engine that idea describes, made concrete. Jenkins was one of the first tools to make "run my tests automatically on every commit" a normal thing teams did, and it predates almost every competitor you have heard of.

That history is the answer to "why won't it die." Jenkins is open source, runs on a box you control, and over nearly two decades grew a plugin for literally everything. A bank, a hospital, a defense contractor, an old factory's IT department: anyone who cannot or will not send their source code to a cloud SaaS has Jenkins. It is the old guard, and the old guard owns a lot of ground.

## The controller and the agents

Here is the single most important picture in Jenkins. Get this and the rest follows.

```text
        ┌─────────────────┐
        │   Controller    │  the brain: web UI, schedules,
        │  (the Jenkins   │  config, plugins, the record
        │     server)     │  of every build
        └────────┬────────┘
                 │ hands out work
        ┌────────┼────────┐
        ▼        ▼        ▼
    ┌───────┐┌───────┐┌───────┐
    │Agent 1││Agent 2││Agent 3│  the muscle: where your
    │linux  ││linux  ││windows│  build commands actually run
    └───────┘└───────┘└───────┘
```

*What just happened:* the **controller** is the Jenkins server itself, the part with the web UI and all the configuration. It does not, as a rule, run your build commands. It decides *what* should run and *when*, then hands the actual work to an **agent** (older docs and plugins call it a "node" or, in language now being retired, a "slave"). Agents are separate machines, or containers, that connect back to the controller and wait for jobs.

Why split it? Three reasons that all matter in production. First, **isolation**: a build that eats all the memory or runs hostile code shouldn't take down the brain that everyone depends on. Second, **scale**: ten agents run ten builds at once; the controller alone could not. Third, **environments**: you need a Windows agent to build a Windows app and a Linux agent for your containers, so the controller farms each job to a machine that fits.

> A small team often runs builds right on the controller to start, with no separate agents. That works, and it's how many Jenkins instances begin. The moment two builds need different environments, or one bad build risks the server, you reach for agents. Knowing the split exists is what matters.

## Jobs, and the leap to pipelines

The unit of work is a **job** (the UI also calls it a "project"). Historically you built jobs by clicking through the web UI: add a build step here, paste shell there, tick a checkbox for this plugin. These are called **Freestyle** jobs, and you will still meet them on older servers.

Freestyle jobs have a fatal flaw: the definition lives *only* inside Jenkins, as config buried in the controller's files. It isn't in your Git repo. You can't review it, diff it, or roll it back with your code. When the server dies, the job dies with it, and the person who configured it has long since left.

The fix, and the thing this guide is really about, is the **Pipeline**: the job's entire definition written as code, in a file named `Jenkinsfile`, checked into your repository right next to the code it builds.

```text
your-repo/
├── src/
├── tests/
└── Jenkinsfile   ← the build is now code, versioned with everything else
```

*What just happened:* the build process became pipeline-as-code. Now the recipe travels with the project. A new hire reads the `Jenkinsfile` to understand the build. A code review covers the pipeline change too. Revert the commit and you revert the build. This single shift, from clicking in a UI to committing a file, is the difference between a Jenkins you fear and one you can reason about.

For builders: if you have used GitHub Actions (see [/guides/your-first-pipeline-github-actions](/guides/your-first-pipeline-github-actions)), the `Jenkinsfile` is the same instinct as a workflow YAML file. Jenkins got there first; the syntax is different, but "your CI config is code in your repo" is the shared idea.

## So what is Jenkins, in one breath

A server (the controller) watches for triggers, and when one fires it hands a job to a worker (an agent), which runs the steps you defined, ideally as code in a `Jenkinsfile`, and records the result forever. Everything else, every plugin, every screen, every gotcha in Phase 3, hangs off that skeleton.

```quiz
[
  {
    "q": "In the controller/agent model, where do your actual build commands normally run?",
    "choices": ["On the controller, always", "On an agent (node)", "In the web browser", "Inside the Git server"],
    "answer": 1,
    "explain": "The controller is the brain that schedules and records; agents are the workers that run the build steps. Small setups may run on the controller, but the intended split puts work on agents."
  },
  {
    "q": "What is the main advantage of a Jenkinsfile over a Freestyle job configured in the UI?",
    "choices": ["It runs faster", "It needs fewer plugins", "The build definition is code, versioned in your repo and reviewable", "It uses less memory on the controller"],
    "answer": 2,
    "explain": "A Jenkinsfile makes the build pipeline-as-code: it lives in Git next to your code, so it can be diffed, reviewed, and rolled back. Freestyle config lives only inside the controller."
  },
  {
    "q": "Why does Jenkins remain common in enterprise despite newer tools?",
    "choices": ["It is the fastest CI tool available", "It is self-hosted and open source, so code never leaves machines you control", "It requires no configuration", "It has no plugins to maintain"],
    "answer": 1,
    "explain": "Organizations that cannot send source to cloud SaaS (banks, hospitals, defense) run Jenkins on their own infrastructure. Self-hosting plus a vast plugin ecosystem keeps it entrenched."
  }
]
```


---

# The Jenkinsfile: pipeline, agent, stages, steps

You know the model now: controller hands work to an agent, and the work is defined as code. This phase is that code. By the end you will be able to read any `Jenkinsfile` on your server and write one that builds and tests a real project.

A warning that will save you an afternoon: there are **two** pipeline syntaxes, Declarative and Scripted. We teach **Declarative**, the one with the `pipeline { }` block. It is the modern default, it gives clear errors, and it is what you should write. Scripted (a raw Groovy script) is the older style; you'll meet it on legacy servers, but don't start there.

## The smallest pipeline that does something

Here is a complete, valid `Jenkinsfile`. Read it top to bottom before the breakdown.

```groovy
pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                echo 'Compiling the project...'
                sh 'make build'
            }
        }
        stage('Test') {
            steps {
                echo 'Running the test suite...'
                sh 'make test'
            }
        }
    }
}
```

*What just happened:* this defines a pipeline with two stages. On the next push, the controller picks an available agent, checks out your code there, runs `make build`, then `make test`. If either `sh` command exits non-zero, the build goes red and stops. The whole recipe is four keywords deep, and those four keywords are the entire skeleton of Declarative Jenkins.

## The four keywords that are everything

Almost every `Jenkinsfile` you read is some arrangement of these. Learn them and the rest is detail.

**`pipeline`** is the outermost block. Everything lives inside it. There is exactly one per file.

**`agent`** answers "where does this run?" Remember from Phase 1 that agents are the workers. `agent any` means "any free agent will do." You can also pin to a specific kind of machine:

```groovy
pipeline {
    agent {
        label 'linux && docker'
    }
    // ...
}
```

*What just happened:* this tells the controller to only schedule this build on an agent tagged with both `linux` and `docker` labels. This is how you guarantee a Windows build lands on a Windows machine, or a Docker build lands somewhere Docker is installed. The `agent` can sit at the top (applies to the whole pipeline) or inside a single `stage` (applies to that stage only).

**`stages`** holds the ordered list of phases of your build. It's the plural container. Inside it are individual **`stage`** blocks, each with a name you choose. Those names are exactly what you see as boxes in the Jenkins UI, the row of blue and red orbs, so name them like a human reading a status board: `Build`, `Test`, `Deploy to Staging`.

**`steps`** is where actual commands live, inside each stage. The two steps you will use most:

- `sh 'some command'` runs a shell command on a Unix agent. On Windows agents you use `bat 'some command'` instead.
- `echo 'message'` prints to the build log.

```text
pipeline ─── the whole thing (one per file)
  └─ agent ──── where it runs
  └─ stages ─── the ordered list
       └─ stage('Build') ─── one named phase (a box in the UI)
            └─ steps ─── the commands
                 └─ sh 'make build'
```

*What just happened:* this is the nesting, drawn out. `pipeline` contains `agent` and `stages`; `stages` contains `stage`s; each `stage` contains `steps`; each step is a command. If you ever get a confusing syntax error, it's almost always a block at the wrong level of this tree.

## Environment, parameters, and `post`: the everyday extras

Three more blocks turn the toy above into something you'd actually run.

**`environment`** sets variables available to every step:

```groovy
pipeline {
    agent any
    environment {
        APP_ENV = 'staging'
        REGION  = 'us-east-1'
    }
    stages {
        stage('Deploy') {
            steps {
                sh 'echo Deploying to $APP_ENV in $REGION'
            }
        }
    }
}
```

*What just happened:* `APP_ENV` and `REGION` became shell environment variables that every `sh` step can read with `$APP_ENV`. This is the clean way to avoid hardcoding the same string in five places. (Secrets are a special case with their own handling, covered in Phase 3, never paste a password here.)

**`post`** runs *after* the stages finish, branching on the outcome. This is the block that emails the team or cleans up:

```groovy
pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                sh 'make test'
            }
        }
    }
    post {
        success {
            echo 'All green. Build artifact is good.'
        }
        failure {
            echo 'Build failed. Notifying the team.'
        }
        always {
            echo 'Cleaning up the workspace.'
            sh 'make clean'
        }
    }
}
```

*What just happened:* after the stages run, exactly one of `success` or `failure` fires depending on the result, and `always` fires no matter what. `post` is where you put the "whatever happens, do this" logic, notifications, cleanup, publishing test reports, so it doesn't clutter your stages. Other conditions exist too, like `unstable` and `aborted`.

## A pipeline that looks like a real one

Putting it together, here is a `Jenkinsfile` shaped like one you'd actually inherit:

```groovy
pipeline {
    agent { label 'linux' }

    environment {
        IMAGE = 'myapp'
    }

    stages {
        stage('Build') {
            steps {
                sh 'docker build -t $IMAGE:$BUILD_NUMBER .'
            }
        }
        stage('Test') {
            steps {
                sh 'docker run --rm $IMAGE:$BUILD_NUMBER make test'
            }
        }
        stage('Push') {
            when {
                branch 'main'
            }
            steps {
                sh 'docker push $IMAGE:$BUILD_NUMBER'
            }
        }
    }

    post {
        failure {
            echo 'Pipeline failed - see the stage view above.'
        }
    }
}
```

*What just happened:* three stages build an image, test inside it, and push it, but the `Push` stage has a `when { branch 'main' }` guard, so it only runs on the `main` branch and is skipped on feature branches. `$BUILD_NUMBER` is one of many variables Jenkins injects automatically (it's the incrementing build counter). This is the everyday core of Jenkins: a handful of stages, a `when` guard or two, and a `post` block to catch failures.

In the wild: most teams keep one `Jenkinsfile` per repo at the root, and Jenkins is configured with a "Multibranch Pipeline" job that automatically builds every branch and pull request that contains one. You write the file; Jenkins finds it.

```quiz
[
  {
    "q": "In a Declarative pipeline, what does the `steps` block contain?",
    "choices": ["The list of agents to use", "The actual commands to run, like `sh` and `echo`", "The names of all stages", "Post-build notifications"],
    "answer": 1,
    "explain": "`steps` lives inside each `stage` and holds the real commands - `sh` for shell on Unix agents, `bat` on Windows, `echo` for log output."
  },
  {
    "q": "When does the `post { failure { ... } }` block run?",
    "choices": ["Before any stage starts", "After the stages finish, only if the build failed", "On every push regardless of result", "Only when manually triggered"],
    "answer": 1,
    "explain": "`post` runs after stages complete. Its `failure` condition fires only when the build failed; `success` fires on success, and `always` fires either way."
  },
  {
    "q": "What does `agent { label 'linux && docker' }` do?",
    "choices": ["Installs Docker on the agent", "Runs the build twice, once per label", "Schedules the build only on an agent tagged with both `linux` and `docker`", "Forces the build to run on the controller"],
    "answer": 2,
    "explain": "Labels let the controller route a job to a matching agent. Requiring both labels guarantees the build lands on a Linux machine that has Docker available."
  }
]
```


---

# Production reality: plugins, credentials, and the tradeoffs

You can read and write a `Jenkinsfile` now. This phase is about everything the happy path doesn't show you: where the power comes from, where the pain comes from, how to handle secrets without leaking them, and the straight answer to "should we even be on Jenkins."

## Plugins: the superpower and the curse

Almost nothing in Jenkins is built in. The ability to talk to Git, to Docker, to Slack, to a cloud provider, to publish a coverage report, every one of those is a **plugin**. There are thousands. This is genuinely why Jenkins can do anything: if a tool exists, someone wrote a Jenkins plugin for it.

It is also the single largest source of Jenkins pain, for reasons worth naming plainly:

- **Plugins are third-party code with full reach into your server.** A plugin can read every credential and run on your controller. The blast radius of a bad one is the whole instance.
- **They have their own versions and their own bugs.** A core upgrade can break a plugin; a plugin upgrade can break a build. There is no single vendor who owns the whole stack working together.
- **They rot.** The maintainer moves on, the plugin stops getting updates, and one day a security advisory lands on something you can't easily replace.
- **They pile up.** Every "let me install this to try it" leaves a plugin behind. Inherited servers commonly carry dozens nobody can account for.

```text
Manage Jenkins → Plugins → Installed

  ✓ Git plugin                    5.x      (in use)
  ✓ Pipeline                      core     (in use)
  ✓ Docker Pipeline               (in use)
  ⚠ Some Old Reporter             no update in years
  ⚠ Mystery Integration          nobody remembers installing this
```

*What just happened:* this is the screen where reality lives. The discipline that keeps a Jenkins server healthy is boring and constant: install only what a pipeline actually needs, keep what you have updated, and uninstall what nothing uses. Treat the plugin list like a dependency file, because that is exactly what it is.

> A practical habit: before installing a plugin, ask whether a plain `sh` step calling a CLI would do the same job. A shell call to `curl` or the `aws` CLI is often more transparent and more maintainable than a plugin that wraps the same thing behind a UI.

## Credentials: never paste a secret in the Jenkinsfile

Your pipeline needs secrets, a registry password, a deploy key, an API token. The `Jenkinsfile` is in Git, readable by everyone with repo access, so a secret in there is a secret published to your whole team and its history forever.

Jenkins solves this with its **Credentials** store. You add a secret once, through the UI (or configuration-as-code), under `Manage Jenkins → Credentials`. Each gets an ID. Your pipeline references the ID, never the value.

```groovy
pipeline {
    agent any
    environment {
        // Pulls the secret named 'registry-token' from the credential store.
        // The Jenkinsfile holds only the ID, never the value.
        REGISTRY_TOKEN = credentials('registry-token')
    }
    stages {
        stage('Push') {
            steps {
                sh 'docker login -u ci --password-stdin <<< "$REGISTRY_TOKEN"'
            }
        }
    }
}
```

*What just happened:* `credentials('registry-token')` looked up the stored secret by ID and bound its value to `REGISTRY_TOKEN` only for the duration of the build. The actual secret never appears in your repo. Jenkins also **masks** it in the build log, so if it accidentally gets echoed, it shows as `****` instead of the real value. For credentials scoped to a single block, the `withCredentials` step does the same thing with a tighter lifetime.

A blunt rule that has saved many incidents: if you ever see a real password, token, or key typed directly into a `Jenkinsfile` or an `environment` value, treat it as a leak. Move it to the credential store and rotate it.

## The real tradeoffs: why teams love it and hate it

You will form an opinion about Jenkins. Here is a fair one to start from.

**Why teams keep it:**

- **You own it.** It runs on your hardware, your network, your rules. For regulated industries where code cannot leave the building, this is not a preference, it's a requirement.
- **It does everything.** Between plugins and raw `sh` steps, there is no build, deploy, or automation task it can't be bent to.
- **No usage bill.** A cloud CI service charges per build minute. A Jenkins box you already run has no per-minute meter; your cost is the machine and the maintenance.

**Why teams curse it:**

- **Somebody has to run it.** Patching, upgrades, agent management, plugin hygiene, backups, that's a real, ongoing job. Cloud CI hands that work to the vendor.
- **The maintenance burden is exactly the cost the "no bill" point hides.** You pay in engineer-hours instead of dollars.
- **The plugin fragility above is constant**, and the UI and defaults show their age.

Where it fits today, plainly: if you're a small team or a greenfield project with code that's allowed in the cloud, a hosted CI like GitHub Actions (see [/guides/your-first-pipeline-github-actions](/guides/your-first-pipeline-github-actions)) will cost you far less pain to start. Jenkins earns its place when you need self-hosting, when you're already deep in it, or when you need something no SaaS offers. It is not the exciting choice, and it is not going away, because the ground it owns, the enterprise that must self-host, isn't going anywhere either.

## What to do when you inherit one

A short, practical playbook, because this is the most likely way you'll meet Jenkins:

1. **Find the Jenkinsfiles.** Read them. They are the actual source of truth for what builds. Verify with: a stage view in the UI maps directly to the `stage` blocks you read.
2. **Audit the plugins.** `Manage Jenkins → Plugins`. Note anything unmaintained or unrecognized. Verify with: cross-reference each against what your pipelines actually call.
3. **Check credentials hygiene.** Confirm secrets live in the credential store, not pasted into Jenkinsfiles. Verify with: grep your repos for anything that looks like a key in a Jenkinsfile.
4. **Confirm there's a backup.** The controller's configuration is the crown jewels. Verify with: locate where `JENKINS_HOME` is backed up, and when it last ran.

For builders: the long-term move once you understand an inherited instance is to push everything *toward* code, Jenkinsfiles in repos, and configuration-as-code for the controller itself, so the server becomes reproducible instead of a hand-tuned pet nobody dares touch.

```quiz
[
  {
    "q": "What is the biggest risk created by Jenkins's plugin ecosystem?",
    "choices": ["Plugins make builds slower", "Plugins are third-party code with full server reach, can rot, and conflict across versions", "Plugins cost money per install", "Plugins can only be installed on agents"],
    "answer": 1,
    "explain": "Plugins are powerful because they integrate everything, but each is third-party code that can read credentials, break on upgrades, lose its maintainer, and accumulate unmanaged. Treat the plugin list like a dependency file."
  },
  {
    "q": "How should a secret like a registry token be handled in a pipeline?",
    "choices": ["Paste it into the environment block as plain text", "Hardcode it in a stage's sh step", "Store it in the Jenkins credential store and reference it by ID with `credentials()`", "Commit it to the repo in a separate file"],
    "answer": 2,
    "explain": "The Jenkinsfile lives in Git, so any secret in it is published. The credential store holds the value; the pipeline references only the ID, and Jenkins masks the value in logs."
  },
  {
    "q": "When does Jenkins most clearly earn its place over a hosted CI like GitHub Actions today?",
    "choices": ["For brand-new small projects with cloud-friendly code", "When you need self-hosting, are already deep in it, or need something no SaaS offers", "When you want zero maintenance work", "When you want the most modern UI"],
    "answer": 1,
    "explain": "Hosted CI is usually less painful to start for greenfield, cloud-allowed projects. Jenkins's strength is self-hosting and total flexibility - exactly what regulated, self-hosted, or already-invested teams need."
  }
]
```
