Anyone who has spent more than a few weeks running production workloads on Kubernetes eventually hits the same wall: manual deployments don’t scale, and neither does manual patience. Jenkins paired with Kubernetes solves this by giving you a build system that can scale its own workers on demand, deploy automatically, and roll back the moment something looks wrong. This guide walks through the whole setup, from the underlying architecture to a working pipeline you can copy into your own cluster today.
Why Jenkins on Kubernetes Makes Sense
Jenkins has been the default CI server for over a decade, and Kubernetes has become the default runtime for over a decade’s worth of microservices. Running Jenkins on Kubernetes (rather than next to it) means:
- Build agents are ephemeral pods, spun up per job and destroyed afterward — no more “works on this one Jenkins agent” bugs.
- Jenkins itself can be scaled, backed up, and rescheduled like any other workload.
- Deployments to the cluster happen via the same API Jenkins already lives inside, simplifying RBAC and networking.
This is different from a classic Jenkins install on a static VM, where agents are long-lived machines that slowly accumulate drift.
Kubernetes Architecture Primer
Before wiring up Jenkins, it helps to be precise about what Kubernetes actually does:
- Control plane: the API server, scheduler, controller manager, and etcd. This is the brain — it stores desired state and reconciles the cluster toward it.
- Nodes: the workers, each running a kubelet, a container runtime (containerd in most modern clusters), and kube-proxy.
- Pods: the smallest deployable unit — one or more containers sharing network and storage.
- Controllers: Deployments, StatefulSets, DaemonSets — objects that manage Pods declaratively.
Jenkins on Kubernetes uses the Kubernetes plugin, which talks to the API server to create Pods as build agents on demand, then deletes them when the job finishes.
Installing Jenkins on Kubernetes
The fastest reliable path is Helm.
helm repo add jenkins https://charts.jenkins.io
helm repo update
kubectl create namespace jenkins
helm install jenkins jenkins/jenkins \
--namespace jenkins \
--set controller.serviceType=LoadBalancer \
--set persistence.enabled=true \
--set persistence.size=10Gi
Check the rollout:
kubectl -n jenkins rollout status statefulset/jenkins
Output:
Waiting for 1 pods to be ready...
statefulset rolling update complete 1 pods at revision jenkins-6d6f9c9b7f...
Retrieve the admin password and service address:
kubectl -n jenkins exec -it jenkins-0 -- \
cat /run/secrets/additional/chart-admin-password
kubectl -n jenkins get svc jenkins
Configuring the Kubernetes Plugin
Once logged in, under Manage Jenkins → Clouds, add a Kubernetes cloud. If Jenkins is running inside the cluster, it can usually auto-detect the API server URL (https://kubernetes.default.svc) and use its own ServiceAccount token for authentication — no extra credentials needed.
A minimal Pod template YAML for a build agent:
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins: agent
spec:
containers:
- name: jnlp
image: jenkins/inbound-agent:latest
- name: docker
image: docker:24-cli
command: ["cat"]
tty: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
RBAC for Jenkins
Jenkins needs permission to create and delete Pods in its namespace. A tightly scoped Role beats a cluster-wide binding:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: jenkins-agent-role
namespace: jenkins
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/log"]
verbs: ["create", "delete", "get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jenkins-agent-binding
namespace: jenkins
subjects:
- kind: ServiceAccount
name: jenkins
namespace: jenkins
roleRef:
kind: Role
name: jenkins-agent-role
apiGroup: rbac.authorization.k8s.io
Apply it:
kubectl apply -f jenkins-rbac.yaml
If Jenkins needs to deploy into other namespaces, create matching Roles/RoleBindings there rather than granting a ClusterRole across the whole cluster — least privilege matters here just as much as anywhere else in the security model.
Writing the Pipeline
Here’s a Jenkinsfile that builds a Docker image, pushes it, and deploys via kubectl:
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: docker
image: docker:24-cli
command: ['cat']
tty: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
- name: kubectl
image: bitnami/kubectl:latest
command: ['cat']
tty: true
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
"""
}
}
environment {
IMAGE = "registry.example.com/myapp:${env.BUILD_NUMBER}"
}
stages {
stage('Build') {
steps {
container('docker') {
sh "docker build -t $IMAGE ."
sh "docker push $IMAGE"
}
}
}
stage('Deploy') {
steps {
container('kubectl') {
sh "kubectl set image deployment/myapp myapp=$IMAGE -n production"
sh "kubectl rollout status deployment/myapp -n production"
}
}
}
}
}
The Deployment Being Updated
For context, a typical target Deployment looks like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 4
selector:
matchLabels:
app: myapp
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: registry.example.com/myapp:latest
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
The RollingUpdate strategy combined with a readiness probe is what makes Jenkins-triggered deploys safe — Kubernetes won’t route traffic to a new Pod until it reports healthy, and it won’t tear down more old Pods than maxUnavailable allows.
Monitoring and Troubleshooting the Pipeline
Common failure points and how to check them:
# Agent pod stuck pending — check events
kubectl -n jenkins describe pod <agent-pod-name>
# Deployment stuck mid-rollout
kubectl -n production rollout status deployment/myapp
kubectl -n production describe deployment myapp
# Roll back a bad deploy
kubectl -n production rollout undo deployment/myapp
If agent pods stay Pending, it’s almost always resource requests exceeding node capacity, or a missing image pull secret. If a rollout hangs, check the readiness probe path is actually correct — a wrong probe path is the single most common cause of pipelines that “hang forever” on deploy.
High Availability for Jenkins Itself
Jenkins’ own controller is not naturally horizontally scalable (its state is file-based), so HA usually means:
- Persistent volume backed by durable storage (not
emptyDir). - Regular backups of
JENKINS_HOMEvia a CronJob. - A cold-standby replica strategy rather than active-active.
apiVersion: batch/v1
kind: CronJob
metadata:
name: jenkins-backup
namespace: jenkins
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: bitnami/kubectl
command: ["/bin/sh", "-c", "tar czf /backup/jenkins-$(date +%F).tar.gz /var/jenkins_home"]
volumeMounts:
- name: jenkins-home
mountPath: /var/jenkins_home
- name: backup
mountPath: /backup
restartPolicy: OnFailure
volumes:
- name: jenkins-home
persistentVolumeClaim:
claimName: jenkins
- name: backup
persistentVolumeClaim:
claimName: jenkins-backup
Multibranch Pipelines for Multiple Environments
A single Jenkinsfile per repository quickly becomes limiting once a team is running feature branches, staging, and production off the same codebase. Multibranch Pipeline jobs solve this by automatically discovering branches and running the Jenkinsfile found in each one, with per-branch logic handled inside the pipeline itself:
pipeline {
agent { kubernetes { yaml podTemplateYaml } }
stages {
stage('Deploy') {
steps {
script {
def targetNamespace = env.BRANCH_NAME == 'main' ? 'production' : 'staging'
container('kubectl') {
sh "kubectl set image deployment/myapp myapp=$IMAGE -n ${targetNamespace}"
}
}
}
}
}
}
Configuring this in Jenkins (New Item → Multibranch Pipeline) means every branch pushed to the repository gets its own build automatically, without manually creating a Jenkins job per branch — a meaningful reduction in operational overhead once a team has more than a couple of active branches at any given time.
Shared Libraries for Reusable Pipeline Logic
Once more than one or two services adopt this pattern, copy-pasting the same Jenkinsfile across repositories becomes its own maintenance burden. Jenkins Shared Libraries let common logic live in one place:
// vars/deployToKubernetes.groovy
def call(String image, String namespace) {
container('kubectl') {
sh "kubectl set image deployment/myapp myapp=${image} -n ${namespace}"
sh "kubectl rollout status deployment/myapp -n ${namespace}"
}
}
Referenced from any project’s Jenkinsfile:
@Library('shared-pipeline-library') _
pipeline {
agent { kubernetes { yaml podTemplateYaml } }
stages {
stage('Deploy') {
steps {
deployToKubernetes("$IMAGE", "production")
}
}
}
}
This is the same principle Helm charts apply to Kubernetes manifests — extract the repeated pattern once, parameterize it, and let every consuming project stay short and focused on what’s actually specific to it.
Securing Credentials in Jenkins
Credentials (registry passwords, kubeconfig files, API tokens) should never be hardcoded into a Jenkinsfile. Jenkins’ built-in credentials store, combined with the Kubernetes Credentials Provider plugin, lets pipelines reference a Kubernetes Secret directly instead:
pipeline {
agent { kubernetes { yaml podTemplateYaml } }
environment {
REGISTRY_CREDS = credentials('registry-credentials')
}
stages {
stage('Push') {
steps {
container('docker') {
sh "docker login registry.example.com -u $REGISTRY_CREDS_USR -p $REGISTRY_CREDS_PSW"
}
}
}
}
}
Behind the scenes, this can be backed by a real Kubernetes Secret rather than credentials stored only inside Jenkins’ own database, keeping secret material consistent with how the rest of the cluster manages sensitive configuration.
Common Mistakes
- Giving the Jenkins ServiceAccount a ClusterRole with
cluster-admin“just to get it working” — this is the most common security regret teams have six months later. - Using
latesttags in production Deployments, which defeats rollback entirely. - Not setting resource requests on agent Pods, causing noisy-neighbor scheduling problems.
- Skipping readiness probes, so Jenkins reports “deployed” while the app is still crash-looping.
Summary
Jenkins on Kubernetes turns CI/CD from a set of scripts glued to a static server into a cluster-native workflow: ephemeral agents, declarative RBAC, and deploys that go through the same rolling-update machinery as any other change to the cluster. The setup cost is front-loaded — Helm install, RBAC, Pod templates — but it pays back every time a build agent disappears cleanly instead of leaking disk space for six months.
References
- Kubernetes documentation: https://kubernetes.io/docs/home/
- Jenkins Kubernetes plugin: https://plugins.jenkins.io/kubernetes/
- Jenkins Helm chart: https://github.com/jenkinsci/helm-charts
- CNCF landscape: https://landscape.cncf.io/