Back to Scaling, Load Balancing & Performance
Q15Question 15 of 20

How do you deploy microservices to containers (Dockerfile, docker-compose, Kubernetes, scaling)

Key Concepts
  • A multi-stage Dockerfile builds an immutable image with only a slim JRE and the jar, no compiler or build tools.
  • docker-compose starts the service with its database and broker on a private network for local development.
  • A Kubernetes Deployment keeps a desired number of replica Pods alive and self-heals by replacing any that die.
  • A Kubernetes Service gives Pods one stable DNS name and load-balances across healthy instances.
  • Liveness and readiness probes via Spring Boot Actuator drive zero-downtime rolling updates.
  • HorizontalPodAutoscaler watches CPU or custom metrics and adjusts replicas between a min and max.
  • Resource requests and limits size each Pod; the cluster autoscaler adds machines when Pods cannot be scheduled.

Definition

The Dockerfile builds one immutable image, docker-compose runs a small set of containers together for local development, and Kubernetes runs them in production with self-healing, rolling updates, and automatic horizontal scaling.
  • Dockerfile – a multi-stage build that compiles and packages in a JDK stage, then copies only the jar into a slim JRE image, creating one immutable, self-contained artifact.
  • docker-compose – declares several services (app, database, broker) with their images, ports, and volumes, starting the whole local stack with one command on a private network.
  • Kubernetes Deployment – declares desired state (image, replica count) and continuously works to match reality, self-healing by replacing crashed Pods.
  • Kubernetes Service – gives a set of Pods one stable virtual IP and DNS name, load-balancing across healthy Pods.
  • Probes – liveness (should the Pod be restarted?) and readiness (should it receive traffic?) drive zero-downtime rolling updates.
  • HorizontalPodAutoscaler – watches a metric (CPU, custom) and adjusts replica count between min and max.

Think of each service at National Bank of India as a specialised counter. A Dockerfile is the instruction sheet for building one fully-equipped portable counter. docker-compose sets up a few counters together on your own desk for testing. Kubernetes is the branch manager of a huge building: you tell it you want three transfer counters running, and it finds space, replaces any that break, routes customers to healthy ones, and when the queue grows it opens more counters automatically.

Understanding the Concepts

What is a Dockerfile and why use multi-stage builds?

A Dockerfile packages a service into one immutable image. A multi-stage build keeps the image small and secure: the first stage uses a full JDK to compile, and the second copies only the jar into a slim JRE base.

DOCKERFILE
# Stage 1: build
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn -q clean package -DskipTests

# Stage 2: runtime
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/transfer-service.jar app.jar
USER 1001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
BASH
docker build -t transfer-service:1.4.0 .
docker run -p 8080:8080 transfer-service:1.4.0

Why must the image be immutable?

The same image that passed tests is the exact one that runs in production, identified by a version tag or digest. You never edit a running container. Configuration comes from environment variables, not baked-in secrets.

How does docker-compose help local development?

A docker-compose.yml declares the service together with its database and broker, starting the whole stack with one command.

YAML
version: '3.9'
services:
  transfer-service:
    build: .
    ports: ['8080:8080']
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/bankdb
    depends_on: [postgres, kafka]
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: bankdb
      POSTGRES_PASSWORD: secret
  kafka:
    image: confluentinc/cp-kafka:7.6.0
BASH
docker compose up

Compose is excellent for development and integration tests but is not a production orchestrator.

How does Kubernetes run services in production?

You declare desired state in YAML and Kubernetes works to match it. A Deployment keeps replica Pods alive. A Service provides a stable DNS name.

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: transfer-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: transfer-service
  template:
    spec:
      containers:
        - name: transfer-service
          image: transfer-service:1.4.0
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: '500m', memory: '512Mi' }
            limits:   { cpu: '1',    memory: '1Gi' }
          livenessProbe:
            httpGet: { path: /actuator/health/liveness, port: 8080 }
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: 8080 }

How do liveness and readiness probes work?

A liveness probe asks "is this process healthy, or should it be killed and restarted?" A readiness probe asks "is this instance ready to receive traffic?" During a rolling update, Kubernetes only sends traffic to Pods that pass readiness.

JAVA
package com.bank.config;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;

@Component
public class DatabaseReadinessIndicator implements HealthIndicator {

    private final DataSource dataSource;

    public DatabaseReadinessIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Health health() {
        try (Connection conn = dataSource.getConnection()) {
            if (conn.isValid(1)) return Health.up().build();
        } catch (Exception e) {
            // Pool exhausted or DB unreachable
        }
        return Health.down().build();
    }
}

How does horizontal autoscaling work?

The HorizontalPodAutoscaler watches a metric and adjusts the replica count.

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

What are common mistakes?

YAML
# WRONG: no resource requests/limits
containers:
  - name: transfer-service
    image: transfer-service:latest  # also: never use 'latest' tag

# CORRECT: pinned tag, explicit resources
containers:
  - name: transfer-service
    image: transfer-service:1.4.0
    resources:
      requests: { cpu: '500m', memory: '512Mi' }
      limits:   { cpu: '1',    memory: '1Gi' }

What should a senior engineer know?

  • Keep services stateless so any Pod can serve any request and scaling is trivial.
  • Implement graceful shutdown: finish in-flight requests on SIGTERM within the termination grace period.
  • Pin base images by digest and scan for vulnerabilities.
  • Prefer a secrets manager over raw Kubernetes Secrets for banking credentials.
  • Expose health, metrics, and tracing so the platform and dashboards can observe the service.

Visual Overview

DIAGRAM
  Dockerfile Multi-Stage Build
  ┌─────────────┐     ┌──────────────┐
  │ JDK Stage   │     │ JRE Stage    │
  │ mvn package │────►│ COPY jar     │───► Immutable Image
  │ (discarded) │     │ USER 1001    │     transfer-service:1.4.0
  └─────────────┘     └──────────────┘

  Kubernetes Deployment
  ┌──────────────────────────────────────┐
  │  Deployment: transfer-service        │
  │  replicas: 3                         │
  │  ┌──────┐  ┌──────┐  ┌──────┐       │
  │  │ Pod  │  │ Pod  │  │ Pod  │       │
  │  │:8080 │  │:8080 │  │:8080 │       │
  │  └──┬───┘  └──┬───┘  └──┬───┘       │
  │     └────┬─────┘────┬────┘           │
  │          ▼          ▼                │
  │   Service (stable DNS + LB)          │
  └──────────────────────────────────────┘

  HPA Scaling
  CPU > 70%  ───► HPA adds Pods ───► up to 20
  CPU < 70%  ───► HPA removes Pods ──► down to 3

Key Takeaway

Deploying a microservice to containers is a progression from one image to a production orchestrator. The Dockerfile packages the service into one immutable image using a multi-stage build so the runtime carries only a slim JRE, runs as a non-root user, and passes configuration through environment variables. docker-compose starts the service with its database and broker on a private network for local development, ideal for integration tests but not a production orchestrator.

In production, Kubernetes keeps the desired number of Pods alive, self-heals by replacing crashed Pods, and exposes them behind a stable Service name with load balancing. Liveness and readiness probes, wired to Spring Boot Actuator, let Kubernetes restart unhealthy Pods and only send traffic to ready ones, making rolling updates zero-downtime. The HorizontalPodAutoscaler watches CPU or custom metrics and scales from 3 to 20 Pods automatically.

At National Bank of India, Pankaj containerised the transfer service after a production release failed due to a Java version mismatch. With Kubernetes, the salary-day rush auto-scaled from 3 to 15 Pods when CPU crossed 70%, and scaled back down afterwards. The key disciplines are keeping services stateless, implementing graceful shutdown, pinning image tags, setting resource requests and limits, and externalising all secrets.

Remember one sentence: the Dockerfile builds one immutable image, docker-compose runs the local stack, and Kubernetes runs production with self-healing Deployments, stable Services, probe-driven zero-downtime rollouts, and automatic horizontal scaling.

---

Share
Use ← → arrow keys to navigate