Jenkins: the controller, agents, Jenkinsfile and shared libraries
Jenkins still runs a huge share of company pipelines: a controller handing work to agents, shop-api's pipeline as a declarative Jenkinsfile, plugins and their risks, and shared libraries.
By the end of this lesson you'll be able to
0 of 6 completeWhy learn Jenkins
Jenkins is the open-source automation server most companies started their CI on, and many still run it: you host it yourself, it connects to any Git server, and thousands of plugins extend it. You'll meet it in banks, telecoms and any team with years of pipelines behind it. The ideas are the ones you already know; the spelling is Groovy.
The controller and agents
| Part | Does | GitHub Actions equivalent |
|---|---|---|
| Controller | UI, jobs, plugins, credentials, queue, logs | GitHub itself |
| Agent | runs builds, connected by SSH or JNLP, or created per build | a runner |
| Executor | one build slot on an agent | one job on a runner |
| Label | which agents may run a stage (linux && docker) | runs-on: labels |
Set the controller's number of executors to 0 (Manage Jenkins → Nodes → Built-In Node). A build on the
controller runs with access to JENKINS_HOME, where every job, credential and secret is stored. With the
Kubernetes plugin, each build gets a fresh pod that's deleted afterwards: the same "clean machine every time" that
GitHub-hosted runners give you.
The Jenkinsfile
A Jenkinsfile in the repository defines the pipeline as code, like .github/workflows/. Use the
declarative syntax: structured and validated. Here's shop-api's pipeline from the earlier lessons:
// Jenkinsfile pipeline { agent { docker { image 'node:22-alpine' } } // every stage runs in this container options { timeout(time: 20, unit: 'MINUTES') disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: '30')) } environment { VERSION = "1.0.${env.BUILD_NUMBER}-${env.GIT_COMMIT.take(7)}" } stages { stage('Install') { steps { sh 'npm ci' } } stage('Checks') { parallel { // lint and unit at the same time stage('Lint') { steps { sh 'npm run lint' } } stage('Unit') { steps { sh 'npm run test:coverage' } } } } stage('Integration') { steps { sh 'npm run test:integration' } } stage('Package') { steps { sh 'mkdir -p dist && cp -r src package.json dist/ && tar -czf shop-api-${VERSION}.tgz -C dist .' archiveArtifacts artifacts: 'shop-api-*.tgz', fingerprint: true } } stage('Deploy to production') { when { branch 'main' } input { message 'Deploy to production?'; ok 'Deploy' } // waits before taking an agent agent { label 'deploy' } // an agent that can reach the servers steps { withCredentials([sshUserPrivateKey(credentialsId: 'deploy-key', keyFileVariable: 'SSH_KEY')]) { sh './scripts/deploy-ssh.sh deploy@shop.example.com "$VERSION" shop-api-*.tgz' } } } } post { failure { echo "Build ${env.BUILD_NUMBER} failed: ${env.BUILD_URL}" } always { cleanWs() } } }
| Jenkinsfile | Means | Actions equivalent |
|---|---|---|
agent | where the pipeline or a stage runs | runs-on: / container: |
stages { stage {} } | ordered steps of the pipeline | jobs and steps |
parallel { } | stages at the same time | jobs without needs |
environment { } | variables for every step | env: |
when { branch 'main' } | run a stage only sometimes | if: |
input { } on a stage | pause for approval, before an agent is taken | environment reviewers |
withCredentials | a secret, only inside the block | ${{ secrets.X }} in env: |
post { always / failure } | after the stages | if: always() / failure() |
archiveArtifacts | keep files with the build | upload-artifact |
The docker agent needs the Docker Pipeline plugin and agents with Docker; cleanWs() comes from the
Workspace Cleanup plugin. Note the quotes: Groovy expands "${VERSION}" in double quotes itself, while in sh '…$VERSION…' (single quotes) the shell does, which is the safe way to pass values, for the same reason as env: in
Actions (lesson 11).
Multibranch pipelines find every branch and pull request with a Jenkinsfile and build each one, and a webhook from GitHub or GitLab triggers them on push. The job's Pipeline Syntax page generates the code for any step from a form.
Plugins: power and risk
Almost everything in Jenkins is a plugin: Git, credentials, pipelines, Docker, Kubernetes agents, Slack messages. That flexibility is also its weakest point:
- Each plugin is maintained separately, and several have had serious vulnerabilities over the years.
- Plugins depend on each other and on the Jenkins version, so upgrades can break pipelines.
- They all run inside the controller, with its access to every credential.
Install only what you use, update Jenkins and plugins regularly (test upgrades on a copy first), and remove plugins nobody needs. Follow the official security advisories (linked below). Manage the controller itself as code: the Configuration as Code plugin describes its settings in YAML, and a Dockerfile can pin the plugin list, so a broken controller can be rebuilt instead of repaired by hand.
Shared libraries
With fifty services, fifty copies of the same Jenkinsfile drift apart, just like workflow files (lesson 12). A shared library is a Git repository of pipeline code that Jenkinsfiles load:
jenkins-shared-library/ └── vars/ └── nodePipeline.groovy # becomes a step called nodePipeline
// vars/nodePipeline.groovy def call(Map config = [:]) { pipeline { agent { docker { image "node:${config.node ?: '22'}-alpine" } } stages { stage('Install') { steps { sh 'npm ci' } } stage('Lint') { steps { sh 'npm run lint' } } stage('Test') { steps { sh 'npm run test:coverage' } } } post { always { cleanWs() } } } }
// Jenkinsfile in each service @Library('company-pipelines@v1') _ nodePipeline(node: '22')
Register the library once under Manage Jenkins → System → Global Pipeline Libraries, pin it to a tag such as
@v1, and version it like any shared code.
Jenkins or GitHub Actions?
| Choose Jenkins when | Choose a hosted CI when |
|---|---|
| builds must stay inside your network, with no cloud service involved | your code is on GitHub or GitLab already |
| you need unusual hardware, OSes or tools | you'd rather not run and patch a CI server |
| years of pipelines and plugins already depend on it | you want fresh runners and a marketplace of actions |
Plenty of companies run both: Jenkins for legacy and on-premises builds, Actions for new services. The pipeline shape you've built in this course works in either.
Run shop-api's pipeline on Jenkins
Start Jenkins locally in Docker, create a Pipeline job that reads shop-api's Jenkinsfile from Git, and get Install, Checks (lint and unit in parallel), Integration and Package passing, with the package archived on the build page.
A green Jenkins build with shop-api-<version>.tgz archivedReveal one safe solution
$ docker volume create jenkins_home $ docker run -d --name jenkins -p 8080:8080 -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk21 $ docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword # paste into http://localhost:8080
docker volume create jenkins_homedockerbuild and run containersvolumemanage volumescreatecreate one
docker run -d --name jenkins -p 8080:8080 -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk21runstart a new container-drun in the background--name jenkinsname the container-p 8080:8080publish a port, host:container-v jenkins_home:/var/jenkins_homemount a volume or folder, source:path-insidejenkins/jenkins:lts-jdk21the image to start
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPasswordexecrun a command in a running containerjenkinsthe containercat /var/jenkins_home/secrets/initialAdminPasswordthe command to run inside it
Install the suggested plugins, plus Docker Pipeline if your agent has Docker. Create New Item → Pipeline,
choose Pipeline script from SCM, give it the repository URL and branch main, and save. The controller in this
container has no Docker, so for the first run change the top line to agent any and install Node on the agent, or
add a Docker-capable agent with the label docker. Build Now: the stage view shows Install, then Lint and Unit side
by side, then Integration and Package, and the build page lists the archived .tgz. Delete the Deploy stage, or
leave it: when { branch 'main' } and input stop it until you approve.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
Why should builds run on agents, not on the Jenkins controller?
Official reading for this lesson
Or get every quiz answer right and it completes itself.