JAVA Contents

Dockerizing, distroless concepts

Containerize Java services correctly using multi-stage builds, minimal base images, non-root users and JVM tuning for container environments.

On this page

Why Naive Dockerfiles Fail in Production

A Docker image that works locally is not automatically production-ready. Production symptoms of poor containerization: - Image size > 1GB - Slow startup times - High memory usage - Running as root - CVE scan full of critical vulnerabilities - OOMKilled in Kubernetes Containers are runtime environments. They must be designed, not improvised.

Incident Scenario: Service OOMKilled After Migration to Kubernetes

A service ran fine on a VM. After moving to Kubernetes with 512Mi memory limit, pods started crashing. Root cause: JVM ignored container memory limits. Default heap sizing assumed full node memory. GC pressure triggered OOMKill. The Dockerfile was correct syntactically. It was wrong operationally.

Anti-Pattern: Single-Stage Fat Dockerfile

FROM openjdk:latest
COPY build/libs/app.jar app.jar
CMD ["java", "-jar", "app.jar"]
Problems: - latest tag is non-deterministic - full JDK included unnecessarily - large attack surface - no user isolation - no JVM container tuning This is demo-level, not production-grade.

Correct Pattern: Multi-Stage Build

Separate build from runtime.
FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
COPY . .
RUN gradle build --no-daemon

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/app.jar app.jar

RUN addgroup --system app && adduser --system --ingroup app app
USER app

ENTRYPOINT ["java","-XX:+UseContainerSupport","-jar","app.jar"]
Benefits: - No build tools in final image - Smaller runtime footprint - Reduced CVE surface - Non-root execution

Distroless Concept

Distroless images remove: - shell - package manager - unnecessary binaries Advantages: - Smaller attack surface - Fewer vulnerabilities - Reduced image size Tradeoff: - Harder debugging (no shell inside container) Distroless is excellent for stable production workloads. Use debug-friendly base image for development, minimal for prod.

Run As Non-Root

Running as root inside container is still risky: - breakout impact increases - file permission escalation risk Always: - create non-root user - switch USER in Dockerfile - ensure filesystem permissions allow execution This is a minimal but important defense.

Layering and Caching

Optimize build cache: - copy build files first - resolve dependencies - then copy source This reduces CI build time significantly. Also: - leverage Spring Boot layered jar if applicable - separate dependencies layer from application layer Faster builds = faster feedback = safer deploys.

JVM Container Awareness

Modern JVM supports container memory awareness, but do not assume defaults are perfect. Tune: - -XX:MaxRAMPercentage - -XX:InitialRAMPercentage - GC selection appropriate for memory limit Example:
ENTRYPOINT ["java",
 "-XX:MaxRAMPercentage=75.0",
 "-XX:InitialRAMPercentage=50.0",
 "-XX:+UseContainerSupport",
 "-jar","app.jar"]
Without tuning: - heap may be too large - GC pauses may increase - container limit may be exceeded

Resource Limits and Requests

Containers must define: - CPU requests - CPU limits - Memory requests - Memory limits JVM behavior changes under CPU throttling. Understand how your GC behaves when CPU is limited.

Security Scanning

Regularly scan images: - base image vulnerabilities - dependency CVEs - outdated runtime components Pin base images by digest or fixed version. Never use floating tags like latest.

Checklist

- Use multi-stage builds - Avoid floating base image tags - Use minimal runtime images (JRE or distroless) - Run as non-root user - Tune JVM for container memory limits - Separate dependency and app layers for caching - Scan images for vulnerabilities - Keep runtime image small and focused - Test container under realistic memory and CPU limits Containerization is not packaging. It is defining the production runtime contract.