Definition
- 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.
# 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"]
docker build -t nbi/sip-investment-service:1.0.0 .
docker images nbi/sip-investment-service
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.
<!-- 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">>
# 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
[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.
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.
# .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.
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=
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.
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 |
# 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.
# 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.
# 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
# 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
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.
---