Deploy Java application with Complete CI/CD pipeline Jenkins

Ā 

0) Prereqs (one-time)

On Jenkins

  • Install tools: JDK 17, Maven 3.9+, Docker (on agents), kubectl, Helm.

  • Plugins: Pipeline, Git, Credentials Binding, Blue Ocean (optional), JUnit, Jacoco, SonarQube Scanner (optional), Kubernetes plugin (if using K8s agents).

  • Credentials:

    • dockerhub-creds (Username/Password or Token)

    • kubeconfig-dev, kubeconfig-staging, kubeconfig-prod (Secret files or Kube tokens)

    • sonar-token (optional)

  • Global Tools in Jenkins → Manage Jenkins → Global Tool Configuration:

    • Name JDK: temurin17

    • Name Maven: maven-3

    • Name Sonar: sonar-lts (if used)

In your Git repo

.
ā”œā”€ pom.xml
ā”œā”€ src/...
ā”œā”€ Jenkinsfile
ā”œā”€ Dockerfile
└─ charts/
   └─ app/          # Helm chart skeleton
      ā”œā”€ Chart.yaml
      ā”œā”€ values.yaml
      └─ templates/
         ā”œā”€ deployment.yaml
         ā”œā”€ service.yaml
         └─ ingress.yaml (optional)

1) Jenkinsfile (Declarative) — build, test, scan, containerize, push, deploy

Drop this at the repo root as Jenkinsfile. Adjust names/IDs as needed.

pipeline {
  agent any
  options {
    timestamps()
    ansiColor('xterm')
    disableConcurrentBuilds()
    buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '10'))
  }
  environment {
    APP_NAME     = 'my-java-app'
    REGISTRY     = 'registry.hub.docker.com/your-org'    // e.g., ghcr.io/your-org
    IMAGE        = "${env.REGISTRY}/${env.APP_NAME}"
    MAVEN_OPTS   = '-Dmaven.test.failure.ignore=false -DskipITs'
    JAVA_HOME    = tool name: 'temurin17', type: 'jdk'
    PATH         = "${JAVA_HOME}/bin:${env.PATH}"
  }
  tools {
    maven 'maven-3'
  }
  triggers {
    // Build every push; add cron/PR triggers as needed
    pollSCM('H/5 * * * *')
  }

  stages {

    stage('Checkout') {
      steps {
        checkout scm
        script {
          env.GIT_SHA = sh(script: "git rev-parse --short=12 HEAD", returnStdout: true).trim()
          env.BUILD_TAGGED = "${env.GIT_SHA}-${env.BUILD_NUMBER}"
        }
      }
    }

    stage('Build & Unit Test') {
      steps {
        sh 'mvn -B -U clean test -Djacoco.skip=false'
      }
      post {
        always {
          junit 'target/surefire-reports/*.xml'
          jacoco execPattern: 'target/jacoco.exec', classPattern: 'target/classes', sourcePattern: 'src/main/java'
        }
      }
    }

    stage('Static Analysis (optional Sonar)') {
      when { expression { return false } } // flip to true if using Sonar
      environment { SONAR_SCANNER_HOME = tool 'sonar-lts' }
      steps {
        withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_TOKEN')]) {
          sh """
            mvn -B sonar:sonar \
              -Dsonar.projectKey=${APP_NAME} \
              -Dsonar.host.url=https://sonar.my-company.com \
              -Dsonar.login=$SONAR_TOKEN
          """
        }
      }
      // Optionally wait for Quality Gate with 'waitForQualityGate()'
    }

    stage('Package') {
      steps {
        sh "mvn -B -DskipTests package"
        stash includes: 'target/*.jar', name: 'jar'
      }
      post {
        success { archiveArtifacts artifacts: 'target/*.jar', fingerprint: true }
      }
    }

    stage('Docker Build') {
      steps {
        unstash 'jar'
        script {
          sh """
            docker build \
              --build-arg JAR_FILE=$(ls target/*.jar | head -n1) \
              -t ${IMAGE}:${BUILD_TAGGED} \
              -t ${IMAGE}:latest .
          """
        }
      }
    }

    stage('Image Scan (Trivy optional)') {
      when { expression { return false } } // set true if Trivy installed on agent
      steps {
        sh "trivy image --exit-code 0 --severity HIGH,CRITICAL ${IMAGE}:${BUILD_TAGGED}"
      }
    }

    stage('Push Image') {
      steps {
        withCredentials([usernamePassword(credentialsId: 'dockerhub-creds', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
          sh """
            echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin ${REGISTRY}
            docker push ${IMAGE}:${BUILD_TAGGED}
            docker push ${IMAGE}:latest
          """
        }
      }
    }

    stage('Deploy DEV') {
      steps {
        withCredentials([file(credentialsId: 'kubeconfig-dev', variable: 'KUBECONFIG')]) {
          sh """
            helm upgrade --install ${APP_NAME} charts/app \
              --namespace dev --create-namespace \
              --set image.repository=${IMAGE} \
              --set image.tag=${BUILD_TAGGED} \
              --set replicaCount=3
          """
        }
      }
    }

    stage('Smoke Tests (DEV)') {
      steps {
        // Replace with your healthcheck/smoke suite
        sh "echo 'Run smoke tests against DEV endpoint...' "
      }
    }

    stage('Promote to STAGING?') {
      when { branch 'main' }
      steps {
        timeout(time: 30, unit: 'MINUTES') {
          input message: "Promote image ${BUILD_TAGGED} to STAGING?"
        }
      }
    }

    stage('Deploy STAGING') {
      when { branch 'main' }
      steps {
        withCredentials([file(credentialsId: 'kubeconfig-staging', variable: 'KUBECONFIG')]) {
          sh """
            helm upgrade --install ${APP_NAME} charts/app \
              --namespace staging --create-namespace \
              --set image.repository=${IMAGE} \
              --set image.tag=${BUILD_TAGGED} \
              --set replicaCount=4 \
              --set resources.limits.cpu=500m
          """
        }
      }
    }

    stage('e2e Tests (STAGING)') {
      when { branch 'main' }
      steps {
        sh "echo 'Run E2E tests here (e.g., REST-assured, Postman, k6)...'"
      }
    }

    stage('Promote to PROD?') {
      when { branch 'main' }
      steps {
        timeout(time: 30, unit: 'MINUTES') {
          input message: "Promote image ${BUILD_TAGGED} to PROD?"
        }
      }
    }

    stage('Deploy PROD (Rolling)') {
      when { branch 'main' }
      steps {
        withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) {
          sh """
            helm upgrade --install ${APP_NAME} charts/app \
              --namespace prod --create-namespace \
              --set image.repository=${IMAGE} \
              --set image.tag=${BUILD_TAGGED} \
              --set replicaCount=6 \
              --set strategy=RollingUpdate
          """
        }
      }
    }
  }

  post {
    success { echo "āœ… Deployed ${APP_NAME} image ${BUILD_TAGGED}" }
    failure { echo "āŒ Build failed. Check stages above."; }
    always  { cleanWs() }
  }
}

2) Dockerfile (multi-stage, lean runtime)

# ---- Build stage (optional if Jenkins already packages the jar) ----
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -B -q -e -DskipTests dependency:go-offline
COPY src ./src
RUN mvn -B -q package -DskipTests

# ---- Runtime stage ----
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
# JVM flags: adjust for container limits
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75 -XX:InitialRAMPercentage=50 -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
EXPOSE 8080
HEALTHCHECK --interval=20s --timeout=3s --start-period=20s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java","-jar","/app/app.jar"]

If Jenkins already runs mvn package and stashes the JAR, the build stage is unused; the runtime stage copies the prebuilt JAR via --build-arg JAR_FILE=target/app.jar.


3) Minimal Helm chart (values + deployment)

charts/app/values.yaml

replicaCount: 2
image:
  repository: registry.hub.docker.com/your-org/my-java-app
  tag: latest
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

resources:
  requests:
    cpu: 200m
    memory: 512Mi
  limits:
    cpu: "1"
    memory: 1Gi

env:
  - name: JAVA_OPTS
    value: "-Xms256m -Xmx768m"

livenessProbe:
  path: /actuator/health/liveness
readinessProbe:
  path: /actuator/health/readiness

charts/app/templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "app.fullname" . }}
spec:
  replicas: {{ .Values.replicaCount }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: {{ include "app.name" . }}
  template:
    metadata:
      labels:
        app: {{ include "app.name" . }}
    spec:
      containers:
        - name: app
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.service.targetPort }}
          env: {{- toYaml .Values.env | nindent 12 }}
          resources: {{- toYaml .Values.resources | nindent 12 }}
          livenessProbe:
            httpGet:
              path: {{ .Values.livenessProbe.path }}
              port: {{ .Values.service.targetPort }}
          readinessProbe:
            httpGet:
              path: {{ .Values.readinessProbe.path }}
              port: {{ .Values.service.targetPort }}

charts/app/templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ include "app.fullname" . }}
spec:
  type: {{ .Values.service.type }}
  selector:
    app: {{ include "app.name" . }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.targetPort }}

(Add ingress if needed.)


4) Branching & promotion model

  • feature branches / PRs → run Checkout, Build, Test, Static Analysis only (guard with when { changeRequest() } if you want).

  • main → full pipeline → DEV → STAGING → PROD with manual approvals.

  • Tag releases (v1.2.3) to create immutable docker tags and chart versions.


5) Quality & security gates (optional but recommended)

  • SonarQube: fail the build on Quality Gate fail (use waitForQualityGate() in a scripted block).

  • Trivy/Snyk: image and dependency scans; fail on Critical findings.

  • Jacoco: enforce minimum line/branch coverage in pom.xml.


6) Rollback

  • Every deploy is a Helm release revision:
    helm history my-java-app -n prod
    helm rollback my-java-app <REV> -n prod

  • Keep last known good image tag (e.g., prod-lkg) for fast rollbacks:

    helm upgrade ... --set image.tag=${GOOD_TAG}
    

7) Fast local smoke test (before pushing)

mvn -B -U clean test
mvn -B package
docker build -t myapp:local .
docker run -p 8080:8080 myapp:local
curl localhost:8080/actuator/health

TL;DR checklist

  • Jenkins tools & creds created

  • Jenkinsfile committed

  • Dockerfile multi-stage or runtime-only

  • Helm chart added

  • Kube credentials per env

  • Optional gates: Sonar, Trivy, E2E

  • Approvals for STAGING/PROD

Back to blog

Leave a comment

Please note, comments need to be approved before they are published.