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)
- Checkout: Pulls the repo; computes image tag (git SHA or supplied).
-
Build & Test:
mvn verify
runs unit tests + JaCoCo. - Static Analysis: SonarQube scan (on main), enforces quality gate.
- Containerize: Build/push Docker image to registry.
- Security Scan: Trivy scans image (you can fail on CRITICAL).
-
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
orprod
using the sameAPP_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.