Jenkins
The self-hosted automation server: pipelines as code in a Jenkinsfile, run on your own agents.
In 30 seconds
Jenkins is an open-source automation server you install and run yourself. A Jenkinsfile in the repository describes the pipeline in a Groovy-based language: stages like build, test and deploy, run on worker machines called agents. Its huge plugin ecosystem connects it to almost any tool, which is why it is still common in enterprises, but you are responsible for upgrades, security and scaling.
Not in my daily stack — I know the concepts and would learn the details when a project needs it.
Why it matters
- It runs entirely on your infrastructure, so it fits air-gapped networks, regulated industries and builds that need special hardware.
- Around two thousand plugins integrate it with nearly every SCM, cloud, test tool and chat system, so it can orchestrate unusual or legacy workflows.
- Many established companies run years of pipelines on Jenkins; understanding it is often required to maintain or migrate them.
- There is no per-minute billing: you pay for machines you control, which can be cheaper for very heavy, constant workloads.
How it works
01Controller, agents and executors
A Jenkins installation has a controller: the web UI, configuration, job scheduling and plugin host. Work should happen on agents, separate machines or containers that connect to the controller and run builds. Each agent offers a number of executors, slots that can run one build each. Labels such as linux, docker or arm64 describe agents, and a pipeline asks for a label rather than a specific machine. Agents can be permanent (a VM connected over SSH or an inbound connection) or ephemeral: the Kubernetes plugin starts a pod per build, and cloud plugins start VMs on demand and remove them afterwards. Think of the controller as a dispatcher in a taxi company and agents as the cars. A key rule: set the controller's own executors to zero, so untrusted build code never runs on the machine that holds every credential and setting.
02Installing Jenkins safely
Jenkins has a weekly release line and a Long-Term Support (LTS) line, picked from a weekly release every twelve weeks and patched for stability; production should run LTS. Current LTS releases require Java 21 or Java 25. The official jenkins/jenkins Docker image is the easiest start: pin an LTS tag with a JDK suffix, persist the jenkins_home volume (all configuration and history live there) and put it behind a reverse proxy with HTTPS. On first start, Jenkins prints an initial admin password to the log, and the setup wizard offers suggested plugins. Install only what you need, since every plugin is code with its own security record. Enable an authentication realm (LDAP, SAML or OIDC via plugins) and matrix or role-based authorization; never leave anonymous users with any write access.
# compose.yaml - Jenkins controller, data in a named volume
services:
jenkins:
image: jenkins/jenkins:lts-jdk21
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080" # expose via a reverse proxy with TLS
- "50000:50000" # inbound agents
volumes:
- jenkins_home:/var/jenkins_home
volumes:
jenkins_home:
03Declarative pipeline: a Jenkinsfile
Pipeline as code means the build is defined in a Jenkinsfile stored with the source, reviewed in pull requests and versioned like everything else. Declarative syntax is the recommended style: a fixed structure with pipeline, agent, stages and steps, plus optional options, environment, when and post sections. It is easier to read and validate than free-form Groovy. The example builds and tests a Node.js app inside a Docker container on any agent labelled docker, keeps only the last twenty builds, times out after thirty minutes, publishes JUnit test results, and always cleans the workspace. A Multibranch Pipeline job scans the repository and creates a pipeline for every branch and pull request that contains a Jenkinsfile, so new branches get CI automatically.
pipeline {
agent { docker { image 'node:24-alpine'; label 'docker' } }
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
stages {
stage('Install') { steps { sh 'npm ci' } }
stage('Test') {
steps { sh 'npm test -- --reporter=junit --outputFile=reports/junit.xml' }
post { always { junit 'reports/*.xml' } }
}
stage('Build') { steps { sh 'npm run build' } }
}
post { always { cleanWs() } }
}
04Credentials and deploy stages
Jenkins stores secrets in its credentials store, encrypted on disk, scoped globally or to a folder so teams only see their own. Pipelines access them with withCredentials or the credentials() helper in an environment block, and Jenkins masks the values in console output. Prefer folder-scoped credentials, and better still, an external secret manager such as HashiCorp Vault or a cloud secret manager through a plugin. A deploy stage usually runs only on the main branch, waits for a human with input, and uses a lock or milestone so overlapping builds cannot deploy out of order. Note the single quotes around the shell script below: Groovy then does not interpolate the secret into the command string, and the shell reads it from the environment instead, which keeps it out of logs and process listings.
stage('Deploy') {
when { branch 'main' }
steps {
input message: 'Deploy to production?', ok: 'Deploy'
withCredentials([sshUserPrivateKey(
credentialsId: 'deploy-ssh-key',
keyFileVariable: 'SSH_KEY',
usernameVariable: 'SSH_USER')]) {
sh '''
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=yes \
"[email protected]" "./deploy.sh $GIT_COMMIT"
'''
}
}
}
05Plugins and shared libraries
Most Jenkins features are plugins: Git, Pipeline, credentials, Docker, Kubernetes agents, test reports. That flexibility is also the main operational risk. Plugins depend on each other and on core versions, and an outdated one is a frequent source of security advisories. Keep the list short, update regularly in a staging Jenkins first, and read the security advisories page. For reuse across many repositories, Shared Libraries hold Groovy code in a separate Git repository: functions in vars/ become pipeline steps, so dozens of Jenkinsfiles can call one standardised buildNodeApp() instead of copying logic. Load libraries at a pinned version with @Library('[email protected]') _. Configuration as Code (the JCasC plugin) describes the controller itself, including security, agents and credentials references, in YAML, so a broken Jenkins can be rebuilt from Git rather than from memory.
Use it when
- Builds must run fully on-premises or in air-gapped networks with no external SaaS.
- You already have a large Jenkins estate and need to maintain, harden or gradually modernise it.
- Unusual orchestration: hardware-in-the-loop tests, special build machines, or integrations only Jenkins plugins provide.
- Very high, constant build volume where owning machines is cheaper than per-minute pricing.
Skip it when
- A new project on GitHub or GitLab with standard needs; the built-in CI is less work to run and secure.
- A small team with no one to own controller upgrades, plugin updates and backups.
- You want zero infrastructure; Jenkins is always something you operate.
Alternatives
- GitHub Actions
Hosted CI integrated with GitHub, no controller to maintain.
- GitLab CI/CD
Built into GitLab, strong self-managed option with runners in your network.
- Buildkite
Hosted control plane with agents on your own machines, a common Jenkins replacement.
- Tekton / Argo Workflows
Kubernetes-native pipelines for teams building their own platform.
Best practices
- Run the LTS line, test upgrades and plugin updates on a staging controller first.
- Set controller executors to zero and run all builds on agents, preferably ephemeral ones.
- Use Declarative Pipeline in a Jenkinsfile and Multibranch jobs rather than UI-configured freestyle jobs.
- Manage controller configuration with Configuration as Code and back up jenkins_home.
- Scope credentials to folders, use single-quoted shell strings for secrets, and prefer an external secret manager.
- Keep the plugin list minimal and act on Jenkins security advisories promptly.
- Put reusable logic in versioned Shared Libraries, keeping Jenkinsfiles short and declarative.
Common mistakes
- Running builds on the controller, giving any pipeline access to all secrets and configuration.
- Installing dozens of plugins nobody remembers, then being unable to upgrade core because of them.
- Interpolating secrets into double-quoted Groovy strings, which can leak them into logs and process lists.
- Exposing Jenkins directly to the internet without HTTPS, SSO and strict authorization.
- Configuring everything by clicking in the UI with no backup, so a disk failure means rebuilding from memory.
Deep dive
06How pipelines survive restarts: CPS and durability
Pipeline code does not run as normal Groovy. Jenkins transforms it into continuation-passing style (CPS) so the state of a running pipeline can be saved to disk and resumed after a controller restart. That is why long builds can survive an upgrade, and also why some normal Groovy breaks: objects that cannot be serialised, certain closures and iterators cause NotSerializableException. Methods marked @NonCPS run as plain Groovy but cannot call pipeline steps. Also remember that Groovy in the Jenkinsfile runs on the controller, not the agent; only steps like sh run on agents. Heavy parsing or loops in Groovy therefore load the controller, so move that work into shell scripts or tools executed on agents. The durability setting (for example PERFORMANCE_OPTIMIZED) trades resume guarantees for less disk I/O on busy controllers.
07Ephemeral agents on Kubernetes
The Kubernetes plugin starts a pod per build from a pod template and connects it as an agent; the pod is deleted when the build ends. Each container in the pod can hold a different toolchain, and container() switches between them within a stage. Benefits: clean workspaces every time, no snowflake build machines, and scaling with the cluster. Watch the details: set CPU and memory requests so builds do not starve each other, run builds as non-root, and avoid mounting the host Docker socket, which gives a build root on the node; use Kaniko-style or BuildKit rootless builders instead. Dependency caches disappear with the pod, so use a persistent volume, a proxy repository such as Nexus or Artifactory, or a registry-backed build cache to keep builds fast.
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:24-alpine
command: [sleep]
args: [infinity]
resources:
requests: { cpu: "500m", memory: 1Gi }
'''
defaultContainer 'node'
}
}
stages { stage('Test') { steps { sh 'npm ci && npm test' } } }
}
08Securing a Jenkins controller
Jenkins has a long list of historical vulnerabilities, mostly in plugins, and attackers actively scan for exposed instances. Defence in layers: keep core and plugins updated against the published security advisories; put the controller behind a reverse proxy with TLS and preferably a VPN or SSO; use matrix-based or role-based authorization with no anonymous access; disable the CLI over remoting and unused protocols; keep the script console to administrators only, because it runs arbitrary Groovy with full controller rights. The Script Security plugin sandboxes pipeline Groovy and requires administrator approval for dangerous methods; do not approve signatures casually. Control which repositories may define pipelines and which credentials each folder can use, because a malicious Jenkinsfile change in a pull request can print any credential the job can access. Configure trusted sources so pull requests from forks cannot change the Jenkinsfile used for their build.
Real-world scenarios
Jenkins upgrade blocked by old plugins
Problem
A controller is two years behind on core and has 140 plugins. Upgrading core breaks several plugins, and nobody knows which ones are used.
How to solve it
Back up jenkins_home and clone it into a staging controller. Export the plugin list and use usage data (the Plugin Usage plugin or grep in job configs) to find unused plugins; remove them. Upgrade Java to 21 first, then core in steps along LTS releases, updating plugins at each step with the plugin installation manager tool. Fix or replace abandoned plugins. Capture the result with Configuration as Code and a plugins.txt in Git, then build the production controller from that image. Afterwards, schedule monthly LTS updates so the gap never grows again.
Builds fail randomly on shared agents
Problem
Builds pass and fail on the same commit depending on which permanent agent runs them. Agents have different tool versions and leftover files.
How to solve it
Move builds into containers: use agent { docker { image 'node:24-alpine' } } or Kubernetes pod templates, so toolchains are defined in the Jenkinsfile and identical everywhere. Build the agent images in CI, versioned and scanned. Clean workspaces with cleanWs() in post, and stop relying on files outside the workspace. Replace permanent agents with ephemeral ones from the Kubernetes or cloud plugin. Keep a single permanent agent only where special hardware is required, and manage its configuration with Ansible so it can be rebuilt.
Migrating from Jenkins to GitHub Actions
Problem
A company moving its code to GitHub wants to retire Jenkins, but has 80 pipelines with Shared Libraries and custom plugins.
How to solve it
Inventory pipelines and group them by pattern; most will share a few shapes. Translate Shared Library functions into reusable workflows and composite actions. GitHub's Actions Importer can produce draft workflows from Jenkinsfiles to speed up the mechanical part. Recreate credentials as environment secrets or, better, OIDC trust. Migrate a pilot group first and run both systems in parallel for a few weeks, comparing results. Keep Jenkins only for jobs that need on-premises hardware, connected through self-hosted runners instead, then switch off the controller after a final backup.
Cheat sheet
- Run Jenkins LTS in Docker
docker run -d -p 127.0.0.1:8080:8080 -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk21- Initial admin password
docker exec <container> cat /var/jenkins_home/secrets/initialAdminPassword- Only run on main
when { branch 'main' }- Time out a build
options { timeout(time: 30, unit: 'MINUTES') }- Keep last 20 builds
options { buildDiscarder(logRotator(numToKeepStr: '20')) }- Use a secret text
environment { API_TOKEN = credentials('api-token') }- Load a shared library
@Library('[email protected]') _- Parallel stages
parallel { stage('Unit') {...} stage('Lint') {...} }- Install plugins in an image
jenkins-plugin-cli --plugin-file plugins.txt- Validate a Jenkinsfile
Pipeline Linter: POST $JENKINS_URL/pipeline-model-converter/validate
Questions & answers
Q1What is the difference between the controller and an agent?
The controller is the brain: it serves the web UI, stores configuration, credentials and build history, schedules builds and runs plugins. Agents are workers that connect to the controller and actually execute build steps. An agent has a number of executors, each able to run one build. Builds pick agents by label, so a job that needs Docker asks for docker rather than a hostname. The controller should not run builds itself, because build scripts are code from repositories and could read its secrets or damage its configuration. Modern setups use ephemeral agents, such as a Kubernetes pod or cloud VM per build, which are clean every time and scale with demand.
Q2Declarative or scripted pipeline?
Declarative pipeline has a fixed structure: pipeline, agent, stages, steps, post, when, options. Jenkins can validate it before running, the Blue Ocean style views understand it, and it is easier for the whole team to read. Scripted pipeline is plain Groovy inside node blocks: maximum flexibility, but easy to turn into an unreadable program with subtle CPS serialisation issues. My default is Declarative for every Jenkinsfile, with a script block only for small unavoidable logic. When real logic is needed, it belongs in a Shared Library with tests, called from a short Declarative Jenkinsfile, rather than inline Groovy in each repository.
Q3How do you handle secrets in Jenkins pipelines?
Store them in the Jenkins credentials store, scoped to the folder of the team that needs them, or fetch them from an external manager like Vault through a plugin. In pipelines, bind them only where needed with withCredentials or credentials() in an environment block, which also masks them in logs. Use single-quoted sh strings so the shell, not Groovy, expands the variable; Jenkins even warns about insecure interpolation. Never echo secrets or pass them as command arguments where they show in process lists. Restrict who can configure jobs, because anyone who can edit a pipeline can print a credential it has access to, and audit credentials usage periodically.
Q4How do you keep a Jenkins instance maintainable over years?
Treat it like any production service. Run LTS and upgrade on a schedule, rehearsing on a staging controller restored from a recent backup. Keep plugins few and current, removing unused ones. Describe the controller with Configuration as Code in Git, so it can be rebuilt; jobs come from Jenkinsfiles via Multibranch or organization folders, not the UI. Back up jenkins_home, or at least the parts not reproducible from Git. Use ephemeral agents from images built in CI, so agent tooling is versioned too. Monitor disk space, queue length and executor usage, and discard old builds and artifacts with logRotator so the disk does not fill silently.
Q5What is a Multibranch Pipeline?
A Multibranch Pipeline is a job type that points at a repository and automatically creates a sub-job for every branch, and optionally every pull request, that contains a Jenkinsfile. When a branch is created, Jenkins discovers it through a webhook or periodic scan and starts building; when the branch is deleted, the sub-job is removed. Combined with when { branch 'main' } or changeRequest() conditions, one Jenkinsfile can behave differently for feature branches, pull requests and main. An Organization Folder goes one level higher and scans a whole GitHub organization or GitLab group, creating Multibranch jobs for every repository with a Jenkinsfile.
Senior-level questions
S1When would you still choose Jenkins for a new platform today?
Rarely by default, but there are honest cases. Air-gapped or highly regulated environments where no SaaS control plane is allowed and the organization already runs Jenkins well. Complex hardware workflows, such as embedded or device testing, that depend on specific plugins or agent setups. Very large build farms where an experienced team operates Jenkins with Kubernetes agents, JCasC and Shared Libraries at a lower cost than hosted minutes. Against it: operational load, plugin security risk, Groovy-specific skills and a weaker developer experience than built-in CI. If the code is on GitLab or GitHub Enterprise Server, their CI with self-hosted runners usually gives the same on-premises control with less to maintain, so I would choose Jenkins only with a clear reason and a team to own it.
S2How do you scale Jenkins for hundreds of teams?
One giant controller becomes a single point of failure and a performance bottleneck, because Groovy pipeline logic, the UI and plugins all run there. The common pattern is several controllers, split by business unit or trust level, each managed identically with Configuration as Code and a curated plugin image, possibly with CloudBees CI or similar tooling to manage them centrally. Agents are ephemeral pods or VMs that autoscale. Shared Libraries provide standard pipeline steps, versioned and tested. Teams get folders with scoped credentials and permissions. Monitoring covers queue time, executor usage, controller heap and disk. The trade-off is fleet management overhead versus blast radius: many controllers mean more upgrades to orchestrate, but a bad plugin or runaway job affects only one part of the organization.
S3A pull request changes the Jenkinsfile to print credentials. How is that prevented?
Several controls must combine. In Multibranch configuration, set trust so that pull requests from forks, or from contributors without write access, build with the Jenkinsfile from the target branch, not their modified one. Scope credentials to folders and give pull request builds none of the production credentials at all; deploy credentials are only bound in stages that run on main. Masking helps but is easily bypassed by encoding, so it is not a control. Keep the Groovy sandbox enabled and do not approve risky script signatures. Require code review for Jenkinsfile and Shared Library changes with branch protection. For the highest-value secrets, use short-lived credentials from Vault so even a leak expires quickly. The underlying principle: anyone who can change pipeline code can use any secret that pipeline can reach.
Glossary
- Controller
- The central Jenkins server: UI, configuration, scheduling and plugins. Formerly called master.
- Agent
- A machine or container that connects to the controller and runs build steps.
- Executor
- A slot on an agent that runs one build at a time.
- Jenkinsfile
- A file in the repository that defines the pipeline as code.
- Declarative Pipeline
- The structured, recommended Jenkinsfile syntax with pipeline, agent, stages and steps blocks.
- Shared Library
- A Git repository of reusable Groovy steps loaded by many Jenkinsfiles.
- JCasC
- Configuration as Code: describes the controller's settings in YAML so it can be rebuilt from Git.
- LTS
- Long-Term Support release line, picked every twelve weeks and patched for stability.
- Multibranch Pipeline
- A job that creates a pipeline for each branch or pull request with a Jenkinsfile.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.