Deploy Jenkins on Kubernetes — Step-by-step Guide (with YAML)


Introduction — Why run Jenkins on Kubernetes?

If you’ve been running Jenkins on a VM or a single server, you know the pain when that server goes down: builds stop, history disappears, and everyone looks at you. Running Jenkins on Kubernetes gives you the flexibility to manage lifecycle, scale agents, and treat Jenkins as another cloud-native application — but you still need reliable storage and correct RBAC, probes, and networking.

In this guide I’ll walk you through a practical, easy-to-follow deployment of Jenkins on Kubernetes using YAML manifests. You’ll get commands you can copy-paste, explanations for why each step matters, and small production-minded notes so you don’t end up surprised when a node is drained or a pod restarts.


What this tutorial covers (high level)

  • Create a devops-tools namespace
  • Create a service account with cluster-admin-like permissions for Jenkins
  • Provision local persistent storage (demo) or show how to swap to cloud storage
  • Deploy Jenkins with health probes and security context
  • Expose Jenkins via a NodePort (or suggest alternatives for production)
  • Retrieve the initial admin password and finish setup

All YAML examples in this post are taken from the scriptcamp/kubernetes-jenkins repo — clone it if you want the full files:

git clone https://github.com/scriptcamp/kubernetes-jenkins

Step 1 — Create a namespace for DevOps tools

Keeping CI/CD tooling in its own namespace helps with organization, resource quotas, and RBAC scoping.

kubectl create namespace devops-tools

This keeps Jenkins and other DevOps components separate from application namespaces.


Step 2 — Create a service account (RBAC)

Jenkins often needs to talk to the Kubernetes API (for dynamic agents, cluster updates, etc.). The simplest approach for demos is to create a service account and give it broad cluster permissions. Warning: giving cluster-wide * permissions is permissive — for production, narrow these rules to only the required verbs and resources.

serviceAccount.yaml

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: jenkins-admin
rules:
  - apiGroups: [""]
    resources: ["*"]
    verbs: ["*"]

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins-admin
  namespace: devops-tools

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: jenkins-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: jenkins-admin
subjects:
- kind: ServiceAccount
  name: jenkins-admin
  namespace: devops-tools

Apply it:

kubectl apply -f serviceAccount.yaml

Tip: When you harden RBAC later, follow the principle of least privilege: allow only namespaces, resources and verbs Jenkins actually needs (Pods, Secrets, ConfigMaps, ServiceAccounts, Deployments, Jobs, etc.).


Step 3 — Persistent storage for Jenkins data

Jenkins stores its configuration, plugins, and build history under /var/jenkins_home. If the pod is ephemeral without persistent storage, you lose everything on restart. For demos you can use a local PersistentVolume; for production, use your cloud or SAN-backed storage class (AWS EBS, GCE PD, Azure Disk, NFS, or an operator-managed CSI solution).

volume.yaml (demo using local storage)

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jenkins-pv-volume
  labels:
    type: local
spec:
  storageClassName: local-storage
  claimRef:
    name: jenkins-pv-claim
    namespace: devops-tools
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  local:
    path: /mnt
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - worker-node01

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-pv-claim
  namespace: devops-tools
spec:
  storageClassName: local-storage
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi

Important: Replace worker-node01 with the actual worker node hostname from kubectl get nodes. Local PVs bind to a single node — if that node is removed, you’ll lose the data. Prefer cloud/storage-class-backed PVs for HA.

Apply:

kubectl apply -f volume.yaml

Step 4 — Deployment: run Jenkins with probes and security context

This deployment uses the official Jenkins LTS image, sets resource requests/limits, readiness/liveness probes, mounts the PVC, and runs Jenkins with a user that can write to the volume.

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: devops-tools
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jenkins-server
  template:
    metadata:
      labels:
        app: jenkins-server
    spec:
      securityContext:
            fsGroup: 1000
            runAsUser: 1000
      serviceAccountName: jenkins-admin
      containers:
        - name: jenkins
          image: jenkins/jenkins:lts
          resources:
            limits:
              memory: "2Gi"
              cpu: "1000m"
            requests:
              memory: "500Mi"
              cpu: "500m"
          ports:
            - name: httpport
              containerPort: 8080
            - name: jnlpport
              containerPort: 50000
          livenessProbe:
            httpGet:
              path: "/login"
              port: 8080
            initialDelaySeconds: 90
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 5
          readinessProbe:
            httpGet:
              path: "/login"
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          volumeMounts:
            - name: jenkins-data
              mountPath: /var/jenkins_home         
      volumes:
        - name: jenkins-data
          persistentVolumeClaim:
              claimName: jenkins-pv-claim

Apply and verify:

kubectl apply -f deployment.yaml
kubectl get deployments -n devops-tools
kubectl describe deployment jenkins -n devops-tools
kubectl get pods -n devops-tools

Alternative (not persistent): If you just want a throwaway Jenkins for experimenting, replace the volume with emptyDir: {} — but don’t do this if you want to keep plugins, jobs or build history.


Step 5 — Expose Jenkins (Service YAML)

You can expose Jenkins multiple ways:

  • NodePort (quick demo) — exposes on all node IPs and a high port (example uses 32000).
  • LoadBalancer — for cloud environments (AWS/Google/Azure). Creates a cloud LB with a stable public IP.
  • Ingress — best practice in production to provide host-based routing, TLS, and central auth.

service.yaml (NodePort example)

apiVersion: v1
kind: Service
metadata:
  name: jenkins-service
  namespace: devops-tools
  annotations:
      prometheus.io/scrape: 'true'
      prometheus.io/path:   /
      prometheus.io/port:   '8080'
spec:
  selector:
    app: jenkins-server
  type: NodePort  
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 32000

Apply it:

kubectl apply -f service.yaml

Open Jenkins: http://<node-ip>:32000.

Note: If you have Ingress, prefer creating an Ingress rule and TLS certificate (Let’s Encrypt or your corporate CA) and keep NodePort for internal clusters or debugging.


Step 6 — Get the initial admin password

Jenkins shows an initial admin password on first start. You can get it from logs or by reading the secret file inside the pod.

Find the pod name:

kubectl get pods -n devops-tools

Get logs and search for initialAdminPassword or read the file directly:

kubectl logs <jenkins-pod-name> -n devops-tools

# or
kubectl exec -it <jenkins-pod-name> -n devops-tools -- cat /var/jenkins_home/secrets/initialAdminPassword

Use that password to unlock Jenkins, install plugins and create your admin user.


Production considerations & best practices

  1. Use a stable, replicated storage solution — cloud disks, NFS, or a CSI-backed distributed storage (Longhorn, OpenEBS, Portworx) so data survives node failures.
  2. Harden RBAC — do not keep ClusterRole with * permissions in production. Create scoped roles for Kubernetes operations Jenkins actually needs.
  3. Run Jenkins masters with Anti-affinity — for HA, run multiple masters or use backup/restore workflows (Jenkins master HA is non-trivial; many teams prefer backing up JENKINS_HOME).
  4. Use agents (ephemeral pods) — run builds on ephemeral Kubernetes agents (Kubernetes plugin or a cloud-native agents approach) so master stays lightweight.
  5. Backups — schedule regular backups of /var/jenkins_home (or the underlying PV snapshot) and test restores.
  6. Monitoring and Alerts — scrape Jenkins metrics and logs using Prometheus/Grafana and alert on job failures, queue length, and disk pressure.

Troubleshooting quick tips

  • Pod stuck ContainerCreating — check kubectl describe pod for volume or node affinity issues.
  • jenkins cannot write to volume — ensure fsGroup and runAsUser are set and the underlying PV permissions allow write.
  • Readiness probe failing — increase initialDelaySeconds if Jenkins takes longer to boot while installing plugins.

Conclusion

Hosting Jenkins on Kubernetes modernizes your CI/CD platform but requires attention to storage, RBAC, and availability. For experiments and learning, the manifests above get you running quickly; for production, swap local PVs with a durable CSI-backed storage class, narrow RBAC permissions, and use Ingress/LoadBalancer with TLS.


Repository used as reference: https://github.com/scriptcamp/kubernetes-jenkins

Leave a Comment