Overview
What changes when your Spring Boot app moves from your laptop to a production cluster: fat JAR, layered JAR for Docker caching, graceful shutdown, Kubernetes probes wired to Actuator, and JVM memory in containers.
Official: Container Images, Graceful Shutdown.
Table of contents
- Fat JAR
- Layered JAR for Docker
- Graceful shutdown
- Kubernetes probes
- JVM memory in containers
- Interview questions
- Summary
1. Fat JAR
mvn package produces one JAR that contains your app plus all dependencies. Run it with:
java -jar app.jar
The embedded Tomcat starts inside. No external servlet container needed.
2. Layered JAR for Docker
Boot can split the fat JAR into layers so Docker caches the slow parts:
| Layer | Changes |
|---|---|
dependencies |
Rarely — only when libraries change |
spring-boot-loader |
Almost never |
snapshot-dependencies |
Occasionally |
application |
Every code change |
Dockerfile copies dependencies first, app code last → rebuilds are fast and the resulting images push less data:
FROM eclipse-temurin:21-jre AS builder
WORKDIR /app
COPY app.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
3. Graceful shutdown
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
Behaviour on SIGTERM:
- Stop accepting new requests.
- Wait up to 30s for in-flight requests to finish.
- Exit.
Critical for Kubernetes rolling updates — without it, in-flight requests are killed when the pod terminates.
4. Kubernetes probes
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
| Probe | Question | Failure action |
|---|---|---|
| Liveness | Is the process alive? | Restart the container |
| Readiness | Should I receive traffic? | Remove from Service endpoints (no restart) |
Rule: keep liveness cheap. Put DB / message broker checks in readiness so a failing dependency drains traffic instead of crash-looping the container.
5. JVM memory in containers
The JVM doesn’t read your container CPU/memory limit unless you tell it:
-XX:MaxRAMPercentage=75.0
That gives the JVM 75 % of the container’s memory limit for the heap. Without it, the JVM may use the host’s RAM total and get OOM-killed.
Also useful in production:
- GC logging (
-Xlog:gc*:file=/var/log/gc.log) or JFR for triage. - Heap dumps on OOM (
-XX:+HeapDumpOnOutOfMemoryError).
6. Interview questions
Q: Why use a layered Docker build?
A: Dependencies change less often than app code. Putting them in a separate layer means rebuilding only changes the small application layer, making builds and image pushes much faster.
Q: What is graceful shutdown? A: On SIGTERM, stop accepting new requests, let in-flight ones finish within a timeout, then exit. Essential for K8s rolling updates.
Q: Should the liveness probe check the database? A: No. Liveness should be cheap. Put DB checks in readiness so a DB outage drains traffic from the pod without restarting it.
Q: How do you size the JVM heap in a container?
A: -XX:MaxRAMPercentage=75 (or similar) so the JVM uses a percentage of the container’s memory limit instead of the host’s.
7. Summary
| Topic | Rule |
|---|---|
| Image | Layered JAR for cache wins |
| Shutdown | server.shutdown=graceful + timeout |
| Probes | Cheap liveness, deep readiness |
| Heap | MaxRAMPercentage so JVM respects container limits |
Next steps
- Actuator health
- API idempotency — retry-safe rolling deploys
Happy Learning