Java HTTP App DevOps Pipeline

Complete guide: from mvn clean package to production pods. For engineers who grep before they Google.

Git
Jenkins
Maven
Docker
Helm
Kubernetes

Architecture Overview

The pipeline flows through six stages, each isolated and independently testable. Failures at any stage halt downstream progression.

Source Code (Git)
    ↓
Jenkins (CI/CD Orchestration)
    ↓
Maven Build & Test           // compile, unit tests, SAST
    ↓
Docker Image Creation         // multi-stage build, CVE scan
    ↓
Registry (Harbor/ECR/ACR)     // immutable tags, signature
    ↓
Helm Chart Packaging          // lint, template, version
    ↓
Kubernetes Deployment         // rollout, probes, HPA
    ↓
Production / Staging          // smoke tests, monitoring

Key Components

ComponentRoleWhy It Matters
MavenBuild automation & dependency mgmtReproducible builds, lifecycle phases, plugin ecosystem
JenkinsCI/CD orchestrationPipeline-as-code, parallel stages, approval gates
DockerContainer runtime & image formatImmutable artifacts, environment parity
HelmK8s package managerTemplated manifests, release management, rollbacks
KubernetesContainer orchestrationSelf-healing, autoscaling, declarative state

🔧 Prerequisites

Infrastructure Requirements

  • Kubernetes cluster 1.24+ (EKS, GKE, AKS, or self-managed)
  • Private container registry (Harbor, ECR, ACR, Artifactory)
  • Jenkins server 2.300+ with pipeline plugins
  • Maven 3.8.1+ with JDK 17+
  • kubectl configured with cluster access
  • Helm 3.0+

Required Jenkins Plugins

Pipeline                    // Core pipeline engine
Docker Pipeline             // Docker build/push steps
Kubernetes plugin           // Dynamic pod-based agents
Git plugin                  // SCM checkout
Maven Integration           // Maven tool management
Credentials Binding         // Secret injection
Pipeline: Stage View        // Visual pipeline progress
Blue Ocean                  // Modern UI (recommended)
Kubernetes CLI              // kubectl/helm steps

Version Check

# Verify all tools are installed and accessible
java -version               # JDK 17+ required for modern Spring Boot
mvn --version               # 3.8.1+
docker --version            # 20.10+
kubectl version --client    # Match cluster minor version ±1
helm version                # 3.x only (Helm 2 is EOL)
Pro tip:

Use asdf or mise to manage tool versions per-project with a .tool-versions file. No more "works on my machine".

Maven Build Process

Project Structure

my-java-app/ ├── pom.xml # Build config & dependencies ├── src/ │ ├── main/ │ │ ├── java/com/example/ │ │ │ └── App.java │ │ └── resources/ │ │ ├── application.yml # Spring Boot config │ │ └── logback.xml # Logging config │ └── test/ │ ├── java/ │ └── resources/ ├── Dockerfile # Multi-stage container build ├── helm/my-java-app/ # Helm chart └── Jenkinsfile # Pipeline definition

POM Configuration

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-java-app</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- Spring Boot Parent: manages dependency versions, plugin defaults -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.5</version>
    </parent>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <!-- Enables /actuator/prometheus endpoint -->
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals><goal>repackage</goal></goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0</version>
            </plugin>
        </plugins>
    </build>
</project>

Build Lifecycle Phases

mvn clean              # Delete target/
mvn compile            # Compile source → target/classes
mvn test               # Run unit tests (Surefire)
mvn package            # Create JAR in target/
mvn verify             # Run integration tests (Failsafe)
mvn install            # Install to ~/.m2/repository
mvn deploy             # Deploy to remote artifact repo

Maven Settings for Private Repos

<!-- ~/.m2/settings.xml -->
<settings>
    <servers>
        <server>
            <id>nexus-releases</id>
            <username>${env.NEXUS_USER}</username>
            <password>${env.NEXUS_TOKEN}</password>
        </server>
    </servers>
    <mirrors>
        <mirror>
            <id>nexus</id>
            <mirrorOf>*</mirrorOf>
            <url>https://nexus.company.com/repository/maven-public</url>
        </mirror>
    </mirrors>
</settings>

Common Build Commands

# Full build with tests
mvn clean package

# Skip tests (dev iteration speed)
mvn clean package -DskipTests

# Build a specific profile
mvn clean package -Pproduction

# Verbose debug output
mvn clean package -X

# Dependency analysis
mvn dependency:tree                    # Full dependency graph
mvn dependency:analyze                 # Find unused / undeclared deps
mvn versions:display-dependency-updates  # Check for newer versions

# Run specific test class or method
mvn test -Dtest=UserControllerTest
mvn test -Dtest=UserControllerTest#testGetUser

# Parallel builds (use available cores)
mvn clean package -T 1C               # 1 thread per CPU core

Build Artifacts

target/ ├── my-java-app-1.0.0.jar # Fat JAR (executable) ├── my-java-app-1.0.0.jar.original # Original (before repackage) ├── classes/ # Compiled .class files ├── test-classes/ # Compiled test classes └── surefire-reports/ # JUnit XML + text reports

🐳 Docker Containerization

Multi-Stage Dockerfile

# ---- Stage 1: Build ----
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app

# Cache dependencies (changes less often than source)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Copy source and build
COPY src/ src/
RUN mvn clean package -DskipTests -B

# ---- Stage 2: Runtime ----
FROM eclipse-temurin:17-jre-alpine

# Non-root user for security
RUN addgroup -S app && adduser -S app -G app

WORKDIR /app
COPY --from=builder /app/target/my-java-app-*.jar app.jar
RUN chown -R app:app /app

USER app

HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
    CMD wget -qO- http://localhost:8080/actuator/health || exit 1

EXPOSE 8080

# JVM flags: use percentage-based memory (container-aware)
ENV JAVA_OPTS="-XX:+UseG1GC \
    -XX:MaxRAMPercentage=75.0 \
    -XX:InitialRAMPercentage=50.0 \
    -XX:+UseStringDeduplication \
    -Djava.security.egd=file:/dev/./urandom"

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Why sh -c?

Using ENTRYPOINT ["sh", "-c", ...] allows the $JAVA_OPTS env var to be expanded at runtime. With exec form ["java", ...], env vars are not interpolated.

Layer Caching Strategy

# BAD: any source change invalidates dependency cache
COPY . /app
RUN mvn clean package

# GOOD: dependencies cached separately from source
COPY pom.xml .
RUN mvn dependency:go-offline -B    # Cached until pom.xml changes
COPY src/ src/
RUN mvn clean package -DskipTests   # Only rebuilds source layer

Image Size Comparison

Base ImageApprox SizeUse Case
eclipse-temurin:17~340 MBFull JDK, debugging
eclipse-temurin:17-jre~220 MBStandard runtime
eclipse-temurin:17-jre-alpine~130 MBProduction (smallest)
gcr.io/distroless/java17~190 MBNo shell, max security

Build & Push

# Build with tag
docker build -t my-registry.azurecr.io/my-java-app:1.0.0 .

# Multi-tag build
docker build \
  -t my-registry.azurecr.io/my-java-app:1.0.0 \
  -t my-registry.azurecr.io/my-java-app:latest .

# Build with build args
docker build --build-arg JAVA_VERSION=17 -t my-java-app:1.0.0 .

# Inspect layers and size
docker history my-java-app:1.0.0
docker image inspect my-java-app:1.0.0 --format='{{.Size}}'

# Push to registry
docker login my-registry.azurecr.io
docker push my-registry.azurecr.io/my-java-app:1.0.0

# Scan for CVEs before pushing
trivy image --severity HIGH,CRITICAL my-java-app:1.0.0
docker scout cves my-java-app:1.0.0

Local Testing

# Run with Spring profile and port mapping
docker run -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=dev \
  -e JAVA_OPTS="-XX:MaxRAMPercentage=75.0" \
  my-java-app:1.0.0

# Mount external config
docker run -p 8080:8080 \
  -v $(pwd)/config:/app/config \
  my-java-app:1.0.0

# Verify health
curl -s http://localhost:8080/actuator/health | jq .

# Follow logs
docker logs -f <container-id>
Never use :latest in production.

Always pin exact versions. :latest is mutable and makes rollbacks impossible to reason about. Use Git SHA or semver tags.

🛠 Jenkins Pipeline

Declarative Pipeline (Jenkinsfile)

pipeline {
    agent any

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 1, unit: 'HOURS')
        timestamps()
    }

    parameters {
        string(name: 'VERSION', defaultValue: '1.0.0', description: 'App version')
        choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'])
        booleanParam(name: 'SKIP_TESTS', defaultValue: false)
    }

    environment {
        REGISTRY   = 'my-registry.azurecr.io'
        IMAGE_NAME = 'my-java-app'
        IMAGE_TAG  = "${params.VERSION}"
    }

    stages {
        stage('Checkout') {
            steps { checkout scm }
        }

        stage('Build') {
            steps {
                withEnv(['MAVEN_OPTS=-Xmx1024m']) {
                    sh """
                        mvn clean package \
                            -DskipTests=\${SKIP_TESTS} \
                            -Drevision=\${VERSION} -B -U
                    """
                }
            }
        }

        stage('Unit Tests') {
            when { expression { !params.SKIP_TESTS } }
            steps {
                sh 'mvn test -Drevision=${VERSION} -B'
            }
            post {
                always { junit '**/surefire-reports/*.xml' }
            }
        }

        stage('Code Quality') {
            parallel {
                stage('SonarQube') {
                    steps {
                        sh """
                            mvn sonar:sonar \
                                -Dsonar.projectKey=my-java-app \
                                -Dsonar.host.url=https://sonar.company.com
                        """
                    }
                }
                stage('OWASP Dep Check') {
                    steps {
                        sh 'mvn org.owasp:dependency-check-maven:check'
                    }
                }
            }
        }

        stage('Docker Build & Push') {
            steps {
                withDockerRegistry([credentialsId: 'docker-creds']) {
                    sh """
                        docker build \
                            --build-arg VERSION=\${VERSION} \
                            -t \${REGISTRY}/\${IMAGE_NAME}:\${IMAGE_TAG} \
                            -t \${REGISTRY}/\${IMAGE_NAME}:latest .

                        trivy image --severity HIGH,CRITICAL \
                            --exit-code 0 \
                            \${REGISTRY}/\${IMAGE_NAME}:\${IMAGE_TAG}

                        docker push \${REGISTRY}/\${IMAGE_NAME}:\${IMAGE_TAG}
                        docker push \${REGISTRY}/\${IMAGE_NAME}:latest
                    """
                }
            }
        }

        stage('Helm Lint & Package') {
            steps {
                sh """
                    helm lint helm/my-java-app --strict
                    helm package helm/my-java-app \
                        --version \${VERSION} --app-version \${VERSION}
                """
            }
        }

        stage('Deploy to Dev') {
            when { expression { params.ENVIRONMENT in ['dev', 'staging'] } }
            steps {
                withCredentials([file(credentialsId: 'kubeconfig-dev', variable: 'KUBECONFIG')]) {
                    sh """
                        helm upgrade --install my-java-app helm/my-java-app \
                            --namespace dev --create-namespace \
                            --values helm/my-java-app/values-dev.yaml \
                            --set image.tag=\${IMAGE_TAG} \
                            --set image.repository=\${REGISTRY}/\${IMAGE_NAME} \
                            --timeout 5m --wait --atomic

                        kubectl rollout status deployment/my-java-app \
                            -n dev --timeout=5m
                    """
                }
            }
        }

        stage('Smoke Tests') {
            when { expression { params.ENVIRONMENT in ['dev', 'staging'] } }
            steps {
                sh """
                    kubectl run smoke-test --rm -i --restart=Never \
                        --image=curlimages/curl -n dev -- \
                        curl -sf http://my-java-app:8080/actuator/health/readiness
                """
            }
        }

        stage('Deploy to Production') {
            when {
                allOf {
                    expression { params.ENVIRONMENT == 'production' }
                    branch 'main'
                }
            }
            input { message "Deploy to production?"; ok "Deploy" }
            steps {
                withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) {
                    sh """
                        helm upgrade --install my-java-app helm/my-java-app \
                            --namespace production --create-namespace \
                            --values helm/my-java-app/values-production.yaml \
                            --set image.tag=\${IMAGE_TAG} \
                            --set image.repository=\${REGISTRY}/\${IMAGE_NAME} \
                            --timeout 10m --wait --atomic

                        kubectl rollout status deployment/my-java-app \
                            -n production --timeout=10m
                    """
                }
            }
        }
    }

    post {
        always  { cleanWs(deleteDirs: true) }
        success { echo 'Pipeline succeeded!' }
        failure { echo 'Pipeline failed - check logs.' }
    }
}

📚 Jenkins Shared Libraries

Reusable pipeline code for teams with multiple Java services.

vars/mavenBuild.groovy

def call(String version, boolean skipTests = false) {
    sh """
        mvn clean package \
            -DskipTests=\${skipTests} \
            -Drevision=\${version} -B
    """
}

Usage in Pipeline

@Library('shared-library') _

pipeline {
    agent any
    stages {
        stage('Build') {
            steps { script { mavenBuild('1.0.0', false) } }
        }
    }
}
Shared library structure:

Place under vars/ for global pipeline steps, src/ for helper classes, and resources/ for non-Groovy files. Configure in Jenkins → Manage Jenkins → Configure System → Global Pipeline Libraries.

Helm Chart Structure

helm/my-java-app/ ├── Chart.yaml # Chart metadata & version ├── values.yaml # Default values ├── values-dev.yaml # Dev overrides ├── values-staging.yaml # Staging overrides ├── values-production.yaml # Production overrides ├── templates/ │ ├── _helpers.tpl # Template helpers & labels │ ├── deployment.yaml # Deployment spec │ ├── service.yaml # ClusterIP / LoadBalancer │ ├── ingress.yaml # Ingress rules + TLS │ ├── configmap.yaml # Application config │ ├── hpa.yaml # Horizontal Pod Autoscaler │ ├── pdb.yaml # Pod Disruption Budget │ ├── serviceaccount.yaml # SA + IRSA/Workload Identity │ └── secret.yaml # Sealed/External secrets └── README.md

Chart.yaml

apiVersion: v2
name: my-java-app
description: Helm chart for Java Spring Boot HTTP application
type: application
version: 1.0.0          # Chart version (change on chart modifications)
appVersion: "1.0.0"     # Application version (matches Docker tag)
keywords: [java, spring-boot, http]
maintainers:
  - name: DevOps Team
    email: devops@company.com

values.yaml (Production-Grade)

# ---- Image ----
image:
  repository: my-registry.azurecr.io/my-java-app
  tag: "1.0.0"
  pullPolicy: IfNotPresent

# ---- Replicas & Scaling ----
replicaCount: 3

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 75
  targetMemoryUtilizationPercentage: 80

podDisruptionBudget:
  enabled: true
  minAvailable: 1

# ---- Resources ----
resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

# ---- Security ----
podSecurityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000
  seccompProfile:
    type: RuntimeDefault

securityContext:
  allowPrivilegeEscalation: false
  capabilities:
    drop: [ALL]
  readOnlyRootFilesystem: true

# ---- Probes ----
startupProbe:
  enabled: true
  httpGet:
    path: /actuator/health/startup
    port: http
  periodSeconds: 10
  failureThreshold: 30          # 5 min max startup time

readinessProbe:
  enabled: true
  httpGet:
    path: /actuator/health/readiness
    port: http
  initialDelaySeconds: 5
  periodSeconds: 10

livenessProbe:
  enabled: true
  httpGet:
    path: /actuator/health/liveness
    port: http
  initialDelaySeconds: 60
  periodSeconds: 10

# ---- Networking ----
service:
  type: ClusterIP
  port: 8080

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/rate-limit: "100"
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: my-java-app-tls
      hosts: [api.example.com]

# ---- Environment ----
env:
  - name: SERVER_PORT
    value: "8080"
  - name: MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE
    value: "health,metrics,prometheus,info"
  - name: SPRING_APPLICATION_NAME
    value: my-java-app

# ---- Pod Annotations (Prometheus scrape) ----
podAnnotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "8080"
  prometheus.io/path: "/actuator/prometheus"

# ---- Scheduling ----
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values: [my-java-app]
          topologyKey: kubernetes.io/hostname

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0           # Zero-downtime rollouts

📄 Helm Templates

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-java-app.fullname" . }}
  labels:
    {{- include "my-java-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  strategy:
    {{- toYaml .Values.strategy | nindent 4 }}
  selector:
    matchLabels:
      {{- include "my-java-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        # Trigger rollout on ConfigMap change
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      labels:
        {{- include "my-java-app.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ include "my-java-app.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 8080
          env:
            {{- toYaml .Values.env | nindent 12 }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}

templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ include "my-java-app.fullname" . }}
  labels:
    {{- include "my-java-app.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "my-java-app.selectorLabels" . | nindent 4 }}

templates/ingress.yaml

{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "my-java-app.fullname" . }}
  labels:
    {{- include "my-java-app.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  ingressClassName: {{ .Values.ingress.className }}
  {{- if .Values.ingress.tls }}
  tls:
    {{- range .Values.ingress.tls }}
    - hosts:
        {{- range .hosts }}
        - {{ . | quote }}
        {{- end }}
      secretName: {{ .secretName }}
    {{- end }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "my-java-app.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}
{{- end }}

🚀 Helm Operations

# ---- Validate ----
helm lint helm/my-java-app --strict
helm template my-java-app helm/my-java-app --values values-dev.yaml

# ---- Install ----
helm install my-java-app helm/my-java-app \
  --namespace dev --create-namespace \
  --values helm/my-java-app/values-dev.yaml \
  --wait --timeout 5m

# ---- Upgrade (or install if not exists) ----
helm upgrade --install my-java-app helm/my-java-app \
  --namespace dev --atomic --timeout 5m

# ---- Diff before upgrade (requires helm-diff plugin) ----
helm diff upgrade my-java-app helm/my-java-app \
  --values values-dev.yaml -n dev

# ---- Inspect ----
helm get values my-java-app -n dev
helm get manifest my-java-app -n dev
helm history my-java-app -n dev

# ---- Rollback ----
helm rollback my-java-app 1 -n dev

# ---- Package & Distribute ----
helm package helm/my-java-app --version 1.0.0
helm push my-java-app-1.0.0.tgz oci://registry.company.com/charts

# ---- Cleanup ----
helm uninstall my-java-app -n dev
Always use --atomic.

On failed upgrades, --atomic auto-rolls back to the last successful release. Without it, you're left in a broken half-deployed state.

Kubernetes Setup

# Verify cluster access
kubectl cluster-info
kubectl get nodes -o wide

# Create namespaces
kubectl create namespace dev
kubectl create namespace staging
kubectl create namespace production

# Create image pull secret
kubectl create secret docker-registry regcred \
  --docker-server=my-registry.azurecr.io \
  --docker-username=$REGISTRY_USER \
  --docker-password=$REGISTRY_TOKEN \
  -n dev

# Set default namespace for context
kubectl config set-context --current --namespace=dev

# Deploy via Helm
helm install my-java-app helm/my-java-app \
  --namespace dev --create-namespace \
  --values values-dev.yaml --wait --timeout 5m

# Verify
kubectl get deployment,pods,svc -n dev
kubectl logs -n dev -l app=my-java-app --tail=50

# Port forward for local testing
kubectl port-forward -n dev svc/my-java-app 8080:8080

📝 Raw K8s Manifests

For teams that prefer raw YAML over Helm, or for understanding what Helm generates.

Deployment + Service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-java-app
  namespace: dev
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
  selector:
    matchLabels: { app: my-java-app }
  template:
    metadata:
      labels: { app: my-java-app, version: "1.0.0" }
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/actuator/prometheus"
    spec:
      serviceAccountName: my-java-app
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        seccompProfile: { type: RuntimeDefault }
      containers:
      - name: app
        image: my-registry.azurecr.io/my-java-app:1.0.0
        ports:
        - { name: http, containerPort: 8080 }
        env:
        - { name: SPRING_PROFILES_ACTIVE, value: "kubernetes,dev" }
        - { name: JAVA_OPTS, value: "-XX:+UseG1GC -XX:MaxRAMPercentage=75.0" }
        resources:
          requests: { cpu: 250m, memory: 256Mi }
          limits:   { cpu: 500m, memory: 512Mi }
        startupProbe:
          httpGet: { path: /actuator/health/startup, port: http }
          periodSeconds: 10
          failureThreshold: 30
        readinessProbe:
          httpGet: { path: /actuator/health/readiness, port: http }
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet: { path: /actuator/health/liveness, port: http }
          initialDelaySeconds: 60
        securityContext:
          allowPrivilegeEscalation: false
          capabilities: { drop: [ALL] }
          readOnlyRootFilesystem: true
        volumeMounts:
        - { name: tmp, mountPath: /tmp }
      volumes:
      - { name: tmp, emptyDir: {} }
      imagePullSecrets:
      - { name: regcred }
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels: { app: my-java-app }
              topologyKey: kubernetes.io/hostname
---
apiVersion: v1
kind: Service
metadata:
  name: my-java-app
  namespace: dev
spec:
  type: ClusterIP
  ports:
  - { port: 8080, targetPort: http, name: http }
  selector: { app: my-java-app }

HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-java-app
  namespace: dev
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-java-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target: { type: Utilization, averageUtilization: 75 }
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # Wait 5m before scaling down
    scaleUp:
      stabilizationWindowSeconds: 0     # Scale up immediately

🩹 Health & Monitoring

Spring Boot Actuator Config

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus,info,threaddump
      base-path: /actuator
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true            # Enables /health/liveness & /health/readiness
  metrics:
    export:
      prometheus:
        enabled: true            # Enables /actuator/prometheus
    tags:
      application: ${spring.application.name}

Probe Endpoints

ProbeEndpointPurpose
Startup/actuator/health/startupIs the app ready to accept probes? (gates liveness/readiness)
Readiness/actuator/health/readinessCan the app handle traffic? (controls Service endpoints)
Liveness/actuator/health/livenessIs the app alive? (failure triggers container restart)

Prometheus Scrape Annotations

# In pod annotations (already in Helm values)
podAnnotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "8080"
  prometheus.io/path: "/actuator/prometheus"
# Verify metrics endpoint is working
kubectl port-forward -n dev svc/my-java-app 8080:8080
curl -s http://localhost:8080/actuator/prometheus | head -20

# Key JVM metrics to watch:
# jvm_memory_used_bytes{area="heap"}
# jvm_gc_pause_seconds_sum
# jvm_threads_live_threads
# http_server_requests_seconds_count
Grafana Dashboard:

Import dashboard ID 4701 (JVM Micrometer) for instant Spring Boot observability. Pair with 12740 for Kubernetes pod metrics.

🔁 GitOps with ArgoCD

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-java-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/company/my-java-app
    targetRevision: main
    path: helm/my-java-app
    helm:
      valueFiles:
      - values.yaml
      - values-prod.yaml
      parameters:
      - name: image.tag
        value: "1.0.0"
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true           # Delete resources removed from Git
      selfHeal: true        # Revert manual cluster changes
    syncOptions:
    - CreateNamespace=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        maxDuration: 3m
GitOps flow:

CI pipeline builds image → updates values.yaml image tag in Git → ArgoCD detects drift → syncs new state to cluster. No kubectl apply in CI.

🎯 Deployment Strategies

Blue-Green

# Deploy green (new version) alongside blue (current)
helm install my-java-app-green helm/my-java-app \
  --namespace production \
  --set version=green \
  --set image.tag=1.1.0

# Verify green is healthy
kubectl rollout status deployment/my-java-app-green -n production

# Switch traffic by updating Service selector
kubectl patch svc my-java-app -n production \
  -p '{"spec":{"selector":{"version":"green"}}}'

# Verify, then remove blue
helm uninstall my-java-app-blue -n production

Canary with Istio

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: my-java-app
spec:
  hosts: [my-java-app]
  http:
  - route:
    - destination:
        host: my-java-app
        subset: v1
      weight: 90              # 90% to stable
    - destination:
        host: my-java-app
        subset: v2
      weight: 10              # 10% canary
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: my-java-app
spec:
  host: my-java-app
  trafficPolicy:
    outlierDetection:
      consecutiveErrors: 5
      interval: 30s
      baseEjectionTime: 30s
  subsets:
  - name: v1
    labels: { version: v1 }
  - name: v2
    labels: { version: v2 }

Multi-Region

# Deploy to multiple clusters
for cluster in us-east us-west eu-west; do
  kubectl config use-context $cluster
  helm upgrade --install my-java-app helm/my-java-app \
    --namespace production \
    --values values-${cluster}.yaml \
    --atomic --timeout 5m
done

🔒 Secrets Management

Sealed Secrets (Bitnami)

# Create a secret, seal it, commit sealed version to Git
echo -n 'mypassword' | kubectl create secret generic db-creds \
  --dry-run=client --from-file=password=/dev/stdin -o yaml | \
  kubeseal --format yaml > sealed-db-creds.yaml

# Apply sealed secret (controller decrypts in-cluster)
kubectl apply -f sealed-db-creds.yaml

External Secrets Operator

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: db-credentials
  data:
  - secretKey: password
    remoteRef:
      key: production/db-credentials
      property: password
Never commit secrets to Git.

Use Sealed Secrets, External Secrets Operator, or inject from Vault/AWS SM/Azure KV at runtime. Base64 is encoding, not encryption.

🐛 Troubleshooting

Maven Build Issues

# Nuke local repo (nuclear option)
rm -rf ~/.m2/repository

# Force re-download all dependencies
mvn clean package -U

# Increase Maven memory
export MAVEN_OPTS="-Xmx2048m -XX:+UseG1GC"

# Find conflicting dependencies
mvn dependency:tree -Dverbose | grep "omitted for conflict"

# Verbose compilation output
mvn clean compile -X 2>&1 | tail -100

Docker Issues

# Build fails: check disk space
docker system df
docker image prune -a              # Remove all unused images
docker builder prune --all         # Clear build cache

# Debug build (show full output)
docker build --progress=plain --no-cache -t my-app:1.0.0 .

# Registry push fails
docker login my-registry.azurecr.io
docker image ls | grep my-java-app  # Verify image exists locally

Kubernetes Pod Failures

StatusCauseDebug Command
PendingNo node has enough resourceskubectl describe pod <name> → Events
CrashLoopBackOffApp crashes on startupkubectl logs <pod> --previous
ImagePullBackOffWrong image or missing credentialskubectl describe pod → check imagePullSecrets
OOMKilledExceeded memory limitIncrease limits.memory or fix leak
CreateContainerConfigErrorMissing ConfigMap/SecretVerify referenced resources exist
# ---- Full Pod Debug Flow ----

# 1. Status overview
kubectl get pods -n dev -o wide

# 2. Detailed pod info + events
kubectl describe pod <name> -n dev

# 3. Logs (current + previous crash)
kubectl logs <pod> -n dev --tail=200
kubectl logs <pod> -n dev --previous

# 4. Exec into container
kubectl exec -it <pod> -n dev -- /bin/sh

# 5. Network debug
kubectl run netshoot --rm -it --image=nicolaka/netshoot -n dev -- bash
# Inside: dig my-java-app.dev.svc.cluster.local
# Inside: curl -sv http://my-java-app:8080/actuator/health

# 6. Check endpoints & network policies
kubectl get endpoints my-java-app -n dev
kubectl get networkpolicy -n dev

# 7. Resource usage
kubectl top pods -n dev --containers
kubectl top nodes

Performance Profiling in K8s

# Java Flight Recorder (in-pod profiling)
kubectl exec -it <pod> -n dev -- \
  jcmd 1 JFR.start name=profile duration=60s filename=/tmp/profile.jfr

# Extract recording
kubectl cp dev/<pod>:/tmp/profile.jfr ./profile.jfr

# Thread dump (deadlock analysis)
kubectl exec <pod> -n dev -- jcmd 1 Thread.print

# Heap dump (memory leak analysis)
kubectl exec <pod> -n dev -- jcmd 1 GC.heap_dump /tmp/heap.hprof
kubectl cp dev/<pod>:/tmp/heap.hprof ./heap.hprof

Helm Troubleshooting

# Template rendering issues
helm install my-java-app ./helm/my-java-app --dry-run --debug

# Lint for syntax errors
helm lint ./helm/my-java-app --strict

# Check release status
helm history my-java-app -n dev

# Rollback (auto-rollback with --atomic on upgrade)
helm rollback my-java-app 1 -n dev --cleanup-on-fail

Certificate & RBAC Issues

# Check cert expiry
kubectl get secret my-java-app-tls -o jsonpath='{.data.tls\.crt}' | \
  base64 -d | openssl x509 -noout -dates

# Check service account permissions
kubectl auth can-i get pods \
  --as=system:serviceaccount:dev:my-java-app -n dev

# List all bindings for a namespace
kubectl get rolebindings -n dev -o wide

JVM Tuning for Containers

Java in containers has specific gotchas. These flags are essential for production.

Recommended JVM Flags

JAVA_OPTS="
  -XX:+UseG1GC                     # G1 GC (best for most workloads)
  -XX:MaxRAMPercentage=75.0         # Use 75% of container memory limit
  -XX:InitialRAMPercentage=50.0     # Start at 50% to reduce GC on startup
  -XX:+UseStringDeduplication       # Reduce heap for string-heavy apps
  -XX:+ExitOnOutOfMemoryError       # Let K8s restart on OOM (don't hang)
  -XX:+HeapDumpOnOutOfMemoryError   # Dump heap before crashing
  -XX:HeapDumpPath=/tmp/heapdump.hprof
  -Djava.security.egd=file:/dev/./urandom  # Faster startup (SecureRandom)
  -Dfile.encoding=UTF-8
"

Container-Aware Memory

FlagEffectWhen to Use
-XX:MaxRAMPercentage=75.0Heap = 75% of container memory limitAlways (replaces -Xmx)
-XX:InitialRAMPercentage=50.0Initial heap = 50% of limitReduce GC during warmup
-XX:MinRAMPercentage=25.0Minimum heap for small containersContainers < 256Mi
Don't use -Xmx in containers.

Hard-coded -Xmx512m ignores the container's actual memory limit. Use -XX:MaxRAMPercentage instead — it adapts to whatever K8s gives you. Leave 25% headroom for off-heap (metaspace, threads, NIO buffers).

GC Selection Guide

GCBest ForFlag
G1GCGeneral purpose, balanced latency/throughput-XX:+UseG1GC
ZGCUltra-low latency (<1ms pauses), large heaps-XX:+UseZGC
ShenandoahLow latency (OpenJDK only)-XX:+UseShenandoahGC
SerialGCSmall containers (<256Mi), single-core-XX:+UseSerialGC

Startup Optimization

# CDS (Class Data Sharing) - reduces startup by 20-30%
# Step 1: Generate class list during build
java -XX:DumpLoadedClassList=classes.lst -jar app.jar --exit

# Step 2: Create shared archive
java -Xshare:dump -XX:SharedClassListFile=classes.lst \
  -XX:SharedArchiveFile=app-cds.jsa -jar app.jar

# Step 3: Use in container ENTRYPOINT
java -Xshare:on -XX:SharedArchiveFile=app-cds.jsa -jar app.jar

# Spring Boot specific: use lazy initialization for dev
# spring.main.lazy-initialization=true  (NOT for production)

Deployment Checklist

Pre-Deployment

  • Java application compiles successfully (mvn clean package)
  • All unit tests pass (mvn test)
  • Code quality gates met (SonarQube/Checkstyle)
  • No HIGH/CRITICAL dependency vulnerabilities (OWASP check)
  • Docker image builds without errors (multi-stage)
  • Image CVE scan passes (Trivy/Scout)
  • Helm chart lints without errors (helm lint --strict)
  • Non-root user in container
  • Image tagged with immutable version (not :latest)

Deployment

  • Kubernetes cluster is accessible (kubectl cluster-info)
  • Image pull secrets created in target namespace
  • Namespace created with resource quotas
  • Network policies defined (default-deny + allow rules)
  • RBAC roles & service accounts configured
  • ConfigMaps & Secrets deployed before app
  • Helm --atomic flag used for safe rollback

Post-Deployment

  • All pods running and Ready (kubectl get pods)
  • Startup, readiness & liveness probes passing
  • Service endpoints populated (kubectl get endpoints)
  • Ingress routing configured with TLS
  • Prometheus scraping metrics (/actuator/prometheus)
  • HPA configured and responding to load
  • PDB in place for safe voluntary disruptions
  • Smoke tests passing from inside the cluster
  • Alerting rules configured (error rate, latency, OOM)