Java HTTP App DevOps Pipeline
Complete guide: from mvn clean package to production pods. For engineers who grep before they Google.
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
| Component | Role | Why It Matters |
|---|---|---|
| Maven | Build automation & dependency mgmt | Reproducible builds, lifecycle phases, plugin ecosystem |
| Jenkins | CI/CD orchestration | Pipeline-as-code, parallel stages, approval gates |
| Docker | Container runtime & image format | Immutable artifacts, environment parity |
| Helm | K8s package manager | Templated manifests, release management, rollbacks |
| Kubernetes | Container orchestration | Self-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+
kubectlconfigured 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)
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
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
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"]
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 Image | Approx Size | Use Case |
|---|---|---|
eclipse-temurin:17 | ~340 MB | Full JDK, debugging |
eclipse-temurin:17-jre | ~220 MB | Standard runtime |
eclipse-temurin:17-jre-alpine | ~130 MB | Production (smallest) |
gcr.io/distroless/java17 | ~190 MB | No 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>
: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.' } } }
Helm Chart Structure
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
--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
| Probe | Endpoint | Purpose |
|---|---|---|
| Startup | /actuator/health/startup | Is the app ready to accept probes? (gates liveness/readiness) |
| Readiness | /actuator/health/readiness | Can the app handle traffic? (controls Service endpoints) |
| Liveness | /actuator/health/liveness | Is 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
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
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
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
| Status | Cause | Debug Command |
|---|---|---|
Pending | No node has enough resources | kubectl describe pod <name> → Events |
CrashLoopBackOff | App crashes on startup | kubectl logs <pod> --previous |
ImagePullBackOff | Wrong image or missing credentials | kubectl describe pod → check imagePullSecrets |
OOMKilled | Exceeded memory limit | Increase limits.memory or fix leak |
CreateContainerConfigError | Missing ConfigMap/Secret | Verify 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
| Flag | Effect | When to Use |
|---|---|---|
-XX:MaxRAMPercentage=75.0 | Heap = 75% of container memory limit | Always (replaces -Xmx) |
-XX:InitialRAMPercentage=50.0 | Initial heap = 50% of limit | Reduce GC during warmup |
-XX:MinRAMPercentage=25.0 | Minimum heap for small containers | Containers < 256Mi |
-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
| GC | Best For | Flag |
|---|---|---|
| G1GC | General purpose, balanced latency/throughput | -XX:+UseG1GC |
| ZGC | Ultra-low latency (<1ms pauses), large heaps | -XX:+UseZGC |
| Shenandoah | Low latency (OpenJDK only) | -XX:+UseShenandoahGC |
| SerialGC | Small 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
--atomicflag 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)