Back to Maven & Gradle
Q1Question 1 of 4

DevOps: Docker multi-stage, K8s deployment, CI/CD pipeline, Maven lifecycle

Key Concepts
  • Docker multi-stage builds compile with JDK in one stage and copy only the JAR into a slim JRE stage, cutting image size from 600 MB to under 200 MB.
  • The Maven lifecycle runs ordered phases -- validate, compile, test, package, verify, install, deploy -- and invoking a later phase executes all earlier ones.
  • Kubernetes Deployments with maxUnavailable 0 and readiness probes give zero-downtime rolling updates.
  • A CI/CD pipeline automates build, test, SonarQube analysis, Docker image push, and Kubernetes rolling deploy on every merge to main.
  • Configuration is injected through ConfigMaps and Secrets, never baked into the image, so the same image runs in dev, staging, and production.
  • HorizontalPodAutoscaler watches CPU and scales replicas automatically during peak load like salary day.
  • Tagging images with the Git commit SHA makes every deployment traceable to an exact commit and enables one-line rollback.

Definition

Docker multi-stage builds produce small images, Kubernetes orchestrates them with probes and autoscaling, a CI/CD pipeline automates the path from commit to production, and the Maven lifecycle orders the build phases.
  • Docker multi-stage build -- compiles source code in a full JDK stage and copies only the finished JAR into a slim JRE stage, so the production image carries no compiler or source code.
  • Kubernetes Deployment -- declares the desired state (replicas, image, probes) and performs rolling updates with zero downtime by starting new Pods before terminating old ones.
  • CI/CD pipeline -- automates the sequence of build, test, static analysis, image push, and deploy so no human touches production by hand.
  • Maven lifecycle -- a fixed ordered chain of phases (validate, compile, test, package, verify, install, deploy) where invoking any phase runs all earlier ones.
  • HorizontalPodAutoscaler -- watches a metric such as CPU and automatically scales the Pod count up or down to handle load.
  • SonarQube quality gate -- a static analysis check that can fail the pipeline if coverage drops or critical bugs appear.

Think of National Bank of India building a new SIP investment counter. Docker is like sealing the counter, the staff, and all tools into one portable shipping container so it runs identically in every branch. The multi-stage build assembles it in a big workshop with heavy tools, then ships only the finished lightweight counter. Kubernetes is the branch manager who opens fresh counters before closing old ones so customers are never turned away. The CI/CD pipeline is the automatic quality-control line that builds, inspects, and rolls out every change without anyone copying files by hand.

Understanding the Concepts

What is a Docker multi-stage build?

A Docker multi-stage build uses two or more FROM statements in a single Dockerfile. The first stage uses a heavy base image that contains the full JDK and Maven to compile the source code. The second stage starts fresh from a slim JRE image and copies only the finished JAR. This means the production image does not carry the compiler, Maven, or the source code, which typically cuts the size from around 600 megabytes to under 200 megabytes. A smaller image means faster pulls, a smaller attack surface, and lower storage cost.

DOCKERFILE
# Stage 1 -- build with full JDK
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN apk add --no-cache maven \
    && mvn clean package -DskipTests

# Stage 2 -- run with slim JRE only
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /build/target/sip-investment-service-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
BASH
docker build -t nbi/sip-investment-service:1.0.0 .
docker images nbi/sip-investment-service
TEXT
REPOSITORY                      TAG     SIZE
nbi/sip-investment-service      1.0.0   187MB

How does the Maven build lifecycle work?

The Maven default lifecycle is a fixed ordered chain of phases. Invoking any phase runs that phase and all phases before it. The main phases are validate, compile, test, package, verify, install, and deploy. Plugins bind their work to phases, and you cannot reorder the chain. This rigidity is a feature because every Maven project builds the same way.

XML
<!-- pom.xml for the SIP Investment Service --syn-kw">>
syn-kw"><projectsyn-kw">>
  syn-kw"><groupIdsyn-kw">>com.bank.investmentsyn-kw"></groupIdsyn-kw">>
  syn-kw"><artifactIdsyn-kw">>sip-investment-servicesyn-kw"></artifactIdsyn-kw">>
  syn-kw"><versionsyn-kw">>1.0.0syn-kw"></versionsyn-kw">>
  syn-kw"><packagingsyn-kw">>jarsyn-kw"></packagingsyn-kw">>

  syn-kw"><parentsyn-kw">>
    syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
    syn-kw"><artifactIdsyn-kw">>spring-boot-starter-parentsyn-kw"></artifactIdsyn-kw">>
    syn-kw"><versionsyn-kw">>3.2.0syn-kw"></versionsyn-kw">>
  syn-kw"></parentsyn-kw">>

  syn-kw"><dependenciessyn-kw">>
    syn-kw"><dependencysyn-kw">>
      syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
      syn-kw"><artifactIdsyn-kw">>spring-boot-starter-websyn-kw"></artifactIdsyn-kw">>
    syn-kw"></dependencysyn-kw">>
  syn-kw"></dependenciessyn-kw">>
syn-kw"></projectsyn-kw">>
BASH
# compile only
mvn compile

# compile + test + package into a JAR
mvn package

# compile + test + package + install to local .m2
mvn install

# skip tests inside Docker build (they already ran in CI)
mvn clean package -DskipTests
TEXT
[INFO] --- maven-compiler-plugin:3.11.0:compile (default-compile) ---
[INFO] --- maven-surefire-plugin:3.2.2:test (default-test) ---
[INFO] Tests run: 24, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) ---
[INFO] BUILD SUCCESS

How does Kubernetes deploy with zero downtime?

A Kubernetes Deployment declares the desired state -- replica count, container image, probes, and update strategy. With a rolling update strategy where maxUnavailable is 0 and maxSurge is 1, Kubernetes starts a new Pod and waits for its readiness probe to pass before terminating an old Pod. This guarantees that the service never has fewer than the desired number of healthy Pods serving traffic.

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sip-investment-service
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: sip-investment-service
  template:
    metadata:
      labels:
        app: sip-investment-service
    spec:
      containers:
        - name: sip-investment-service
          image: nbi/sip-investment-service:abc1234
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 20
            periodSeconds: 10
          envFrom:
            - configMapRef:
                name: sip-config
            - secretRef:
                name: sip-secrets

How does the CI/CD pipeline automate the full path?

The CI/CD pipeline triggers on every merge to main. It runs the Maven build and tests, performs SonarQube static analysis with a quality gate, builds the Docker image tagged with the commit SHA, pushes it to a registry, and triggers the Kubernetes rolling update. Tagging by commit SHA makes every deployment traceable.

YAML
# .github/workflows/cicd.yml
name: CI/CD Pipeline
on:
  push:
    branches: [main]
jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - run: mvn clean verify
      - run: mvn sonar:sonar -Dsonar.qualitygate.wait=true

  docker-push:
    needs: build-test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          docker build -t nbi/sip-investment-service:${{ github.sha }} .
          docker push nbi/sip-investment-service:${{ github.sha }}

  deploy:
    needs: docker-push
    runs-on: ubuntu-latest
    steps:
      - run: |
          kubectl set image deployment/sip-investment-service \
            sip-investment-service=nbi/sip-investment-service:${{ github.sha }}
          kubectl rollout status deployment/sip-investment-service

How is configuration injected without baking it into the image?

Configuration is stored in Kubernetes ConfigMaps for non-secret values and Secrets for passwords. The container reads them as environment variables through envFrom, and Spring Boot automatically maps these variables to application properties. This means the same image runs in dev, staging, and production with different configuration.

YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: sip-config
data:
  SPRING_DATASOURCE_URL: jdbc:postgresql://ledger-db:5432/sipdb
  SIP_MAX_MONTHLY_AMOUNT: "500000"
---
apiVersion: v1
kind: Secret
metadata:
  name: sip-secrets
type: Opaque
data:
  SPRING_DATASOURCE_PASSWORD: cGFzc3dvcmQxMjM=
JAVA
package com.bank.investment.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import java.math.BigDecimal;

@Configuration
public class SipConfig {

    @Value("${sip.max-monthly-amount:100000}")
    private BigDecimal maxMonthlyAmount;

    public BigDecimal getMaxMonthlyAmount() {
        return maxMonthlyAmount;
    }
}

How does autoscaling handle peak load?

The HorizontalPodAutoscaler watches a metric such as average CPU utilization and scales the Pod count between a minimum and maximum. On salary day when thousands of customers set up SIP investments at once, the autoscaler adds Pods to absorb the spike, then scales back down afterward to save cost.

YAML
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: sip-investment-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sip-investment-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

How does Maven compare to Gradle?

Maven uses declarative XML with a fixed lifecycle and strong conventions. Gradle uses a programmable Groovy or Kotlin DSL with a task graph, incremental builds, and a build cache for speed. Maven favors standardization and predictability, while Gradle favors flexibility and performance.

| Feature | Maven | Gradle |
|---|---|---|
| Configuration | XML (pom.xml) | Groovy/Kotlin DSL |
| Build model | Fixed lifecycle phases | Directed task graph |
| Speed | Rebuilds eagerly | Incremental + cache + daemon |
| Learning curve | Low -- rigid and predictable | Higher -- flexible and scriptable |
| Bank preference | Auditable, uniform | Fast, complex custom builds |

BASH
# Maven: compile, test, package
mvn clean package

# Gradle: equivalent
./gradlew clean build

What Git branching model works for banking teams?

Trunk-based development with short-lived feature branches is common in banking platform teams. A developer branches off main, opens a pull request that triggers CI checks, gets a review, and merges back. Main stays always releasable.

BASH
# create feature branch
git checkout -b feature/sip-returns main

# work and commit
git add src/main/java/com/bank/investment/SipReturnsService.java
git commit -m "feat: add SIP returns calculation"

# push and open PR
git push -u origin feature/sip-returns
gh pr create --base main --title "feat: SIP returns calculation"

# after review and CI passes, merge
gh pr merge --squash

What are common mistakes in this DevOps setup?

The most common mistakes are baking secrets into the Docker image, using the latest tag instead of a commit SHA, running tests again inside the Docker build after they already passed in CI, and setting the HikariCP pool size equal to the Tomcat thread count.

DOCKERFILE
# WRONG -- secret baked into image
ENV DB_PASSWORD=secret123

# CORRECT -- injected at runtime through Kubernetes Secret
# No ENV in Dockerfile; use envFrom in the Deployment spec
YAML
# WRONG -- latest tag
image: nbi/sip-investment-service:latest

# CORRECT -- commit SHA tag
image: nbi/sip-investment-service:abc1234

What should a senior engineer know?

A senior engineer understands that skipping tests inside the Docker build with -DskipTests is safe only because the CI verify step already ran them. They know that the commit SHA tag is the join key that ties a running Pod back to an exact commit, a build log, and a changelog. They enforce that released images are immutable and never overwritten, that rollback is a single kubectl rollout undo, and that the quality gate in SonarQube fails the pipeline if coverage drops or critical bugs appear.

Visual Overview

DIAGRAM
  Developer Workstation           CI/CD Pipeline              Kubernetes Cluster
  ────────────────────           ───────────────              ──────────────────

  ┌─────────────────┐
  │ git push feature │
  │ branch to origin │
  └────────┬────────┘
           │
           ▼
  ┌─────────────────┐   trigger   ┌──────────────────────┐
  │  Open Pull       │──────────▶│  1. mvn clean verify  │
  │  Request         │           │  2. mvn sonar:sonar   │
  └─────────────────┘           │  3. Quality gate check │
                                 └──────────┬───────────┘
                                            │ pass
                                            ▼
                                 ┌──────────────────────┐
                                 │  4. docker build      │
                                 │     tag with SHA      │
                                 │  5. docker push       │
                                 └──────────┬───────────┘
                                            │
                                            ▼
                                 ┌──────────────────────┐    ┌───────────────┐
                                 │  6. kubectl set image │───▶│ Rolling Update │
                                 │  7. rollout status    │   │ Pod1(new) ✓   │
                                 └──────────────────────┘   │ Pod2(new) ✓   │
                                                             │ Pod3(new) ✓   │
                                                             └───────────────┘

  Maven Lifecycle (fixed order):
  ┌──────────┐   ┌─────────┐   ┌──────┐   ┌─────────┐   ┌────────┐   ┌─────────┐   ┌────────┐
  │ validate │──▶│ compile │──▶│ test │──▶│ package │──▶│ verify │──▶│ install │──▶│ deploy │
  └──────────┘   └─────────┘   └──────┘   └─────────┘   └────────┘   └─────────┘   └────────┘

Key Takeaway

In a banking project every microservice goes through a repeatable path from source code to production. The Docker multi-stage build compiles the code in a full JDK stage and copies only the finished JAR into a slim JRE image, cutting the size dramatically. The Maven lifecycle runs the fixed phases of validate, compile, test, package, verify, install, and deploy, where each phase runs all earlier ones, so one command does the whole build in a predictable, auditable order.

Kubernetes orchestrates the images in production. A Deployment keeps a fixed number of replicas alive, and a rolling update with maxUnavailable set to zero starts a new Pod and waits for its readiness probe to pass before terminating an old one, giving genuine zero-downtime deploys. Configuration is injected through ConfigMaps and Secrets so the same image runs unchanged across every environment, and a HorizontalPodAutoscaler watches CPU to scale Pods during peak load like salary day.

The CI/CD pipeline ties everything together. When a developer merges to main, the pipeline compiles, tests, runs SonarQube with a quality gate, builds the Docker image tagged with the commit SHA, pushes it to the registry, and triggers the rolling update. Tagging by SHA makes every deploy traceable to an exact commit and makes rollback a one-line command.

Remember one sentence: Docker multi-stage builds produce small images, Kubernetes rolling updates deploy them with zero downtime, and a CI/CD pipeline automates the entire path from commit to production so no human touches the release by hand.

---

Share
Use ← → arrow keys to navigate