Moving Spring Boot apps to K8s, there are tons of tutorials online, but most stay at the “get it running” level. In actual production, there are many problems that tutorials won’t tell you. Here are 5 real pitfalls I encountered, each with specific solutions.

Pitfall 1: Improper Health Check Config Causes Frequent Restarts

Many people use default config for K8s livenessProbe and readinessProbe, but if the app starts slightly slower, K8s judges it as unhealthy and restarts it repeatedly.

Root cause: Spring Boot app needs to load Spring container, connect to database, warm up cache during startup, time may exceed default probe timeout.

Correct config:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 60   # Give enough startup time
  periodSeconds: 15
  failureThreshold: 3
  timeoutSeconds: 5

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3
  timeoutSeconds: 5

Also enable Actuator’s grouped health checks in application.yml:

management:
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true
  endpoint:
    health:
      probes:
        enabled: true

liveness only checks if the app is alive (JVM is normal), readiness checks if ready to receive traffic (database connection is normal, etc.). Configure them separately to avoid false kills.


Pitfall 2: ConfigMap Updated but App Didn’t Reload

Put config in ConfigMap, after updating found the app had no changes at all. Reason: K8s doesn’t automatically restart Pods when ConfigMap updates, mounted as files get delayed sync (usually 1-2 minutes), but environment variable injection never updates.

Three solutions:

SolutionReal-timeComplexity
Manual rollout restart after ConfigMap updateManual triggerLow
Spring Cloud Config ServerReal-time pushHigh
Reloader (third-party controller)Auto-detectMedium

For most scenarios, the simplest solution is enough:

kubectl rollout restart deployment/your-app -n your-namespace

Add this to CI/CD pipeline, auto-trigger restart after ConfigMap updates.


Pitfall 3: JVM Memory Settings Don’t Match Container Memory

Without setting JVM heap memory limits, JVM tries to use all container memory, then gets killed by OOM Killer.

Correct approach: Explicitly set JVM parameters in container environment variables:

env:
  - name: JAVA_OPTS
    value: "-Xms512m -Xmx1g -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "1.5Gi"
    cpu: "1000m"

Key parameter explanations:

  • -XX:+UseContainerSupport: Let JVM detect container memory limits (enabled by default in JDK 8u191+)
  • -XX:MaxRAMPercentage=75.0: Heap uses up to 75% of container memory, leaving space for Metaspace, thread stacks, off-heap memory
  • Set memory limit to 1.5x Xmx, leaving room for non-heap memory

Pitfall 4: App Receives SIGTERM and Immediately Interrupts Requests

When K8s scales down or does rolling update, it sends SIGTERM signal to Pod, by default Spring Boot app exits immediately after receiving the signal, requests in progress get interrupted.

Enable graceful shutdown:

# application.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Also configure terminationGracePeriodSeconds in K8s Deployment, should be slightly larger than Spring’s timeout:

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 40  # Slightly larger than Spring's 30s
      containers:
        - name: your-app
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 5"]  # Wait for K8s to update Endpoints

The preStop sleep 5 is key: K8s needs time to update Endpoints (remove this Pod from Service), if shutdown starts immediately, new requests might still get routed here.


Pitfall 5: Logs Disappear in Container

After container restart, previous logs are gone. If using logback to write logs to files inside container, restart equals log wipe.

Correct solution: Output to stdout, let K8s log system collect

Modify logback-spring.xml, remove file appender, keep only Console:

<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <!-- JSON format, convenient for log system parsing -->
      <pattern>{"time":"%date{ISO8601}","level":"%level","logger":"%logger{36}","msg":"%message"}%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
  </root>
</configuration>

After outputting to stdout, use kubectl logs to view in real-time, EFK/Loki and other log systems can also collect directly.


Summary

ProblemKey Solution
Frequent restartsConfigure sufficient initialDelaySeconds, separate liveness/readiness
ConfigMap not reloadingTrigger rollout restart after update
OOM killedExplicitly set -Xmx, use MaxRAMPercentage
Request interruptionEnable server.shutdown: graceful, add preStop sleep
Logs disappearingOutput to stdout only, don’t write to files

I encountered all 5 of these in production, after fixing them system stability improved significantly. K8s itself isn’t complicated, most pitfalls come from not understanding how K8s works.