Spring Boot 3 min read

Deploying Spring Boot Applications to Kubernetes: A Complete Guide

Master the process of deploying Spring Boot applications to Kubernetes, from creating optimized Docker images to implementing health checks and managing configurations. This practical guide covers everything you need for successful container deployment.

Deploying Spring Boot applications to Kubernetes can seem daunting at first, but with the right approach, it becomes a straightforward process. In this guide, we'll walk through the steps to containerize and deploy a Spring Boot application to Kubernetes.

Key Takeaways

  • Use multi-stage builds to create smaller Docker images
  • Leverage Spring Boot Actuator for health checks
  • Configure resource limits and readiness probes
  • Use ConfigMaps for environment-specific configurations
  • Implement rolling updates for zero-downtime deployments

Prerequisites

  • A Spring Boot application
  • Docker installed locally
  • Access to a Kubernetes cluster
  • Basic understanding of Docker and Kubernetes concepts

Step 1: Creating an Optimized Docker Image

First, let's create an optimized Docker image using a multi-stage build. This approach helps reduce the final image size by only including necessary components.

FROM amazoncorretto:23-alpine3.20 as corretto-jdk
RUN apk add --no-cache binutils

# Build small JRE image
RUN $JAVA_HOME/bin/jlink \
         --verbose \
         --add-modules ALL-MODULE-PATH \
         --strip-debug \
         --no-man-pages \
         --no-header-files \
         --compress=2 \
         --output /customjre

FROM alpine:latest
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"

COPY --from=corretto-jdk /customjre $JAVA_HOME

ARG JAR_FILE=target/*-SNAPSHOT.jar

# Add app user for security
ARG APPLICATION_USER=appuser
RUN adduser --no-create-home -u 1000 -D $APPLICATION_USER

# Configure working directory
RUN mkdir /app && \
    chown -R $APPLICATION_USER /app

USER 1000

COPY  --chown=1000:1000  ${JAR_FILE} /app/app.jar
WORKDIR /app

EXPOSE 8080
ENTRYPOINT [ "/jre/bin/java", "-jar", "-Duser.timezone=UTC", "/app/app.jar", 
             "-XX:+ExitOnOutOfMemoryError", 
             "-XX:+UseCGroupMemoryLimitForHeap", 
             "-XX:+UseSerialGC"]

This Dockerfile:

  • Uses multi-stage builds to create a minimal JRE
  • Runs the application as a non-root user
  • Includes important JVM flags for container environments
  • Sets up proper file permissions

Step 2: Configuring Kubernetes Resources

Service Definition

First, let's create a Kubernetes Service to expose our application:

apiVersion: v1
kind: Service
metadata:
  name: spring-app
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8080"
    prometheus.io/path: "/actuator/prometheus"
spec:
  selector:
    app: spring-app
  ports:
    - name: http-traffic
      port: 8080
      targetPort: 8080
      protocol: TCP

Deployment Configuration

Next, create a Deployment to manage your application pods:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-app
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: spring-app
    spec:
      containers:
        - name: spring-app
          image: your-registry/spring-app:latest
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "kubernetes"
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            periodSeconds: 5
            timeoutSeconds: 2
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            failureThreshold: 30
            periodSeconds: 10
          resources:
            requests:
              memory: "512Mi"
              cpu: "500m"
            limits:
              memory: "1Gi"
              cpu: "1000m"
          volumeMounts:
            - name: config-volume
              mountPath: /config
      volumes:
        - name: config-volume
          configMap:
            name: spring-app-config

ConfigMap for Application Properties

Store your application configuration in a ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: spring-app-config
data:
  application.yaml: |
    spring:
      datasource:
        url: jdbc:postgresql://db-service:5432/mydb
    management:
      endpoint:
        health:
          probes:
            enabled: true

Step 3: Implementing Health Checks

Add the following dependencies to your build.gradle.kts:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-actuator")
}

Configure health probes in your application:

@Configuration
class ActuatorConfig {
    @Bean
    fun customHealthIndicator(): HealthIndicator {
        return HealthIndicator {
            val health = Health.Builder()
            try {
                // Add your health check logic here
                health.up()
            } catch (e: Exception) {
                health.down().withException(e)
            }
            health.build()
        }
    }
}

Step 4: Managing Kubernetes Resources with Terraform

For production environments, it's recommended to use Infrastructure as Code (IaC) tools like Terraform to manage your Kubernetes resources. Here's how to define your resources:

# Create namespace
resource "kubernetes_namespace" "app_namespace" {
  metadata {
    name = "spring-app"
  }
}

# Create service
resource "kubernetes_service" "app_service" {
  metadata {
    namespace = kubernetes_namespace.app_namespace.metadata[0].name
    name      = "spring-app"
    annotations = {
      "prometheus.io/scrape" = "true"
      "prometheus.io/port"   = "8080"
      "prometheus.io/path"   = "/actuator/prometheus"
    }
  }

  spec {
    selector = {
      app = "spring-app"
    }

    port {
      name        = "http-traffic"
      port        = 8080
      target_port = 8080
      protocol    = "TCP"
    }
  }
}

# Create deployment
resource "kubernetes_deployment" "app_deployment" {
  metadata {
    namespace = kubernetes_namespace.app_namespace.metadata[0].name
    name      = "spring-app"
  }

  spec {
    replicas = 2

    selector {
      match_labels = {
        app = "spring-app"
      }
    }

    strategy {
      type = "RollingUpdate"
    }

    template {
      metadata {
        labels = {
          app = "spring-app"
        }
      }

      spec {
        container {
          image = "your-registry/spring-app:latest"
          name  = "spring-app"

          env {
            name  = "SPRING_PROFILES_ACTIVE"
            value = "kubernetes"
          }

          # Reference secrets
          env {
            name = "DB_PASSWORD"
            value_from {
              secret_key_ref {
                name = "app-secrets"
                key  = "db-password"
              }
            }
          }

          readiness_probe {
            http_get {
              path = "/actuator/health/readiness"
              port = 8080
            }
            period_seconds = 5
            timeout_seconds = 2
          }
        }
      }
    }
  }
}

# Create ConfigMap
resource "kubernetes_config_map" "app_config" {
  metadata {
    namespace = kubernetes_namespace.app_namespace.metadata[0].name
    name      = "spring-app-config"
  }

  data = {
    "application.properties" = file("${path.module}/config/application.properties")
  }
}

Step 5: Deployment Process

  1. Build your application:
./gradlew build
  1. Build and push Docker image:
docker build -t your-registry/spring-app:latest .
docker push your-registry/spring-app:latest
  1. Apply Kubernetes configurations:
kubectl apply -f service.yaml
kubectl apply -f deployment.yaml
kubectl apply -f configmap.yaml
  1. Verify deployment:
kubectl get pods
kubectl get services

Best Practices

  1. Resource Management
    • Always set resource requests and limits
    • Monitor resource usage and adjust accordingly
  2. Configuration
    • Use ConfigMaps for configuration
    • Store secrets in Kubernetes Secrets
    • Use environment-specific profiles
  3. Health Checks
    • Implement both readiness and liveness probes
    • Use Spring Boot Actuator for health endpoints
    • Configure appropriate timeout values
  4. Security
    • Run containers as non-root users
    • Use network policies to control traffic
    • Regularly update base images

Common Issues and Solutions

  1. Out of Memory Issues
    • Ensure proper JVM flags are set
    • Monitor heap usage with actuator
    • Set appropriate memory limits
  2. Slow Startup
    • Use startup probes with appropriate thresholds
    • Consider lazy initialization where appropriate
    • Monitor startup time with actuator metrics

Remember to adjust these configurations based on your specific requirements and environment constraints.