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

50 min

Lesson 1 of 4

1d streak

The 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).

Dockerfile
# 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

In Spring Boot 3.5, 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.

enable-cds.sh
# 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.jar

Container 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

Prefer a dedicated non-root UID/GID and a read-only root filesystem where the application supports it. Mount only the writable paths the runtime actually needs, such as a temporary directory.

Container Inspection and Security Proving Checkpoints

Inspect image layers and verify non-root execution; choose image-size targets from your runtime and vulnerability budget:

verify-container.sh
# 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:latest

Active 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.

Deployment pipeline with runtime gates: The image should be immutable; environment-specific values belong in deployment configuration and secrets.

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

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

Supplementary Module Resources