Back to Blog
Docker Spring BootKubernetes Javacontainerization JavaDocker ComposeKubernetes deploymentSpring Boot Docker

Containerizing Spring Boot in Production: Docker & Kubernetes Complete Guide

J

JOptimize Team

May 5, 2026· 5 min read

Containerizing Spring Boot in Production: Docker & Kubernetes Complete Guide

Your Spring Boot application works perfectly on your laptop.

Then you push to production and everything breaks.

Different OS. Different Java version. Different environment variables. Different dependencies.

This is the problem Docker solves.

In this guide, I'll show you how to:

  • ✅ Create production-grade Docker images
  • ✅ Optimize image size and startup time
  • ✅ Deploy to Kubernetes safely
  • ✅ Scale automatically
  • ✅ Monitor and debug in containers
  • ✅ Handle networking and storage

Why Docker? Why Now?

Before DockerWith Docker
"Works on my machine"Works everywhere
Manual environment setupReproducible builds
1-2 hours to deploy5 minutes to deploy
Hard to scaleAuto-scaling trivial
Ops nightmareInfrastructure as code

Bottom line: If you're not containerizing, you're wasting time.


Part 1: Docker Basics for Spring Boot

Step 1: Create a Dockerfile

# Multi-stage build (production best practice) # Stage 1: Build FROM eclipse-temurin:21-jdk as builder WORKDIR /app # Copy only pom.xml first (caching layer) COPY pom.xml . # Download dependencies (cached if pom.xml doesn't change) RUN mvn dependency:go-offline -B # Copy source code COPY src ./src # Build the application RUN mvn clean package -DskipTests # Stage 2: Runtime (small final image) FROM eclipse-temurin:21-jre WORKDIR /app # Copy only the JAR from build stage COPY --from=builder /app/target/*.jar app.jar # Add non-root user (security) RUN useradd -m -u 1000 appuser USER appuser # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 # Expose port EXPOSE 8080 # Run the application ENTRYPOINT ["java", "-jar", "app.jar"]

Why multi-stage?

  • Build stage: 800MB (includes Maven, dependencies, source)
  • Final image: 200MB (only runtime)
  • 4x smaller = faster deploys, less memory

Step 2: Optimize the Dockerfile

# Even better: Use Spring Boot optimized layers FROM eclipse-temurin:21-jdk as builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # Extract JAR layers RUN java -Djarmode=layertools -jar target/*.jar extract # Final stage FROM eclipse-temurin:21-jre WORKDIR /app # Copy layers in order (most frequently changed last) COPY --from=builder /app/dependencies/ ./ COPY --from=builder /app/spring-boot-loader/ ./ COPY --from=builder /app/snapshot-dependencies/ ./ COPY --from=builder /app/application/ ./ RUN useradd -m -u 1000 appuser USER appuser EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s \ CMD curl -f http://localhost:8080/actuator/health || exit 1 ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

This enables layer caching:

  • If only your code changes → only top layers rebuild
  • Dependencies layer cached → much faster builds

Step 3: Build the Docker Image

# Build docker build -t myapp:1.0.0 . # Check size docker images myapp:1.0.0 # myapp 1.0.0 abcd1234 165MB # Run locally docker run -p 8080:8080 myapp:1.0.0

Step 4: Docker Compose (Local Development)

version: '3.8' services: app: build: . ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mydb SPRING_DATASOURCE_USERNAME: postgres SPRING_DATASOURCE_PASSWORD: password depends_on: postgres: condition: service_healthy networks: - app-network postgres: image: postgres:15-alpine ports: - "5432:5432" environment: POSTGRES_DB: mydb POSTGRES_PASSWORD: password healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 networks: - app-network redis: image: redis:7-alpine ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 3 networks: - app-network networks: app-network: driver: bridge

Run everything with one command:

docker-compose up # App is at http://localhost:8080 # Postgres at localhost:5432 # Redis at localhost:6379

Part 2: Kubernetes Deployment

Step 1: Create Kubernetes Manifests

Deployment (deployment.yaml):

apiVersion: apps/v1 kind: Deployment metadata: name: myapp namespace: production spec: replicas: 3 # Run 3 instances strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 # Zero-downtime deployments selector: matchLabels: app: myapp template: metadata: labels: app: myapp annotations: prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/actuator/prometheus" spec: serviceAccountName: myapp containers: - name: myapp image: myregistry.azurecr.io/myapp:1.0.0 imagePullPolicy: Always ports: - containerPort: 8080 name: http env: - name: SPRING_PROFILES_ACTIVE value: "production" - name: SPRING_DATASOURCE_URL valueFrom: secretKeyRef: name: db-secret key: url - name: SPRING_DATASOURCE_PASSWORD valueFrom: secretKeyRef: name: db-secret key: password # Resource limits (critical!) resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1000m memory: 1Gi # Health checks livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 20 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 # Graceful shutdown lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 15"] # Pod disruption budget (for safe drains) affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - myapp topologyKey: kubernetes.io/hostname

Service (service.yaml):

apiVersion: v1 kind: Service metadata: name: myapp namespace: production spec: type: LoadBalancer # Or ClusterIP for internal only selector: app: myapp ports: - port: 80 targetPort: 8080 protocol: TCP name: http sessionAffinity: None

ConfigMap (config.yaml):

apiVersion: v1 kind: ConfigMap metadata: name: myapp-config namespace: production data: application.yml: | spring: jpa: hibernate: ddl-auto: validate show-sql: false cache: type: redis redis: time-to-live: 3600000 logging: level: root: WARN com.myapp: INFO

Secret (secret.yaml):

apiVersion: v1 kind: Secret metadata: name: db-secret namespace: production type: Opaque data: url: amRiYzpwb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6NTQzMi9teWRi # base64 encoded password: c3VwZXJzZWNyZXRwYXNzd29yZA== # base64 encoded

HPA (Auto-scaling - hpa.yaml):

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: myapp-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: myapp minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 50 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 60 policies: - type: Percent value: 100 periodSeconds: 30

Step 2: Deploy to Kubernetes

# Create namespace kubectl create namespace production # Apply manifests kubectl apply -f deployment.yaml kubectl apply -f service.yaml kubectl apply -f config.yaml kubectl apply -f secret.yaml kubectl apply -f hpa.yaml # Check deployment kubectl get deployment -n production kubectl get pods -n production kubectl get svc -n production # Watch rollout kubectl rollout status deployment/myapp -n production # Get logs kubectl logs -f deployment/myapp -n production

Step 3: Zero-Downtime Deployments

# Update image kubectl set image deployment/myapp myapp=myregistry.azurecr.io/myapp:2.0.0 -n production # Watch the rollout (old → new pods gradually) kubectl rollout status deployment/myapp -n production # If something goes wrong, rollback kubectl rollout undo deployment/myapp -n production

Part 3: Production Best Practices

1. Resource Management (CRITICAL)

resources: requests: cpu: 500m # Container gets at least 0.5 CPU memory: 512Mi # Container gets at least 512MB limits: cpu: 1000m # Container can use max 1 CPU memory: 1Gi # Container can use max 1GB

Without limits:

  • Container runs away with CPU → starves other apps
  • Container leaks memory → node crashes With limits:
  • Container throttled at limit
  • Out-of-memory → pod killed and restarted

2. Health Checks (CRITICAL)

// Spring Boot Actuator provides health checks @Configuration public class HealthConfig { @Bean public HealthIndicator customHealth() { return new HealthIndicator() { @Override public Health health() { // Check database if (databaseConnected()) { return Health.up().build(); } return Health.down().build(); } }; } }

Kubernetes uses:

  • Liveness: Is the app alive? (restart if not)
  • Readiness: Is it ready to receive traffic? (remove from load balancer if not)

3. Graceful Shutdown

lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 15"] # Give in-flight requests 15s
server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s

4. Monitoring & Observability

annotations: prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/actuator/prometheus"

Access metrics:

kubectl port-forward service/myapp 8080:80 -n production curl http://localhost:8080/actuator/prometheus

5. Network Policies (Security)

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: myapp-network-policy namespace: production spec: podSelector: matchLabels: app: myapp policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: name: ingress-nginx ports: - protocol: TCP port: 8080 egress: - to: - namespaceSelector: {} ports: - protocol: TCP port: 5432 # Database - protocol: TCP port: 443 # HTTPS

Part 4: Common Problems & Solutions

Problem 1: Image Too Large

Before: 500MB After: 150MB

Solution:

  • Use JRE instead of JDK
  • Use multi-stage builds
  • Remove unnecessary dependencies
  • Use Alpine base image

Problem 2: Slow Startup

Before: 30 seconds After: 10 seconds

Solution:

  • Enable Spring Boot startup logging
  • Use CDS (Class Data Sharing)
  • Lazy load beans
  • Profile startup with -XX:+FlightRecorder

Problem 3: High Memory Usage

# Container using 1GB but limited to 512MB? kubectl describe pod myapp-xyz -n production # Solution: Increase JVM heap docker run -e JAVA_OPTS="-Xmx1g" myapp:1.0.0

Problem 4: Pod Gets OOMKilled

# Check events kubectl describe pod myapp-xyz -n production # Solutions: # 1. Increase memory limits # 2. Fix memory leak # 3. Add more replicas and distribute load

Optimization Checklist

  • ☐ Multi-stage Docker build
  • ☐ Resource requests AND limits set
  • ☐ Liveness and readiness probes configured
  • ☐ Graceful shutdown enabled
  • ☐ Health checks passing
  • ☐ Non-root user running container
  • ☐ No hardcoded secrets (use ConfigMap/Secret)
  • ☐ Logging to stdout (not files)
  • ☐ Metrics exposed to Prometheus
  • ☐ Network policies defined
  • ☐ Pod disruption budget set
  • ☐ HPA configured

Detect Containerization Issues with JOptimize

Before containerizing, audit your code:

npm install -g @joptimize/cli joptimize auth YOUR_API_KEY joptimize analyze .

JOptimize detects:

  • ✓ Memory leaks (will cause OOMKilled in K8s)
  • ✓ Inefficient resource usage
  • ✓ Startup time bottlenecks
  • ✓ Performance anti-patterns
  • ✓ Configuration issues

Key Takeaways

  1. Multi-stage builds are essential. Reduces image size by 4-5x.
  2. Resource limits prevent disasters. Set them ALWAYS.
  3. Health checks are not optional. Liveness + readiness both needed.
  4. Graceful shutdown saves data. Don't force-kill requests.
  5. HPA scales automatically. Let Kubernetes do the work.
  6. Monitoring is critical. Expose metrics, set alerts.
  7. Secrets are not ConfigMaps. Use Secret for passwords.
  8. Zero-downtime deployments are now trivial. Rolling updates handle it.

Next Steps

  1. Create Dockerfile (multi-stage)
  2. Build and test locally with Docker Compose
  3. Create Kubernetes manifests (deployment, service, config)
  4. Deploy to dev cluster and verify
  5. Add HPA for auto-scaling
  6. Enable monitoring (Prometheus + Grafana)
  7. Deploy to production with rolling updates Audit your code before containerizing:
npm install -g @joptimize/cli joptimize auth YOUR_API_KEY joptimize analyze .

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.