10-Day Roadmap

Jenkins + Kubernetes
CI/CD Engineering

From zero to production-grade pipelines — deploying a 2-tier Golang + MongoDB app using Jenkins, Docker, and Kubernetes.

Stack: Golang · MongoDB · Docker · Jenkins · Kubernetes
Level: Beginner → Advanced
Format: Theory + Real YAML
Learning progression
Beginner (Days 1–3)
Intermediate (Days 4–7)
Advanced (Days 8–10)
DAY 01
Foundations — CI/CD Concepts & App Overview
Beginner · ~3 hours · Theory + App structure
Goal: Understand CI/CD theory, meet the 2-tier app you'll be deploying, and set up your local environment.
What you'll learn
  • CI vs CD vs CD (Continuous Integration / Delivery / Deployment)
  • The role of Jenkins in a modern pipeline
  • Our app: Golang REST API backed by MongoDB
  • Tool versions: Jenkins LTS, Kubernetes 1.29+, Docker 24+, Go 1.22+
The 2-Tier Application
┌─────────────────────────────────────────────┐ 2-Tier Architecture ├─────────────────────────────────────────────┤ Backend (Golang) :8080 ├── GET /api/users ├── POST /api/users └── GET /health Database (MongoDB) :27017 └── db: appdb col: users └─────────────────────────────────────────────┘
Golang Backend — main.go
main.go — Golang REST API
package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type User struct {
    Name  string `json:"name" bson:"name"`
    Email string `json:"email" bson:"email"`
}

var client *mongo.Client

func main() {
    mongoURI := os.Getenv("MONGO_URI")
    if mongoURI == "" {
        mongoURI = "mongodb://localhost:27017"
    }
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    var err error
    client, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
    if err != nil { log.Fatal(err) }
    defer client.Disconnect(ctx)

    http.HandleFunc("/health", healthCheck)
    http.HandleFunc("/api/users", usersHandler)
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func healthCheck(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    col := client.Database("appdb").Collection("users")
    w.Header().Set("Content-Type", "application/json")
    switch r.Method {
    case http.MethodGet:
        cursor, _ := col.Find(context.Background(), bson.D{})
        var users []User
        cursor.All(context.Background(), &users)
        json.NewEncoder(w).Encode(users)
    case http.MethodPost:
        var u User
        json.NewDecoder(r.Body).Decode(&u)
        col.InsertOne(context.Background(), u)
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(u)
    }
}
Official Documentation
DAY 02
Install & Configure Jenkins
Beginner · ~3 hours · Jenkins LTS, plugins, first job
Goal: Install Jenkins via Docker, install essential plugins, and create your first freestyle job.
Run Jenkins with Docker
docker-compose.jenkins.yml
version: "3.8"
services:
  jenkins:
    image: jenkins/jenkins:lts-jdk17
    container_name: jenkins
    privileged: true
    user: root
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - jenkins_home:/var/jenkins_home
      # Mount Docker socket so Jenkins can build images
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - JAVA_OPTS=-Djenkins.install.runSetupWizard=false
volumes:
  jenkins_home:
Auto-install Plugins (JCasC)
plugins.txt — essential plugins list
# Paste into Jenkins plugin manager or use install-plugins.sh
git
workflow-aggregator          # Pipeline plugin suite
pipeline-stage-view
docker-workflow
kubernetes
credentials-binding
blueocean
github
slack
junit
cobertura
First Shell Job — Test Go Build
Shell script (Execute Shell step in Freestyle job)
#!/bin/bash
set -e

# Install Go if not present
export GOPATH=/tmp/go
export PATH=$PATH:/usr/local/go/bin

cd $WORKSPACE
go mod tidy
go build ./...
go test ./... -v
echo "Build successful!"
Official Documentation
DAY 03
Dockerize the 2-Tier App
Beginner · ~4 hours · Dockerfile, docker-compose, registry push
Goal: Write production-grade Dockerfiles for Golang backend and wire it with MongoDB via docker-compose.
Golang Multi-Stage Dockerfile
Dockerfile — Golang (multi-stage, minimal image)
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app

# Cache deps separately for faster rebuilds
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .

# Stage 2: Runtime — distroless (no shell, minimal attack surface)
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]
docker-compose.yml — Full Stack
docker-compose.yml
version: "3.8"
services:
  backend:
    build: .
    container_name: go-backend
    ports:
      - "8080:8080"
    environment:
      - MONGO_URI=mongodb://mongo:27017
    depends_on:
      mongo:
        condition: service_healthy
    restart: unless-stopped

  mongo:
    image: mongo:7.0
    container_name: mongodb
    ports:
      - "27017:27017"
    volumes:
      - mongo_data:/data/db
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  mongo_data:
Official Documentation
DAY 04
Your First Declarative Jenkinsfile Pipeline
Intermediate · ~4 hours · Declarative syntax, stages, agents
Goal: Write a full Declarative Jenkinsfile that checks out code, runs Go tests, and builds a Docker image.
Jenkinsfile — Declarative Pipeline
Jenkinsfile
pipeline {
  agent { docker { image 'golang:1.22-alpine' } }

  environment {
    APP_NAME   = 'go-backend'
    REGISTRY   = 'docker.io/yourdockerhubuser'
    IMAGE_TAG  = "${env.BUILD_NUMBER}"
  }

  stages {
    stage('Checkout') {
      steps {
        checkout scm
        sh 'echo "Branch: ${GIT_BRANCH}, Commit: ${GIT_COMMIT}"'
      }
    }

    stage('Dependencies') {
      steps {
        sh 'go mod download'
        sh 'go mod verify'
      }
    }

    stage('Lint') {
      steps {
        sh '''
          go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
          golangci-lint run ./...
        '''
      }
    }

    stage('Test') {
      steps {
        sh 'go test ./... -v -coverprofile=coverage.out'
        sh 'go tool cover -func=coverage.out'
      }
      post {
        always {
          junit '**/test-results/*.xml'
        }
      }
    }

    stage('Build Binary') {
      steps {
        sh 'CGO_ENABLED=0 GOOS=linux go build -o server .'
      }
    }

    stage('Build Docker Image') {
      agent any  // switch back to host for Docker
      steps {
        script {
          def img = docker.build("${REGISTRY}/${APP_NAME}:${IMAGE_TAG}")
          img.tag('latest')
        }
      }
    }
  }

  post {
    failure {
      echo 'Pipeline failed! Sending notification...'
    }
    success {
      echo "Build ${IMAGE_TAG} succeeded!"
    }
    always {
      cleanWs()
    }
  }
}
Official Documentation
DAY 05
Kubernetes Fundamentals for the App
Intermediate · ~5 hours · Pods, Deployments, Services, ConfigMaps
Goal: Write all Kubernetes manifests needed to run the Golang + MongoDB stack manually before automating with Jenkins.
MongoDB StatefulSet + PVC
k8s/mongo-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
  namespace: app
spec:
  serviceName: mongodb
  replicas: 1
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
        - name: mongodb
          image: mongo:7.0
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: mongo-storage
              mountPath: /data/db
          readinessProbe:
            exec:
              command: ["mongosh", "--eval", "db.adminCommand('ping')"]
            initialDelaySeconds: 15
            periodSeconds: 10
  volumeClaimTemplates:
    - metadata:
        name: mongo-storage
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
  name: mongodb
  namespace: app
spec:
  clusterIP: None  # Headless — StatefulSet needs this
  selector:
    app: mongodb
  ports:
    - port: 27017
Golang Backend Deployment + Service
k8s/backend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-backend
  namespace: app
  labels:
    app: go-backend
    version: v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: go-backend
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: go-backend
    spec:
      containers:
        - name: backend
          image: docker.io/yourdockerhubuser/go-backend:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          env:
            - name: MONGO_URI
              value: "mongodb://mongodb.app.svc.cluster.local:27017"
          resources:
            requests:
              cpu: "100m"
              memory: "64Mi"
            limits:
              cpu: "500m"
              memory: "128Mi"
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: go-backend-svc
  namespace: app
spec:
  selector:
    app: go-backend
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP
Official Documentation
DAY 06
Jenkins Deploys to Kubernetes
Intermediate · ~5 hours · kubectl in pipeline, Docker push, rolling update
Goal: Extend the Jenkinsfile to push images to DockerHub and trigger a rolling update on Kubernetes after every merge to main.
Full Build → Push → Deploy Jenkinsfile
Jenkinsfile — Build, Push, Deploy
pipeline {
  agent any

  environment {
    REGISTRY     = "yourdockerhubuser/go-backend"
    IMAGE_TAG    = "${env.GIT_COMMIT[0..7]}"
    KUBECONFIG   = credentials('kubeconfig-prod')
    DOCKER_CREDS = credentials('dockerhub-creds')
  }

  stages {
    stage('Test') {
      agent { docker { image 'golang:1.22-alpine' } }
      steps {
        sh 'go test ./... -coverprofile=coverage.out'
      }
    }

    stage('Build & Push Image') {
      steps {
        script {
          docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-creds') {
            def img = docker.build("${REGISTRY}:${IMAGE_TAG}")
            img.push()
            img.push('latest')
          }
        }
      }
    }

    stage('Deploy to Kubernetes') {
      steps {
        sh """
          kubectl --kubeconfig=$KUBECONFIG set image \
            deployment/go-backend \
            backend=${REGISTRY}:${IMAGE_TAG} \
            -n app

          kubectl --kubeconfig=$KUBECONFIG rollout status \
            deployment/go-backend \
            -n app \
            --timeout=120s
        """
      }
    }
  }

  post {
    failure {
      // Auto-rollback on deploy failure
      sh "kubectl --kubeconfig=$KUBECONFIG rollout undo deployment/go-backend -n app"
    }
  }
}
Official Documentation
DAY 07
Multibranch Pipelines & Webhooks
Intermediate · ~4 hours · GitHub webhooks, branch strategies, PR builds
Goal: Set up Multibranch Pipeline so feature branches get isolated builds, PRs get tested, and only main deploys to production.
Multibranch Jenkinsfile with Branch Logic
Jenkinsfile — multibranch with branch conditions
pipeline {
  agent any

  triggers {
    // Triggered by GitHub webhook automatically
    githubPush()
  }

  stages {
    stage('Test') {
      agent { docker { image 'golang:1.22-alpine' } }
      steps {
        sh 'go test ./... -race -coverprofile=coverage.out'
      }
    }

    stage('Build Image') {
      when {
        anyOf {
          branch 'main'
          branch 'release/*'
        }
      }
      steps {
        script {
          docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-creds') {
            def tag = env.BRANCH_NAME == 'main' ? 'latest' : env.BRANCH_NAME.replace('/', '-')
            docker.build("yourdockerhubuser/go-backend:${tag}").push()
          }
        }
      }
    }

    stage('Deploy Staging') {
      when { branch 'release/*' }
      steps {
        sh "kubectl set image deployment/go-backend backend=yourdockerhubuser/go-backend:${BRANCH_NAME.replace('/','-')} -n staging"
      }
    }

    stage('Deploy Production') {
      when { branch 'main' }
      steps {
        input message: 'Deploy to production?', ok: 'Deploy'
        sh "kubectl set image deployment/go-backend backend=yourdockerhubuser/go-backend:latest -n app"
        sh "kubectl rollout status deployment/go-backend -n app --timeout=120s"
      }
    }
  }
}
Official Documentation
DAY 08
Helm Charts — Package & Version Releases
Advanced · ~5 hours · Helm 3, templating, Jenkins + Helm deploy
Goal: Package the 2-tier app as a Helm chart so deployments are parameterised, versioned and easily rolled back.
Helm Chart Structure
helm-chart/ ├── Chart.yaml ← chart metadata ├── values.yaml ← default values └── templates/ ├── deployment.yaml ← go-backend Deployment ├── service.yaml ← ClusterIP Service ├── mongo-ss.yaml ← MongoDB StatefulSet ├── secret.yaml ← Mongo credentials ├── configmap.yaml ← App config └── _helpers.tpl ← named templates
Chart.yaml
helm-chart/Chart.yaml
apiVersion: v2
name: go-mongo-app
description: Golang REST API + MongoDB 2-tier app
type: application
version: 0.1.0
appVersion: "1.0.0"
values.yaml
helm-chart/values.yaml
backend:
  image:
    repository: yourdockerhubuser/go-backend
    tag: latest
    pullPolicy: Always
  replicas: 2
  resources:
    requests:
      cpu: 100m
      memory: 64Mi
    limits:
      cpu: 500m
      memory: 128Mi

mongodb:
  image: mongo:7.0
  storage: 5Gi

namespace: app
Helm Deployment Template
helm-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "go-mongo-app.fullname" . }}
  namespace: {{ .Values.namespace }}
  labels:
    {{- include "go-mongo-app.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.backend.replicas }}
  selector:
    matchLabels:
      app: go-backend
  template:
    metadata:
      labels:
        app: go-backend
    spec:
      containers:
        - name: backend
          image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
          imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
          env:
            - name: MONGO_URI
              valueFrom:
                secretKeyRef:
                  name: mongo-secret
                  key: mongo-uri
Jenkins Helm Deploy Stage
Jenkinsfile — Helm upgrade stage
stage('Helm Deploy') {
  steps {
    sh """
      helm upgrade --install go-mongo-app ./helm-chart \
        --namespace app \
        --create-namespace \
        --set backend.image.tag=${IMAGE_TAG} \
        --set backend.replicas=2 \
        --atomic \
        --timeout 3m \
        --wait
    """
  }
}
Official Documentation
DAY 09
Secrets, Environment Config & Security Hardening
Advanced · ~4 hours · K8s Secrets, Jenkins Credentials, RBAC
Goal: Properly manage MongoDB credentials, Docker registry auth, and Kubernetes RBAC so Jenkins can deploy with least privilege.
Kubernetes Secret for MongoDB URI
k8s/secrets.yaml — Never commit plaintext secrets
apiVersion: v1
kind: Secret
metadata:
  name: mongo-secret
  namespace: app
type: Opaque
stringData:
  # In prod: use Sealed Secrets or External Secrets Operator
  mongo-uri: "mongodb://admin:CHANGEME@mongodb.app.svc.cluster.local:27017/appdb"
---
# Docker registry pull secret
apiVersion: v1
kind: Secret
metadata:
  name: dockerhub-pull-secret
  namespace: app
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: <base64-encoded-docker-config>
RBAC — Jenkins Service Account
k8s/jenkins-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins-deployer
  namespace: app
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: jenkins-deploy-role
  namespace: app
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "update", "patch"]
  - apiGroups: [""]
    resources: ["pods", "services"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jenkins-deploy-binding
  namespace: app
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jenkins-deploy-role
subjects:
  - kind: ServiceAccount
    name: jenkins-deployer
    namespace: app
Jenkins Credentials Binding in Pipeline
Jenkinsfile — using credentials() binding
environment {
  DOCKER_HUB = credentials('dockerhub-creds')    // binds USR + PSW
  KUBECONFIG = credentials('kubeconfig-prod')     // file credential
}

steps {
  sh '''
    echo $DOCKER_HUB_PSW | docker login -u $DOCKER_HUB_USR --password-stdin
    kubectl --kubeconfig=$KUBECONFIG get pods -n app
  '''
}
Official Documentation
DAY 10
Production Pipeline — HPA, Monitoring & GitOps Patterns
Advanced · ~6 hours · HPA, Prometheus, ArgoCD integration, full pipeline
Goal: Graduate to a fully production-ready CI/CD system with autoscaling, observability hooks, and the complete Jenkinsfile for the 2-tier app.
Horizontal Pod Autoscaler
k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: go-backend-hpa
  namespace: app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-backend
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
Final Production Jenkinsfile
Jenkinsfile — FINAL production pipeline (all stages)
pipeline {
  agent any

  options {
    timeout(time: 20, unit: 'MINUTES')
    buildDiscarder(logRotator(numToKeepStr: '10'))
    disableConcurrentBuilds()
  }

  environment {
    REGISTRY      = "yourdockerhubuser/go-backend"
    IMAGE_TAG     = "${env.GIT_COMMIT[0..7]}"
    DOCKER_CREDS  = credentials('dockerhub-creds')
    KUBECONFIG    = credentials('kubeconfig-prod')
    SLACK_CHANNEL = '#deployments'
    NAMESPACE     = 'app'
  }

  stages {
    stage('Checkout & Info') {
      steps {
        checkout scm
        sh 'git log --oneline -5'
      }
    }

    stage('Unit Tests') {
      agent { docker { image 'golang:1.22-alpine'; reuseNode true } }
      steps {
        sh 'go mod download'
        sh 'go test ./... -race -coverprofile=coverage.out -json | tee test-report.json'
        sh 'go tool cover -html=coverage.out -o coverage.html'
      }
      post {
        always {
          archiveArtifacts 'coverage.html'
        }
      }
    }

    stage('Build & Push') {
      steps {
        script {
          docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-creds') {
            def img = docker.build("${REGISTRY}:${IMAGE_TAG}",
              "--label git-commit=${env.GIT_COMMIT} .")
            img.push()
            if (env.BRANCH_NAME == 'main') { img.push('latest') }
          }
        }
      }
    }

    stage('Security Scan') {
      steps {
        sh """
          docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
            aquasec/trivy:latest image --exit-code 1 --severity HIGH,CRITICAL \
            ${REGISTRY}:${IMAGE_TAG}
        """
      }
    }

    stage('Deploy to K8s') {
      when { branch 'main' }
      steps {
        sh """
          helm upgrade --install go-mongo-app ./helm-chart \
            --namespace ${NAMESPACE} \
            --create-namespace \
            --set backend.image.tag=${IMAGE_TAG} \
            --atomic \
            --timeout 3m \
            --wait \
            --kubeconfig=$KUBECONFIG
        """
      }
    }

    stage('Smoke Test') {
      when { branch 'main' }
      steps {
        sh """
          sleep 5
          kubectl --kubeconfig=$KUBECONFIG get pods -n ${NAMESPACE}
          POD=$(kubectl --kubeconfig=$KUBECONFIG get pod -l app=go-backend -n ${NAMESPACE} -o name | head -1)
          kubectl --kubeconfig=$KUBECONFIG exec $POD -n ${NAMESPACE} -- \
            wget -qO- http://localhost:8080/health | grep 'ok'
        """
      }
    }
  }

  post {
    success {
      slackSend(channel: env.SLACK_CHANNEL,
        color: 'good',
        message: "✅ Deploy OK | ${env.JOB_NAME} #${env.BUILD_NUMBER} | ${REGISTRY}:${IMAGE_TAG}")
    }
    failure {
      sh "helm --kubeconfig=$KUBECONFIG rollback go-mongo-app -n ${NAMESPACE} || true"
      slackSend(channel: env.SLACK_CHANNEL,
        color: 'danger',
        message: "❌ Deploy FAILED | ${env.JOB_NAME} #${env.BUILD_NUMBER} | Auto-rolled back")
    }
    always {
      cleanWs()
    }
  }
}
Official Documentation