Dockerizing Java Applications: From JAR to Production Image
Containerizing a Java app looks trivial — FROM openjdk and copy a JAR. In production, that image is too big, starts too slowly, and carries a full JDK plus the kitchen sink. Here is the flow I use.
Start from a runtime image, not a JDK
Compile with the JDK, run with a JRE. Alpine-based JRE images are small but historically fragile with native libs; eclipse-temurin gives you a solid, distroless-friendly choice. The common pattern:
# --- Build stage: full JDK + Maven cache ---
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -B dependency:go-offline
COPY src ./src
RUN mvn -B -DskipTests package
# --- Runtime stage: minimal JRE + the JAR only ---
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/app.jar app.jar
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
dependency:go-offline caches dependencies in a separate layer so source changes do not re-download the internet.
Layer the JAR for fast cold starts
Spring Boot fat JARs unpack beautifully. Use the built-in layers so only the layer you change is rebuilt:
COPY --from=build /app/target/app.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
COPY --from=build /app/dependencies/ ./
COPY --from=build /app/spring-boot-loader/ ./
COPY --from=build /app/snapshot-dependencies/ ./
COPY --from=build /app/application/ ./
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "org.springframework.boot.loader.launch.JarLauncher"]
Reduce attack surface
- Run as a non-root user:
USER 10001:10001. - Set
LABEL org.opencontainers.image.source=...andHEALTHCHECK— Kubernetes does its own probes, but localdocker runbenefits. - Pin image tags to digest or at least a versioned tag.
latestis a moving target. - Scan images in CI with
docker scoutor Trivy and fail the build on critical CVEs.
Keepalive, memory, and JVM flags
Inside a container, the JVM cannot read cgroup limits reliably without help:
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:+UseContainerSupport"
MaxRAMPercentage ties the heap to the pod's memory limit instead of the host's. This one flag prevents the most common Java-in-K8s outage: an OOM-killed pod on a healthy host.
Validate locally, then hand it to the orchestrator
docker compose up --build for local parity, then a k8s probe spec with both readiness and liveness on /actuator/health/*. The image is the smallest reproducible artifact your platform receives — make it boring, minimal, and repeatable.