Dockerizing Java Applications: From JAR to Production Image

2 min readIntermediate
DockerJavaDevOpsKubernetes

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=... and HEALTHCHECK — Kubernetes does its own probes, but local docker run benefits.
  • Pin image tags to digest or at least a versioned tag. latest is a moving target.
  • Scan images in CI with docker scout or 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.

← Back to technical articles