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, thebuild
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