Home DevOps Jenkins on Kubernetes: Helm, Pod Templates & Kaniko Gotchas
Advanced ✅ Tested on Jenkins 2.440+ | Kubernetes Plugin 1.0+ | K8s 1.28+ 11 min · 2026-07-09

Jenkins on Kubernetes: Helm, Pod Templates & Kaniko Gotchas

Production guide to Jenkins-Kubernetes integration: Helm deployment, dynamic pod templates, RBAC, Kaniko vs Docker-in-Docker, PVC caching, and debugging pod retention..

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
July 09, 2026
last updated
230
articles · all by Naren
Before you start⏱ 30 min
  • Deep devops experience in a production environment
  • Familiarity with debugging, profiling, and performance analysis
  • Understanding of distributed systems and failure modes
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use helm install jenkins jenkins/jenkins --version 5.0.8 with a custom values.yaml for production.
  • Set controller.serviceType: ClusterIP and expose via ingress for production-grade access.
  • Configure pod templates with podTemplate(yaml: '...') in pipeline for multi-container agents.
  • RBAC: create a ServiceAccount with cluster-admin role for Jenkins master, use roleRef with kind: ClusterRole.
  • Prefer Kaniko over Docker-in-Docker for building container images — no privileged mode or DinD sidecar needed.
  • Use PersistentVolumeClaim for Maven/Gradle cache: mount under /home/jenkins/.m2 with ReadWriteMany access mode.
  • Set podRetention: never() to avoid zombie pods; use resource.limits to prevent CPU/memory starvation.
  • Debug with kubectl logs -l app=jenkins --tail=50 and kubectl describe pod for pod template issues.
✦ Definition~90s read
What is Jenkins Kubernetes Integration?

Jenkins Kubernetes Integration is the ability for Jenkins master to dynamically provision build agents as Kubernetes pods. Instead of maintaining a static pool of agents (which wastes resources or causes contention), the Kubernetes plugin (org.csanchez.jenkins.plugins.kubernetes) creates a pod with one or more containers when a build is triggered.

Imagine you run a busy restaurant kitchen (Jenkins) and every time a new order comes in, you need a dedicated chef (build agent).

The pod runs the build, then terminates — all within seconds. This is not just about running Jenkins in Kubernetes (though we cover that too), but about using Kubernetes as an elastic agent backend.

At its core, the integration consists of two parts: (1) deploying Jenkins itself on Kubernetes (typically via Helm), and (2) configuring the Kubernetes plugin to define pod templates. The pod template specifies the image(s), resource requests/limits, environment variables, volumes, and labels.

When a pipeline uses agent { label 'maven' }, Jenkins looks for a pod template with that label and creates a pod. The pod's main container runs the Jenkins agent (connected via JNLP or WebSocket), and additional sidecar containers (like a Docker daemon or Kaniko) are available for the build.

This solves the classic 'agent management' problem: you no longer need to pre-provision VMs or maintain agent images. Each build gets a fresh, isolated environment. You can have different pod templates for different project types (Maven, Node.js, Go) and they scale down to zero when idle.

However, the flexibility introduces complexity: RBAC must allow Jenkins to create/delete pods, pod templates must be correctly configured, and resource limits must match your cluster capacity. Missteps here lead to the exact 3 AM incident I described.

Plain-English First

Imagine you run a busy restaurant kitchen (Jenkins) and every time a new order comes in, you need a dedicated chef (build agent). Instead of hiring a permanent chef for each order, you have a magic wand that summons a chef from a pool of temporary workers (Kubernetes pods). The chef arrives with exactly the tools they need (multi-container pod templates) and works until the order is done. If the chef takes too long or uses too many ingredients, the kitchen manager (RBAC + resource limits) steps in. Some chefs prefer to cook in a separate sealed container (Kaniko) to avoid messing up the kitchen, while others bring their own portable stove (Docker-in-Docker). This article shows you how to set up that magic wand correctly so your kitchen runs smoothly without chaos.

It was 3 AM on a Tuesday when my pager went off. A production pipeline had been running for 8 hours — a Java microservice build that normally took 20 minutes. The Jenkins master was showing 50+ queued builds, and every agent pod was stuck in Pending state. I checked the Kubernetes cluster: 30 nodes, plenty of resources. But the pod templates were configured with requests: { cpu: 4, memory: 8Gi } and the cluster had only 4 CPU per node. The scheduler couldn't find a node with enough resources, so builds piled up. That night I learned the hard way that resource limits in pod templates are not just suggestions — they're hard constraints that can bring your CI/CD to a halt.

Jenkins and Kubernetes integration has evolved from a nice-to-have to a must-have for any organization running cloud-native CI/CD. The Kubernetes plugin (version 1.31.5 as of writing) allows Jenkins to dynamically spin up agent pods on-demand, giving you elastic build capacity. But with great power comes great complexity: RBAC misconfigurations, pod retention policies that leak resources, and the eternal debate of DinD vs Kaniko for container builds.

In this article, I'll walk you through the complete production-grade setup: deploying Jenkins on Kubernetes using the official Helm chart (v5.0.8), configuring dynamic pod templates with multiple containers, setting up RBAC so Jenkins can talk to the Kubernetes API, and choosing the right container build strategy. I'll cover PVC-backed caching to speed up Maven/Gradle builds, and share the pod retention and resource limit settings that saved my 3 AM sanity.

This isn't a beginner's guide. You already know what Jenkins and Kubernetes are. You've probably tried the basic integration and hit a wall. I'm here to show you the walls I've hit, the scars I've earned, and the exact YAML and commands that made it work.

Deploying Jenkins on Kubernetes with Helm Chart v5.0.8

The official Jenkins Helm chart (jenkins/jenkins) is the recommended way to deploy Jenkins on Kubernetes. As of writing, the latest stable version is 5.0.8, which uses Jenkins 2.440.3. The chart creates a Deployment, Service, ServiceAccount, and RBAC resources. For production, you must override many defaults.

``bash helm repo add jenkins https://charts.jenkins.io helm repo update helm show values jenkins/jenkins --version 5.0.8 > values.yaml ``

``yaml controller: image: jenkins/jenkins tag: 2.440.3-lts-jdk17 serviceType: ClusterIP # expose via ingress servicePort: 8080 targetPort: 8080 ingress: enabled: true hostName: jenkins.example.com tls: - secretName: jenkins-tls hosts: - jenkins.example.com resources: requests: cpu: 2 memory: 4Gi limits: cpu: 4 memory: 8Gi javaOpts: '-Xms2048m -Xmx4096m -Djenkins.install.runSetupWizard=false' installPlugins: - kubernetes:1.31.5 - workflow-job:1385.vb_58b_86ea_fff1 - workflow-aggregator:596.v8c21c963d92d - git:5.2.0 - configuration-as-code:1775.v810dc9b_2c43 adminUser: admin adminPassword: <generate-secure-password> persistence: enabled: true size: 50Gi storageClass: gp2 # adjust for your cloud rbac: create: true serviceAccountName: jenkins ``

One critical gotcha: the default serviceType: LoadBalancer exposes Jenkins on a public IP. In production, always use ClusterIP and an ingress controller with TLS termination. Also, set jenkins.install.runSetupWizard=false to avoid manual setup.

``bash kubectl create namespace jenkins helm install jenkins jenkins/jenkins --version 5.0.8 -f values.yaml -n jenkins ``

``bash kubectl exec -n jenkins svc/jenkins -c jenkins -- cat /run/secrets/additional/chart-admin-password ``

Never use LoadBalancer in production
Exposing Jenkins via LoadBalancer bypasses your ingress security policies. Always use ClusterIP + ingress with TLS and authentication middleware.
Production Insight
In one deployment, I forgot to set serviceType: ClusterIP and the chart created an AWS NLB. Someone found the public DNS and tried to brute-force the admin password. The login page was exposed for 48 hours before I noticed. Now I always set serviceType: ClusterIP and use an ingress with OIDC authentication.
Key Takeaway
Always disable the setup wizard and use ClusterIP + ingress for production Jenkins on Kubernetes.

Kubernetes Plugin Installation and Configuration via Helm

The Kubernetes plugin (version 1.31.5) is included in the installPlugins list. After deployment, you need to configure it to connect to the Kubernetes API. The plugin supports two modes: using the service account token (easiest) or a kubeconfig file.

For Helm deployment, the plugin automatically uses the pod's service account (jenkins) which has cluster-admin role by default. To verify:

``bash kubectl get clusterrolebinding jenkins -o yaml ``

If you want to restrict permissions, create a custom ClusterRole with only necessary verbs:

``yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: jenkins-agent rules: - apiGroups: [''] resources: ['pods', 'pods/log', 'pods/exec'] verbs: ['get', 'list', 'watch', 'create', 'delete'] - apiGroups: [''] resources: ['pods/attach'] verbs: ['create'] ``

``yaml rbac: create: true serviceAccountName: jenkins serviceAccountAnnotations: {} roleRef: jenkins-agent # custom ClusterRole name ``

In the Jenkins UI, go to Manage Jenkins → Nodes and Clouds → Configure Clouds → Add a new cloud → Kubernetes. Set: - Kubernetes URL: https://kubernetes.default.svc.cluster.local - Kubernetes Namespace: jenkins-agents (create it first) - Credentials: use the global token from service account (auto-detected) - Jenkins URL: http://jenkins:8080 (internal service) - Jenkins tunnel: jenkins:50000 (for JNLP)

Test connection by clicking 'Test Connection'. If it fails, check pod logs: kubectl logs -l app=jenkins -c jenkins.

Use a separate namespace for agents
Create a dedicated namespace (e.g., 'jenkins-agents') for agent pods. This isolates resources and makes cleanup easier. Set the namespace in the cloud configuration.
Production Insight
I once had the plugin configured with the wrong namespace — agents were created in the 'default' namespace, which had a network policy blocking egress to the Jenkins master. Agents started but couldn't connect. The fix: move agents to a namespace with proper egress rules.
Key Takeaway
Always use a separate namespace for agent pods and ensure network policies allow communication between Jenkins master and agents.

Dynamic Pod Templates with Multi-Container Agents

Dynamic pod templates are defined in Jenkins as YAML or via the UI. For production, I recommend defining them in a shared library or using the configuration-as-code plugin. A pod template can have multiple containers: the JNLP container (mandatory) and sidecar containers for tools like Docker, Maven, or Kaniko.

Example pod template YAML for a Maven build with Docker and Kaniko:

``yaml apiVersion: v1 kind: Pod metadata: labels: jenkins-agent: maven-docker spec: containers: - name: jnlp image: jenkins/inbound-agent:3206.vb_15dcf73f6a_9-1 args: ['$(JENKINS_SECRET)', '$(JENKINS_NAME)'] resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1Gi - name: maven image: maven:3.9.6-eclipse-temurin-21 command: ['cat'] tty: true resources: requests: cpu: 1 memory: 2Gi limits: cpu: 2 memory: 4Gi volumeMounts: - name: maven-cache mountPath: /root/.m2 - name: kaniko image: gcr.io/kaniko-project/executor:v1.21.0 command: ['cat'] tty: true resources: requests: cpu: 500m memory: 1Gi limits: cpu: 1 memory: 2Gi volumeMounts: - name: docker-config mountPath: /kaniko/.docker volumes: - name: maven-cache persistentVolumeClaim: claimName: maven-cache - name: docker-config secret: secretName: regcred items: - key: .dockerconfigjson path: config.json ``

``groovy podTemplate(yaml: libraryResource('podtemplates/maven-docker.yaml')) { node(POD_LABEL) { stage('Build') { container('maven') { sh 'mvn clean package' } } stage('Docker Build') { container('kaniko') { sh ''' /kaniko/executor \ --context=dir:///workspace \ --destination=registry.example.com/myapp:${BUILD_NUMBER} \ --cache=true \ --cache-repo=registry.example.com/cache ''' } } } } ``

Note: the JNLP container must be first in the list. The command: ['cat'] with tty: true keeps sidecar containers alive so they don't exit prematurely.

Container order matters
The JNLP container must be the first container in the pod spec. If you put it second, Jenkins will fail to connect to the agent.
Production Insight
I once defined a pod template with the JNLP container second. The pod started but the agent never connected. I spent hours debugging network policies until I noticed the container order. Reordering fixed it instantly.
Key Takeaway
Keep sidecar containers alive with command: ['cat'] and tty: true to use them in pipeline steps.

RBAC Configuration for Jenkins in Kubernetes

Jenkins needs permissions to create, list, watch, and delete pods in the agent namespace. The default Helm chart creates a ClusterRoleBinding with cluster-admin, which is excessive. For production, follow least privilege.

``yaml apiVersion: v1 kind: ServiceAccount metadata: name: jenkins-agent namespace: jenkins-agents --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: jenkins-agent namespace: jenkins-agents rules: - apiGroups: [''] resources: ['pods', 'pods/log', 'pods/exec', 'pods/attach'] verbs: ['get', 'list', 'watch', 'create', 'delete'] - apiGroups: [''] resources: ['events'] verbs: ['get', 'list', 'watch'] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: jenkins-agent namespace: jenkins-agents roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: jenkins-agent subjects: - kind: ServiceAccount name: jenkins-agent namespace: jenkins ``

``yaml rbac: create: false # we create our own serviceAccount: create: true name: jenkins annotations: {} ``

But wait — the Jenkins master itself needs to read secrets (like registry credentials) in its own namespace. So you also need:

``yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: jenkins-master namespace: jenkins rules: - apiGroups: [''] resources: ['secrets'] verbs: ['get', 'list'] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: jenkins-master namespace: jenkins roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: jenkins-master subjects: - kind: ServiceAccount name: jenkins namespace: jenkins ``

Apply both sets of RBAC resources before deploying Jenkins.

One more gotcha: the Kubernetes plugin uses the service account from the namespace where Jenkins runs. If you change the namespace, the plugin won't have permissions. Always test with kubectl auth can-i.

Don't use cluster-admin in production
Cluster-admin gives Jenkins full control over all namespaces, including the ability to delete critical system pods. Always scope permissions to the agent namespace.
Production Insight
After a security audit, we discovered our Jenkins had cluster-admin. The auditor was not happy. We created scoped RBAC and tested with kubectl auth can-i create pods --as=system:serviceaccount:jenkins:jenkins -n jenkins-agents.
Key Takeaway
Create a dedicated Role in the agent namespace with minimal verbs (get, list, watch, create, delete) for pods and subresources.

Docker-in-Docker vs Kaniko: Building Container Images

When your pipeline builds Docker images, you have two main options: Docker-in-Docker (DinD) or Kaniko. Both have trade-offs.

Docker-in-Docker (DinD): Run a Docker daemon as a sidecar container. The daemon needs privileged mode, which is a security risk. Also, the daemon consumes resources and may leave behind images that fill up the pod's disk.

``yaml - name: dind image: docker:24.0.7-dind securityContext: privileged: true volumeMounts: - name: docker-socket mountPath: /var/run - name: docker-storage mountPath: /var/lib/docker ``

``groovy container('docker') { sh 'docker build -t myapp .' } ``

Kaniko: Builds images without privileged mode. It runs in userspace and pushes directly to the registry. Kaniko is faster for cold builds because it caches base layers.

``groovy container('kaniko') { sh ''' /kaniko/executor \ --context=dir:///workspace \ --destination=registry.example.com/myapp:${BUILD_NUMBER} \ --cache=true \ --cache-repo=registry.example.com/cache ''' } ``

Comparison: Kaniko is more secure (no privileged mode), integrates with GCR/ECR/ACR natively, and supports caching. DinD is more familiar but requires privileged mode and careful cleanup. In production, I always choose Kaniko unless there's a specific reason to use DinD (e.g., building Dockerfiles with special features like multi-stage that Kaniko doesn't support — but Kaniko supports it now).

One gotcha with Kaniko: it needs a .docker/config.json file for registry authentication. Mount it as a secret:

``bash kubectl create secret docker-registry regcred --docker-server=registry.example.com --docker-username=myuser --docker-password=mypass -n jenkins-agents ``

Then mount in pod template as shown earlier.

Prefer Kaniko over DinD for production
Kaniko eliminates the need for privileged containers, reducing attack surface. It also integrates well with cloud registries and supports caching out of the box.
Production Insight
I ran DinD in production for months until a security scan flagged the privileged container. We switched to Kaniko and saw build times drop by 20% because of layer caching. No more 'docker: command not found' errors either.
Key Takeaway
Use Kaniko for container builds in Kubernetes — no privileged mode, native caching, and better security posture.

PVC for Caching Across Builds (Maven/Gradle)

Build tools like Maven and Gradle download dependencies on every build. Without caching, each build downloads the internet. A PersistentVolumeClaim (PVC) mounted in the agent pod can cache these dependencies across builds.

Create a PVC that supports ReadWriteMany (RWX) so multiple pods can mount it simultaneously. For example, using NFS or EFS:

``yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: maven-cache namespace: jenkins-agents spec: accessModes: - ReadWriteMany resources: requests: storage: 20Gi storageClassName: nfs-client # adjust for your provisioner ``

In the pod template, mount the PVC at the Maven local repository path:

``yaml volumes: - name: maven-cache persistentVolumeClaim: claimName: maven-cache containers: - name: maven volumeMounts: - name: maven-cache mountPath: /root/.m2 ``

For Gradle, mount at /home/gradle/.gradle or /root/.gradle depending on the image.

Gotcha: Concurrent writes to the same cache can cause corruption. To avoid this, use a tool like mvn dependency:resolve with -o (offline) after the first build, or use a shared cache with proper locking. Alternatively, use a read-only cache and copy it to a writable volume per pod. But for most teams, the risk is low.

Another approach: use a sidecar that runs a caching proxy (like Nexus or Artifactory) and configure Maven/Gradle to use it. This avoids PVC altogether and is more reliable.

Production insight: I once used a PVC with ReadWriteOnce (RWO) and builds started failing with 'volume is already used by another pod'. The scheduler couldn't place two builds on the same node. Always use RWX for build caches.

Use ReadWriteMany for build caches
RWO volumes can only be mounted by one pod at a time, causing build backlogs. Ensure your storage class supports RWX (e.g., NFS, EFS, or Longhorn).
Production Insight
Our Maven builds took 15 minutes each without caching. After mounting a 50Gi RWX PVC, they dropped to 2 minutes. The key was using mvn dependency:go-offline in the first build to populate the cache.
Key Takeaway
Always use RWX PVCs for build dependency caches to avoid pod scheduling conflicts.

Helm Chart Deployment Patterns for Jenkins

Beyond the basic install, there are several deployment patterns for production:

1. Blue/Green deployment: Run two Helm releases (jenkins-blue, jenkins-green) with different tags. Use a load balancer to switch traffic. This allows zero-downtime upgrades.

2. Canary deployment: Use Helm's --set to modify a small subset of replicas with a new image. But Jenkins is stateful (build queue, config), so this is tricky. Better to use a canary of the agent configuration.

3. GitOps with ArgoCD: Manage Jenkins Helm chart as an ArgoCD Application. The values.yaml is stored in Git. ArgoCD syncs automatically. This ensures reproducibility.

``yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: jenkins spec: project: default source: repoURL: https://charts.jenkins.io chart: jenkins targetRevision: 5.0.8 helm: values: | controller: imageTag: 2.440.3-lts-jdk17 serviceType: ClusterIP destination: server: https://kubernetes.default.svc namespace: jenkins syncPolicy: automated: prune: true selfHeal: true ``

4. Helmfile: For multi-environment deployments (dev, staging, prod), use helmfile to manage differences.

``yaml releases: - name: jenkins chart: jenkins/jenkins version: 5.0.8 values: - values/{{ .Environment.Name }}.yaml ``

Gotcha: Helm upgrades can break if you change the nameOverride or fullnameOverride between releases. Always keep them consistent.

Production insight: I once did a helm upgrade with a new values.yaml that accidentally changed the serviceType from ClusterIP to LoadBalancer. The upgrade succeeded but exposed Jenkins to the internet. Now I use ArgoCD with automated drift detection.

Helm upgrade can change service types
Always review diff before upgrading: helm diff upgrade jenkins jenkins/jenkins -f new-values.yaml. Use tools like helm-diff or ArgoCD PreSync hooks.
Production Insight
Using ArgoCD, I accidentally synced a values.yaml that had persistence.enabled: false. Jenkins lost all jobs and config. Now I always have a PreSync check that validates persistence is enabled.
Key Takeaway
Use GitOps (ArgoCD) or helmfile with environment-specific values to manage Jenkins deployments safely.

Kubernetes Deployment via Jenkins Pipeline

Once Jenkins is set up, you can deploy applications to Kubernetes from within pipelines. Use the kubectl CLI or the Kubernetes plugin's kubeconfig step.

Method 1: Using kubectl with service account

``groovy stage('Deploy to K8s') { container('kubectl') { withKubeConfig([credentialsId: 'k8s-sa-token', serverUrl: 'https://kubernetes.default.svc']) { sh ''' kubectl set image deployment/myapp myapp=registry.example.com/myapp:${BUILD_NUMBER} -n production kubectl rollout status deployment/myapp -n production ''' } } } ``

Method 2: Using the Kubernetes Continuous Deploy plugin

``groovy step([$class: 'KubernetesDeploy', apiUrl: 'https://kubernetes.default.svc', credentialsId: 'k8s-sa-token', namespace: 'production', configs: 'deployment.yaml', enableConfigSubstitution: true]) ``

Method 3: Helm deploy

``groovy stage('Helm Deploy') { container('helm') { sh ''' helm upgrade --install myapp ./chart \ --set image.tag=${BUILD_NUMBER} \ --namespace production ''' } } ``

Important: Use a dedicated service account for deployment, not the Jenkins master's token. Create a ServiceAccount with limited permissions in the target namespace:

``yaml apiVersion: v1 kind: ServiceAccount metadata: name: jenkins-deployer namespace: production --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: deployer namespace: production rules: - apiGroups: ['apps', 'extensions'] resources: ['deployments'] verbs: ['get', 'list', 'watch', 'update', 'patch'] - apiGroups: [''] resources: ['services'] verbs: ['get', 'list', 'watch', 'update', 'patch'] ``

Then create a secret for the token and use it in Jenkins credentials.

Gotcha: The kubectl container image must have kubectl installed. Use an image like bitnami/kubectl:1.29.0.

Use a dedicated deployer service account
Separate the deployment permissions from the Jenkins master's permissions. This limits blast radius if the pipeline is compromised.
Production Insight
We had a pipeline that used the Jenkins master's service account to deploy. Someone accidentally deleted the production namespace. Now we use scoped tokens with kubectl auth can-i checks before deployment.
Key Takeaway
Always use a separate, scoped service account for deployments to production namespaces.

Pod Retention and Resource Limits Configuration

By default, Jenkins keeps agent pods for a while after the build completes (pod retention). This can lead to resource leaks. Configure pod retention in the pod template YAML or in the cloud configuration.

Pod retention options
  • never(): delete pod immediately after build
  • always(): keep pod until Jenkins restart (not recommended)
  • onFailure(): keep only if build failed (useful for debugging)

``yaml spec: podRetention: never() ``

Or in the cloud configuration (Manage Jenkins → Configure Clouds → Advanced): - Pod Retention: Never

Resource limits: Set both requests and limits for each container. Without limits, a single build can consume all cluster resources.

``yaml resources: requests: cpu: 200m memory: 256Mi limits: cpu: 500m memory: 512Mi ``

``yaml resources: requests: cpu: 1 memory: 2Gi limits: cpu: 2 memory: 4Gi ``

Gotcha: If you set limits too low, builds will be OOMKilled. Check actual usage with kubectl top pod after a successful build.

Another gotcha: Pod retention always() combined with activeDeadlineSeconds can cause pods to be killed mid-build. Set activeDeadlineSeconds to a value higher than your longest build (e.g., 3600 for 1 hour).

Production insight: I set pod retention to always() for debugging and forgot to change it back. After a week, we had 5000 completed pods in the namespace, causing etcd performance issues. Now I use never() and rely on build logs.

Never use pod retention 'always' in production
Completed pods accumulate and can overwhelm the Kubernetes API server. Use 'never' and configure log storage separately.
Production Insight
A developer set podRetention: onFailure() and then had a failing test that created a pod every minute. After 2 hours, the namespace had 120 pods, all in Completed state. The cluster autoscaler kept adding nodes. We fixed it by setting never() and adding a cronjob to clean up old pods.
Key Takeaway
Set pod retention to never() and use activeDeadlineSeconds to prevent runaway builds.

Debugging Pod Templates: Logs, Events, and Labels

  1. Check pod events: kubectl describe pod <agent-pod> shows events like 'FailedScheduling' or 'BackOff'.
  2. Check pod logs: kubectl logs <agent-pod> -c jnlp shows JNLP connection attempts.
  3. Verify pod spec: kubectl get pod <agent-pod> -o yaml shows the actual pod spec created from the template.
  4. Check Jenkins system log: Manage Jenkins → System Log → Kubernetes Plugin logs.
Common issues
  • Pod template label mismatch: the pipeline uses label 'maven' but the template has label: 'java'.
  • Missing command: ['cat'] in sidecar containers: they exit immediately.
  • Wrong image tag: jenkins/inbound-agent:latest might pull an incompatible version. Use a specific tag like 3206.vb_15dcf73f6a_9-1.

Useful labels: Add labels to pod templates for easy identification:

``yaml metadata: labels: jenkins-agent: maven-docker team: platform ``

Then you can filter: kubectl get pods -l team=platform.

Production insight: I once spent 3 hours debugging why a pod template wasn't being used. Turns out I had two templates with the same label, and Jenkins randomly picked one. The fix: use unique labels per template.

Use unique labels for each pod template
Avoid label collisions by using descriptive, unique labels (e.g., 'maven-jdk21', 'node-18'). This prevents Jenkins from picking the wrong template.
Production Insight
We had a 'maven' template and a 'gradle' template both with the same label 'build'. Jenkins randomly assigned builds to either, causing failures. We renamed them to 'maven-build' and 'gradle-build'.
Key Takeaway
Always use unique labels per pod template to avoid ambiguous matching.

Advanced: Using Init Containers and Secrets in Pod Templates

Pod templates can include init containers for setup tasks (e.g., cloning a repo, setting up credentials). They run before the main containers start.

``yaml spec: initContainers: - name: git-clone image: alpine/git:2.43.0 command: ['git', 'clone', 'https://github.com/myorg/myrepo.git', '/workspace'] volumeMounts: - name: workspace mountPath: /workspace containers: - name: jnlp ... volumes: - name: workspace emptyDir: {} ``

Secrets: Mount Docker config or SSH keys as secrets:

``yaml volumes: - name: ssh-key secret: secretName: jenkins-ssh-key defaultMode: 0600 containers: - name: jnlp volumeMounts: - name: ssh-key mountPath: /home/jenkins/.ssh ``

ConfigMap: Mount configuration files (e.g., Maven settings.xml):

``yaml volumes: - name: maven-settings configMap: name: maven-settings containers: - name: maven volumeMounts: - name: maven-settings mountPath: /root/.m2/settings.xml subPath: settings.xml ``

Production insight: We used an init container to decrypt secrets using Vault before the build started. This kept secrets out of the pod template YAML.

Use init containers for pre-build setup
Init containers can fetch secrets, clone repos, or set up networking before the build starts. This keeps the main containers clean.
Production Insight
We had a secret that was mounted in the pod template YAML as a literal value. It was visible in the Jenkins configuration UI. We moved it to a Kubernetes secret and mounted it.
Key Takeaway
Always use Kubernetes secrets or external secret stores (Vault) for sensitive data in pod templates.

Performance Tuning: JNLP Tunnels and WebSocket Mode

By default, the JNLP agent connects to Jenkins master via TCP on port 50000. This requires a stable network path. In Kubernetes, use the Jenkins tunnel parameter to specify the service: jenkins:50000.

WebSocket mode: Newer Jenkins agents support WebSocket connections, which can bypass load balancers that don't handle TCP. Enable it in the pod template:

``yaml env: - name: JENKINS_WEB_SOCKET value: 'true' ``

Or in the cloud configuration: 'WebSocket' checkbox.

Performance: For large builds, increase the JVM heap of the agent:

``yaml env: - name: JAVA_OPTS value: '-Xmx2048m -Xms1024m' ``

Connection timeout: Set connectionTimeout in the cloud configuration to 300 seconds (default 60) to avoid premature disconnections.

Gotcha: WebSocket mode requires Jenkins 2.217+ and inbound-agent 4.7+. If you use an older agent image, it will fail with 'WebSocket not supported'.

Production insight: We had a load balancer (AWS NLB) that timed out TCP connections after 350 seconds. Long builds would disconnect. Switching to WebSocket mode fixed it because WebSocket uses HTTP upgrade and the NLB kept the connection alive.

WebSocket mode for long-lived connections
If you experience agent disconnections after a few minutes, enable WebSocket mode. It's more reliable through load balancers and proxies.
Production Insight
After switching to WebSocket, our builds that took 40 minutes no longer disconnected. The error 'JNLP-connect: Connection closed' disappeared.
Key Takeaway
Enable WebSocket mode in the Kubernetes cloud configuration to avoid agent disconnections through load balancers.
● Production incidentPOST-MORTEMseverity: high

Resource Limits Starvation at 3 AM

Symptom
All agent pods stuck in Pending state with event '0/30 nodes are available: 30 Insufficient cpu, 30 Insufficient memory.' Jenkins queue grew to 200+ builds.
Assumption
The cluster was overloaded or the scheduler was broken. Team considered adding more nodes.
Root cause
Pod template had requests: { cpu: 4, memory: 8Gi } but cluster nodes had only 4 CPU and 16Gi memory. The scheduler could never place a pod because no node had 4 CPU available (OS + kubelet reserved ~0.5 CPU).
Fix
Changed pod template to requests: { cpu: 1, memory: 2Gi } and limits: { cpu: 2, memory: 4Gi }. Also added resource quotas in namespace to prevent runaway pods.
Key lesson
  • Always set pod template resource requests to what the build actually needs, not what you 'might' need.
  • Use kubectl describe node to see allocatable resources.
  • Prefer small requests with higher limits for bursting.
Production debug guideSymptom → Root cause → Fix4 entries
Symptom · 01
Agent pod stuck in Pending with 'Insufficient cpu' or 'Insufficient memory'
Fix
Root cause: pod template resource requests exceed node allocatable capacity. Fix: reduce requests in pod template or add more nodes. Run kubectl describe node to see allocatable resources. Update pod template YAML with lower requests.
Symptom · 02
Agent pod starts but pipeline says 'Agent disconnected' after a few seconds
Fix
Root cause: JNLP connection failure — often due to wrong Jenkins URL or tunnel configuration. Fix: ensure JENKINS_URL environment variable in pod template points to the Jenkins service (e.g., http://jenkins:8080). Check pod logs: kubectl logs <agent-pod> -c jnlp.
Symptom · 03
Build fails with 'docker: command not found' but pod includes a Docker container
Fix
Root cause: the pipeline is trying to run docker in the wrong container (the JNLP container). Fix: set container('docker') in pipeline step, or use sidecarContainer with command: ['cat'] to keep it alive. For Kaniko, use container('kaniko').
Symptom · 04
Agent pods not being cleaned up after build — hundreds of Completed pods
Fix
Root cause: pod retention set to always() or not configured. Fix: set pod retention to never() in pod template YAML. Also set controller.cleanup in Helm values to run cleanup cronjob. Run kubectl delete pod -l jenkins=agent --field-selector status.phase=Succeeded manually to clean up.
★ Jenkins Kubernetes Integration Quick Referenceprint this for your desk
Pod pending: insufficient resources
Immediate action
Check pod events: `kubectl describe pod <pod-name>`
Commands
kubectl describe pod <agent-pod> | grep -A5 Events
kubectl describe node | grep -A5 Allocatable
Fix now
Reduce pod template resource requests in Jenkins config or values.yaml
Agent disconnected after pod starts+
Immediate action
Check JNLP container logs
Commands
kubectl logs <agent-pod> -c jnlp
kubectl get svc jenkins -o jsonpath='{.spec.clusterIP}'
Fix now
Set env JENKINS_URL=http://jenkins-svc:8080 in pod template
Docker command not found in pipeline+
Immediate action
Verify which container the step runs in
Commands
kubectl get pod <agent-pod> -o jsonpath='{.spec.containers[*].name}'
Fix now
Use container('docker') in pipeline before docker commands
Completed agent pods accumulating+
Immediate action
Check pod retention setting
Commands
kubectl get pods --field-selector status.phase=Succeeded
kubectl delete pod --field-selector status.phase=Succeeded
Fix now
Set podRetention: never() in pod template YAML
Build fails with 'no route to host' to Docker registry+
Immediate action
Check network policies or DNS
Commands
kubectl exec -it <agent-pod> -c jnlp -- nslookup registry.example.com
kubectl get networkpolicies -n jenkins
Fix now
Add egress rule to allow traffic to registry IP/CIDR
Docker-in-Docker vs Kaniko for Container Builds
FeatureDocker-in-Docker (DinD)KanikoNotes
Privileged modeRequiredNot requiredDinD is a security risk
Layer cachingBuilt-in (docker layers)Supported with --cache-repoKaniko caches to registry
Build speedFast for repeated builds (local cache)Slower first build, faster subsequent with cacheDepends on caching strategy
Registry authdocker login (sidecar)Mount .docker/config.jsonKaniko is simpler
Resource usageHigh (daemon + build)Low (only build process)Kaniko is lighter
CompatibilitySupports all Dockerfile featuresSupports most, some edge casesCheck Kaniko issues for unsupported features
Disk cleanupMust clean /var/lib/dockerNo cleanup neededKaniko is ephemeral
📦 Downloadable Quick Reference

Print-friendly master reference covering all topics in this track.

⇩ Download PDF

Key takeaways

1
Deploy Jenkins on Kubernetes using Helm chart v5.0.8 with ClusterIP service and ingress for production.
2
Configure the Kubernetes plugin with a dedicated service account and scoped RBAC in a separate agent namespace.
3
Use pod templates with multiple containers (JNLP + sidecars) and keep sidecars alive with `command
['cat']`.
4
Prefer Kaniko over Docker-in-Docker for container builds to avoid privileged mode and improve security.
5
Use ReadWriteMany PVCs for build dependency caching (Maven/Gradle) to speed up builds.
6
Set pod retention to never() and resource limits to prevent resource leaks and runaway builds.
7
Use WebSocket mode for reliable agent connections through load balancers.
8
Always use unique labels per pod template to avoid ambiguous matching.

Common mistakes to avoid

6 patterns
×

Setting pod template resource requests too high

Symptom
All agent pods stuck in Pending with insufficient resources
Fix
Use kubectl describe node to see allocatable resources; set requests to average build usage
×

Forgetting to set `command: ['cat']` on sidecar containers

Symptom
Sidecar container exits immediately; pipeline step fails with container not found
Fix
Add command: ['cat'] and tty: true to sidecar containers in the pod template
×

Using `podRetention: always()` in production

Symptom
Thousands of Completed pods accumulate, causing API server load and etcd issues
Fix
Set podRetention: never() and clean up with kubectl delete pod --field-selector status.phase=Succeeded
×

Not using a separate namespace for agent pods

Symptom
Network policies or resource quotas interfere with agent pod creation
Fix
Create a dedicated namespace (e.g., jenkins-agents) and configure the cloud plugin to use it
×

Using LoadBalancer service type for Jenkins

Symptom
Jenkins exposed to internet; security audit fails
Fix
Set serviceType: ClusterIP and use ingress with TLS and authentication
×

Not setting `activeDeadlineSeconds` on pod template

Symptom
Runaway builds consume resources indefinitely
Fix
Set activeDeadlineSeconds: 3600 (or appropriate timeout) in pod template YAML
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does the Jenkins Kubernetes plugin dynamically provision agents?
Q02SENIOR
What are the security implications of using Docker-in-Docker vs Kaniko i...
Q03SENIOR
How would you configure RBAC for Jenkins in Kubernetes with least privil...
Q04SENIOR
What is the purpose of the `command: ['cat']` and `tty: true` in sidecar...
Q05SENIOR
How do you debug a Jenkins agent pod that stays in Pending state?
Q06SENIOR
Explain how to use a PersistentVolumeClaim for Maven caching across buil...
Q07JUNIOR
What is the difference between `requests` and `limits` in pod template r...
Q08SENIOR
How would you set up a Jenkins pipeline to deploy a Docker image to Kube...
Q01 of 08SENIOR

How does the Jenkins Kubernetes plugin dynamically provision agents?

ANSWER
The plugin listens for builds that match a pod template label. When a build requires an agent with that label, the plugin uses the Kubernetes API to create a pod in the configured namespace. The pod contains a JNLP container that connects back to Jenkins master. After the build completes, the pod is deleted (depending on pod retention policy).
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
What is the latest version of the Jenkins Kubernetes plugin?
02
Can I use the same PVC for multiple agent pods simultaneously?
03
Why do my agent pods show 'BackOff' in events?
04
How do I pass secrets to a pod template?
05
What is the difference between `jenkins/inbound-agent` and `jenkins/slave` images?
06
How do I restrict which namespaces Jenkins can deploy to?
07
Can I use pod templates defined in a shared library?
08
What is the recommended way to upgrade Jenkins Helm chart?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 09, 2026
last updated
230
articles · all by Naren
🔥

That's Jenkins. Mark it forged?

11 min read · try the examples if you haven't

Previous
Jenkins Backup and Restore
34 / 39 · Jenkins
Next
Jenkins Groovy Scripting for Pipelines