Deploy Java application with Complete CI/CD pipeline Jenkins

Here’s a clean, end-to-end CI/CD example for a typical Java (Spring Boot) app using Jenkins → Docker → Helm → Kubernetes. You can swap the deploy step for EC2/Tomcat if you prefer (I’ve added that variant at the end).

1) Prerequisites (once)

  • Jenkins (LTS), with:
    • Pipeline, Git, Credentials Binding, Docker Pipeline, Blue Ocean
    • Optional: SonarQube Scanner, Slack, Kubernetes, OWASP Dependency-Check/Trivy
  • Tools: JDK 17, Maven 3.9+, Docker CLI/daemon on the agent, kubectl & helm installed (for K8s deploy)
  • A Kubernetes cluster (dev/stage/prod namespaces) and a registry (Docker Hub, ECR, GHCR, etc.)
  • In Jenkins » Manage Credentials:
    • dockerhub-creds (username/password or token)
    • kubeconfig (Secret file or string for the target cluster)
    • sonar-token (Secret text), if using SonarQube
    • slack-webhook (Secret text), if notifying Slack

2) Minimal project files

Dockerfile (simple, secure base; runs non-root):

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY target/*.jar app.jar
USER app
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]

pom.xml (key test/coverage bits):

<build>
  <plugins>
    <plugin>
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <version>0.8.12</version>
      <executions>
        <execution>
          <goals><goal>prepare-agent</goal></goals>
        </execution>
        <execution>
          <id>report</id>
          <phase>verify</phase>
          <goals><goal>report</goal></goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Optional sonar-project.properties:

sonar.projectKey=myorg:myapp
sonar.java.binaries=target/classes
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml

Helm chart (skeleton) helm/myapp/values.yaml:

image:
  repository: myorg/myapp
  tag: "latest"
  pullPolicy: IfNotPresent

app:
  replicas: 2

resources: {}
service:
  port: 80
  targetPort: 8080

Helm Deployment template helm/myapp/templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: {{ .Values.app.replicas }}
  selector:
    matchLabels: { app: myapp }
  template:
    metadata:
      labels: { app: myapp }
    spec:
      containers:
        - name: myapp
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet: { path: /actuator/health, port: 8080 }
            initialDelaySeconds: 10
          livenessProbe:
            httpGet: { path: /actuator/health, port: 8080 }
            initialDelaySeconds: 30

3) The Jenkinsfile (Declarative, multibranch-friendly)

  • Builds, tests, runs Sonar (optional), builds & scans the image, pushes it, then deploys via Helm.
  • Uses parameters to promote to dev/stage/prod.
pipeline {
  agent any

  options {
    timestamps()
    ansiColor('xterm')
    buildDiscarder(logRotator(numToKeepStr: '20'))
    disableConcurrentBuilds()
  }

  parameters {
    choice(name: 'ENV', choices: ['dev', 'stage', 'prod'], description: 'Target environment')
    string(name: 'APP_VERSION', defaultValue: '', description: 'Optional image/app version (defaults to git SHA)')
  }

  environment {
    REGISTRY      = 'docker.io'
    IMAGE_REPO    = 'myorg/myapp'
    DOCKER_CREDS  = 'dockerhub-creds'
    KUBECONFIG_ID = 'kubeconfig'
    SONARQUBE_ENV = 'sonarqube' // Name in Jenkins Global Tool Config (optional)
  }

  stages {

    stage('Checkout') {
      steps {
        checkout scm
        script {
          def sha = sh(returnStdout: true, script: "git rev-parse --short HEAD").trim()
          env.BUILD_TAGGED = params.APP_VERSION?.trim() ? params.APP_VERSION.trim() : sha
        }
      }
    }

    stage('Set JDK & Maven') {
      tools {
        jdk 'jdk17'        // Configure in Jenkins Global Tool Configuration
        maven 'maven3'     // Configure in Jenkins Global Tool Configuration
      }
      steps { sh 'java -version && mvn -v' }
    }

    stage('Build & Unit Tests') {
      steps {
        sh 'mvn -B -U -DskipTests=false clean verify'
      }
      post {
        always {
          junit 'target/surefire-reports/*.xml'
          publishHTML(target: [reportDir: 'target/site/jacoco', reportFiles: 'index.html', reportName: 'JaCoCo Coverage'])
        }
      }
    }

    stage('Static Analysis (SonarQube)') {
      when { anyOf { branch 'main'; branch 'master' } }
      environment { SONAR_TOKEN = credentials('sonar-token') }
      steps {
        withSonarQubeEnv("${SONARQUBE_ENV}") {
          sh """
            mvn -B sonar:sonar \
              -Dsonar.login=${SONAR_TOKEN} \
              -Dsonar.projectKey=myorg:myapp
          """
        }
      }
    }

    stage('Quality Gate') {
      when { anyOf { branch 'main'; branch 'master' } }
      steps {
        timeout(time: 5, unit: 'MINUTES') {
          waitForQualityGate abortPipeline: true
        }
      }
    }

    stage('Build Docker Image') {
      steps {
        script {
          docker.withRegistry("https://${env.REGISTRY}", DOCKER_CREDS) {
            def app = docker.build("${IMAGE_REPO}:${BUILD_TAGGED}")
            app.push()
            // Also push a branch tag (optional)
            sh "docker tag ${IMAGE_REPO}:${BUILD_TAGGED} ${IMAGE_REPO}:${env.BRANCH_NAME}"
            sh "docker push ${IMAGE_REPO}:${env.BRANCH_NAME}"
          }
        }
      }
    }

    stage('Container Security Scan (Trivy)') {
      steps {
        sh """
          trivy image --exit-code 0 --severity HIGH,CRITICAL ${IMAGE_REPO}:${BUILD_TAGGED} || true
        """
        // Optionally fail on critical vulns:
        // sh "trivy image --exit-code 1 --severity CRITICAL ${IMAGE_REPO}:${BUILD_TAGGED}"
      }
      post {
        always {
          archiveArtifacts artifacts: 'trivy-report*.*', allowEmptyArchive: true
        }
      }
    }

    stage('Deploy (Helm → Kubernetes)') {
      steps {
        withCredentials([file(credentialsId: "${KUBECONFIG_ID}", variable: 'KCFG')]) {
          sh """
            export KUBECONFIG=${KCFG}
            kubectl config current-context
            helm upgrade --install myapp ./helm/myapp \
              --namespace ${params.ENV} --create-namespace \
              --set image.repository=${IMAGE_REPO} \
              --set image.tag=${BUILD_TAGGED} \
              --set app.replicas=${params.ENV == 'prod' ? 4 : 2} \
              --atomic --timeout 5m
          """
        }
      }
    }
  }

  post {
    success {
      echo "Deployed ${IMAGE_REPO}:${BUILD_TAGGED} to ${params.ENV}"
      // Example Slack notify:
      // withCredentials([string(credentialsId: 'slack-webhook', variable: 'HOOK')]) {
      //   sh "curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"✅ ${JOB_NAME} #${BUILD_NUMBER} → ${params.ENV} (${BUILD_TAGGED})\"}' $HOOK"
      // }
    }
    failure {
      echo "Build failed; check logs."
    }
    always {
      cleanWs(deleteDirs: true)
    }
  }
}

4) How this pipeline works (flow)

  1. Checkout: Pulls the repo; computes image tag (git SHA or supplied).
  2. Build & Test: mvn verify runs unit tests + JaCoCo.
  3. Static Analysis: SonarQube scan (on main), enforces quality gate.
  4. Containerize: Build/push Docker image to registry.
  5. Security Scan: Trivy scans image (you can fail on CRITICAL).
  6. Deploy: Helm upgrade/install to chosen namespace, with --atomic for safe rollback on failure.

5) Recommended Jenkins setup tips

  • Use Multibranch Pipeline job with GitHub/Bitbucket webhooks.
  • Use cooperative agents (Docker or K8s agents) so builds are reproducible.
  • Put common steps in a Shared Library (vars/ci.groovy) if multiple repos share patterns.
  • Store infra config in Git (Helm, K8s manifests) → GitOps workflows (Argo CD/Flux) if you prefer pull-based deploys.

6) Rollbacks / promotions

  • Kubernetes/Helm: helm rollback myapp <REVISION> -n <env>
  • Promotion: re-run the job with ENV=stage or prod using the same APP_VERSION (immutable image tag).

7) Variant: Deploy to a VM (Systemd) instead of K8s

Use SSH credentials and copy the JAR + restart a service.

Systemd unit on VM (/etc/systemd/system/myapp.service):

[Unit]
Description=MyApp
After=network.target

[Service]
User=myuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
Restart=always
Environment=JAVA_OPTS=-Xms256m -Xmx512m

[Install]
WantedBy=multi-user.target

Jenkins deploy stage replacing Helm:

stage('Deploy (VM)') {
  steps {
    sshagent(['vm-ssh-key']) {
      sh '''
        scp target/*.jar myuser@vm.example:/opt/myapp/app.jar
        ssh myuser@vm.example "sudo systemctl daemon-reload && sudo systemctl restart myapp && sudo systemctl status myapp --no-pager"
      '''
    }
  }
}

8) Security & quality checklist

  • enable.idempotence & acks=all for any Kafka producer downstream (if applicable).
  • Sign images (Sigstore cosign) and generate SBOM (Syft) if you need supply-chain attestations.
  • Run SAST (Sonar/CodeQL), SCA (OWASP Dependency-Check), and container scans (Trivy/Grype).
  • Keep secrets in Jenkins Credentials and pass via env/withCredentials; never commit secrets.
  • Use --atomic in Helm and readiness/liveness probes in K8s.
  • Use branch protections and PR checks; enforce quality gates.


Back to blog

Leave a comment

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