Advanced · Ship
Package & Run an Immutable Spring Boot Service
Build a small non-root layered image and configure JVM/container behavior without rebuilding it per environment.
Primary source: Spring Boot 3.5 Reference: Container Images & Probes
Lesson 1 of 4
1d streakThe Architectural Question
Fat images rebuilt per environment drift from what CI actually tested, and a JVM ignoring container memory limits gets killed exactly when traffic peaks.
What this guide proves
- Use a supported runtime image or buildpack
- Separate image content from runtime configuration
- Set CPU and memory requests from measured behavior
Cloud-Native Packaging: Buildpacks vs Multi-Stage Dockerfiles
Packaging a Spring Boot application for production requires minimal image size, reproducible builds, and strict layer caching. Spring Boot provides two standard production packaging approaches:
1. Cloud-Native Buildpacks: Executed via ./mvnw spring-boot:build-image. Automatically uses Paketo Buildpacks to build an OCI-compliant image with no Dockerfile required, applying automated OS CVE patching and layer caching.
2. Multi-Stage Dockerfile: Provides total control over base operating system, Java runtime flags, and multi-layer jar extraction (Spring Boot layered jar format).
# Stage 1: Dependency Extraction
FROM bellsoft/liberica-openjdk-alpine:21 AS builder
WORKDIR /workspace
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted
# Stage 2: Hardened Runtime Container
FROM bellsoft/liberica-openjre-alpine:21
WORKDIR /app
# Run as non-privileged non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser:appgroup
# Copy layers in order of change frequency (rarely changed to frequently changed)
COPY --from=builder /workspace/extracted/dependencies/ ./
COPY --from=builder /workspace/extracted/spring-boot-loader/ ./
COPY --from=builder /workspace/extracted/snapshot-dependencies/ ./
COPY --from=builder /workspace/extracted/application/ ./
# High-performance JVM flags for container constraints
ENTRYPOINT ["java", \
"-XX:+UseZGC", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:+ExitOnOutOfMemoryError", \
"org.springframework.boot.loader.launch.JarLauncher"]Spring Boot 3.5 Layered Extraction
java -Djarmode=tools -jar application.jar extract --layers replaces legacy layertools, cleanly separating third-party dependencies from your business application code. Docker layer caching ensures subsequent CI builds upload only changed application code.Class Data Sharing (CDS) and Spring AOT Startup Optimization
CDS and Spring AOT can reduce startup work in some deployments, but the benefit depends on the application, JVM, image, and measurement method. Benchmark the same artifact and workload before presenting a percentage improvement.
CDS creates an archive that the JVM can map during startup; the startup and memory benefit depends on the classes archived and the runtime environment.
# 1. Generate CDS archive during container build
java -XX:ArchiveClassesAtExit=application.jsa -Dspring.context.exit=onRefresh -jar application.jar
# 2. Launch container with memory-mapped CDS archive
java -XX:SharedArchiveFile=application.jsa -jar application.jarContainer Hardening: Non-Root Execution and Read-Only Filesystems
Run containerized Java applications as non-root by default. Root inside a container increases process privilege and potential blast radius; it does not automatically grant root access to the host, which remains protected by container isolation and platform configuration.
Root Execution Risk in Production
Container Inspection and Security Proving Checkpoints
Inspect image layers and verify non-root execution; choose image-size targets from your runtime and vulnerability budget:
# 1. Verify user execution ID inside container
docker run --rm my-app:latest id
# EXPECTED: uid=10001(appuser) gid=10001(appgroup)
# 2. Inspect layer sizes and review the result against the chosen image budget
docker history my-app:latest
# 3. Test running with a read-only root filesystem
docker run --rm --read-only --tmpfs /tmp my-app:latestActive Recall Checkpoint
Why should dependencies and application layers be copied in separate Dockerfile instructions?
Hint: Consider the Docker build cache hierarchy.
Show answer & explanation
Third-party dependencies usually change less often than application code. Separate layers so the build can reuse unchanged layers; measure the cache benefit instead of assuming a fixed number of megabytes.
Runtime Architecture & Flow
High-value visual model
Container to Kubernetes Rollout
A production deployment is a sequence of immutable artifact creation, environment binding, rollout, health gating, and rollback readiness.
1. Build layered image
Compile, test, and package an executable artifact into a repeatable container image.
2. Bind runtime config
Profiles, config maps, secrets, and resource limits are applied by the platform.
3. Roll out new replicas
New pods start beside old pods while readiness gates traffic.
4. Observe and rollback if needed
Metrics, logs, traces, and health checks decide whether rollout continues.
Synthesis & Production Verification
Production Acceptance Criteria
- Build one non-root layered image per commit and reuse it everywhere
- Separate image content from runtime configuration
- Set CPU and memory requests from measured startup and steady-state usage
Key Takeaway
Cloud-native packaging leverages multi-stage layered jar extraction, non-root user execution, and memory-aware JVM container flags for minimal attack surface.
Architectural & Technical Review
Core Interview Questions
Q01What is the advantage of Spring Boot layered jar extraction over fat jar packaging in Dockerfiles?Show answer
Layered jar extraction separates static Maven dependencies from frequently modified application code. Docker caches the heavy dependency layers, speeding up CI build and deployment upload times.
Q02How do Class Data Sharing (CDS) and Spring AOT reduce JVM startup time in Kubernetes pods?Show answer
CDS dumps pre-processed class metadata into a memory-mapped archive (.jsa) that the JVM maps instantly on start, bypassing class loading and bytecode verification overhead to reduce startup time by up to 50%.
Q03Why must containerized Java applications execute under a dedicated non-root UID/GID?Show answer
Running as non-root (e.g. UID 10001) adheres to the principle of least privilege, preventing an attacker who achieves remote code execution inside the container from gaining root privileges on the Kubernetes host kernel.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Running containerized Spring Boot processes as root (UID 0) — Allows container breakout exploits to compromise the underlying Kubernetes host node kernel.
- Pitfall: Omitting -XX:MaxRAMPercentage in container entrypoints — Causes JVM to miscalculate available memory on older runtimes, leading to silent OOMKilled pod evictions.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources