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:
| Solution | Real-time | Complexity |
|---|---|---|
| Manual rollout restart after ConfigMap update | Manual trigger | Low |
| Spring Cloud Config Server | Real-time push | High |
| Reloader (third-party controller) | Auto-detect | Medium |
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 limitto 1.5xXmx, 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
| Problem | Key Solution |
|---|---|
| Frequent restarts | Configure sufficient initialDelaySeconds, separate liveness/readiness |
| ConfigMap not reloading | Trigger rollout restart after update |
| OOM killed | Explicitly set -Xmx, use MaxRAMPercentage |
| Request interruption | Enable server.shutdown: graceful, add preStop sleep |
| Logs disappearing | Output 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.