DevNotes
Support
Microservices Fundamentals · Chapter 11

Deploying Microservices — Docker, Kubernetes & CI/CD

Containerizing Spring Boot with a multi-stage Dockerfile and layered jars, Docker Compose for local development, Kubernetes Deployments, Services, probes, resources and autoscaling, graceful shutdown, rolling, blue-green and canary releases, and CI/CD pipelines.

◷ 6 min read§ 9 sections? 5 interview questions{ } 0 code examples

#Introduction

Microservices are only practical with automated, repeatable deployment. Containers package each service with its runtime; an orchestrator like Kubernetes runs, heals and scales them; CI/CD pipelines take every commit from code to production safely.

#Containerizing a Spring Boot Service

A Multi-Stage Dockerfile

DOCKERFILE
# ---- build stage ----
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests \
 && java -Djarmode=tools -jar target/*.jar extract --layers --launcher --destination extracted

# ---- runtime stage ----
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd --system --uid 10001 appuser
COPY --from=build /app/extracted/dependencies/ ./
COPY --from=build /app/extracted/spring-boot-loader/ ./
COPY --from=build /app/extracted/snapshot-dependencies/ ./
COPY --from=build /app/extracted/application/ ./
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "org.springframework.boot.loader.launch.JarLauncher"]
PracticeWhy
Multi-stage buildThe final image contains a JRE and the app — not Maven, sources or the JDK
Layered jarDependencies change rarely, so their layer is cached; rebuilds only push the small application layer
Non-root userLimits the damage if the container is compromised
-XX:MaxRAMPercentageThe JVM sizes its heap from the container's memory limit
Pinned, minimal base imageSmaller attack surface; reproducible builds

Docker Compose for Local Development

YAML
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: orders
      POSTGRES_PASSWORD: secret
  kafka:
    image: apache/kafka:latest
    ports: ["9092:9092"]
  order-service:
    build: ./order-service
    ports: ["8080:8080"]
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/orders
      SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
    depends_on: [postgres, kafka]

Spring Boot's spring-boot-docker-compose support can also start a compose.yaml automatically when you run the app locally.

#Kubernetes Essentials

ObjectPurpose
PodOne or more containers running together — the unit Kubernetes schedules
DeploymentKeeps N replicas of a pod running; performs rolling updates
ServiceStable DNS name and load balancing across ready pods
Ingress / Gateway APIRoutes external HTTP traffic into the cluster
ConfigMap / SecretConfiguration and credentials
HorizontalPodAutoscalerScales replicas on CPU, memory or custom metrics
NamespaceGroups and isolates resources per team or environment

A Deployment

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels: { app: order-service }
  template:
    metadata:
      labels: { app: order-service }
    spec:
      containers:
        - name: order-service
          image: registry.example.com/order-service:1.4.2
          ports: [{ containerPort: 8080 }]
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: prod
          resources:
            requests: { cpu: "500m", memory: "768Mi" }
            limits:   { memory: "768Mi" }
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: 8080 }
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /actuator/health/liveness, port: 8080 }
            periodSeconds: 10
          startupProbe:
            httpGet: { path: /actuator/health/liveness, port: 8080 }
            failureThreshold: 30
            periodSeconds: 5
SettingWhy
Readiness probeNo traffic until the app is ready, or while it's temporarily unhealthy
Liveness probeRestart a stuck container
Startup probeGive slow-starting JVMs time before liveness checks begin
Resource requestsLet the scheduler place pods correctly
Memory limitProtect the node; the JVM sizes itself from it (MaxRAMPercentage)
Immutable image tags1.4.2, never latest, so deployments are reproducible

Autoscaling

YAML
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: order-service }
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }

For Kafka consumers, scaling on consumer lag (e.g. with KEDA) often works better than CPU — and replicas beyond the partition count add nothing.

#Graceful Shutdown

During a rolling update, Kubernetes sends SIGTERM. Let in-flight requests finish:

YAML
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Spring Boot stops accepting new requests, completes in-flight ones, closes Kafka consumers and producers (committing offsets) and then exits. Keep terminationGracePeriodSeconds in the pod spec longer than this timeout.

#Release Strategies

StrategyHowTrade-off
Rolling update (Kubernetes default)Replace pods a few at a timeSimple; old and new versions run side by side briefly
Blue-greenRun the new version (green) fully alongside the old (blue), then switch all trafficInstant rollback; needs double capacity
CanarySend a small share of traffic (e.g. 5%) to the new version, watch metrics, then increaseLimits blast radius; needs traffic splitting (mesh, ingress, Argo Rollouts)
Feature flagsDeploy code dark, enable features at runtime per user groupDecouples deploy from release

#CI/CD Pipelines

Commit
Build and unit tests
Static analysis and security scans
Container image
Integration and contract tests
Deploy to staging
Deploy to production — canary, then full
  • One pipeline per service, so services deploy independently.
  • Testcontainers for integration tests with real databases and Kafka.
  • Contract tests (Pact, Spring Cloud Contract) to catch breaking API changes.
  • Image and dependency scanning (Trivy, OWASP Dependency-Check, Snyk).
  • GitOps (Argo CD, Flux): the desired state of each environment lives in Git, and the cluster syncs to it.

#The Twelve-Factor App (Highlights)

FactorIn practice
CodebaseOne repo (or module) per service, many deploys
DependenciesDeclared explicitly (Maven/Gradle) and packaged in the image
ConfigIn the environment, not the code
Backing servicesDatabases and brokers attached through configuration
ProcessesStateless — state lives in databases, caches or Kafka
DisposabilityFast startup, graceful shutdown
Dev/prod paritySame images and similar dependencies everywhere
LogsWrite to stdout; the platform collects them

#Interview Questions

Q1How do you containerize a Spring Boot application?

With a multi-stage Dockerfile that builds the jar and copies its extracted layers onto a minimal JRE image running as a non-root user — or with spring-boot:build-image (Buildpacks).

Q2What are liveness, readiness and startup probes?

Liveness restarts a stuck container; readiness controls whether a pod receives traffic; a startup probe delays liveness checks until a slow-starting app is up. Spring Boot provides /actuator/health/liveness and /readiness.

Q3What is graceful shutdown and why does it matter?

On SIGTERM, the app stops taking new requests and finishes in-flight work before exiting (server.shutdown=graceful), so rolling deployments don't fail user requests or lose messages.

Q4Rolling vs blue-green vs canary deployments?

Rolling replaces instances gradually; blue-green runs a full new environment and switches traffic at once (easy rollback); canary routes a small share of traffic to the new version and increases it as metrics stay healthy.

Q5How do you handle database changes with zero downtime?

Expand–contract migrations: add new structures in a backward-compatible way, deploy code that uses both, migrate data, then remove the old structures in a later release.

#Key Takeaways

  • Build small, layered, non-root images with a JRE — or use Buildpacks.
  • Kubernetes Deployments, Services, probes, resources and HPAs run and scale services.
  • Configure graceful shutdown so rollouts don't drop requests.
  • Choose rolling, blue-green or canary releases; keep every change backward compatible.
  • One CI/CD pipeline per service, with integration, contract and security checks.