Interview Prep Zoneby CuriouserLabs

Question 2 of 5 · staff level

Containerize a Spring Boot app properly: images, layers, and JVM memory in a memory-limited container.

🎤 Say this first

Image: layered — Boot's layertools/buildpacks split dependencies (rarely change) from your classes (change every build) so pushes/pulls move kilobytes, not the fat jar; run a slim JRE base (or jlink), non-root, with CDS/AOT cache for startup. Memory: the JVM is container-aware but the default MaxRAMPercentage (25%) is wrong for a dedicated container — set ~70–75%, and remember limit = heap + Metaspace + stacks + code cache + direct buffers + native; sizing heap = limit is the OOMKilled generator. CPU: limits shape availableProcessors → GC/pool sizing; know your requests/limits policy.

The full picture

Layered Dockerfile + honest memory math
FROM eclipse-temurin:21-jre-alpine AS base
# --- build stage extracts layers ---
FROM base AS build
COPY app.jar .
RUN java -Djarmode=tools -jar app.jar extract --layers --destination out
# --- runtime: layers ordered by change frequency ---
FROM base
RUN adduser -D app
USER app
COPY --from=build out/dependencies/ ./
COPY --from=build out/spring-boot-loader/ ./
COPY --from=build out/snapshot-dependencies/ ./
COPY --from=build out/application/ ./            # only this layer changes per build
ENTRYPOINT ["java",   "-XX:MaxRAMPercentage=75", "-XX:+ExitOnOutOfMemoryError",   "-XX:MaxMetaspaceSize=192m",   "org.springframework.boot.loader.launch.JarLauncher"]

# 1GiB limit budget (why 75%, not 90%):
# heap ~750m | metaspace ~150m | stacks (200 thr) ~200m? -> tune -Xss or use VTs
# + code cache ~100m + direct buffers + malloc overhead  => headroom is survival
  • Buildpacks alternative (bootBuildImage): reproducible, maintained base, memory-calculator included — the 'no Dockerfile expertise required' paved road; custom Dockerfiles when you need control.
  • `-XX:+ExitOnOutOfMemoryError`: in containers, a half-dead JVM after OOM is worse than a fast restart — die loudly, let the orchestrator replace you.
  • OOMKilled (exit 137) vs OutOfMemoryError are different diagnoses: 137 = the container limit (native total exceeded — often direct buffers/threads, heap innocent); OOME = heap/metaspace. Metrics for both or you'll chase the wrong one.
  • Startup extras: CDS (spring.context.exit=onRefresh training run) or AOT cache cut cold-start ~40–60%; non-root user + read-only filesystem are table-stakes hardening.

🔄 Likely follow-up questions

  • How do CPU limits interact with GC threads and common pool sizing?
  • Buildpacks vs Dockerfile vs jib — team-level trade-offs?
  • When does jlink/native-image change this picture?