Docker with Java — Slim Images and JVM Tuning for Containers That Start in Seconds
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

Docker with Java — Slim Images and JVM Tuning for Containers That Start in Seconds

[2026-08-08] Author: Ing. Calogero Bono
> share
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

The Java container that takes 40 seconds to start and weighs 800 MB is not a technical issue. It's a cost you pay at every deploy, every scale-up, every CI pipeline. We see it often in projects that come to us: images based on full JDKs, poorly built layers, a JVM configured for a physical machine. The result? Wasted resources, slow response times, and a cloud provider smiling at you while billing.

We at Meteora Web have been working with Java in production for years. We're not talking theory: we've optimized containers for clients who needed to scale during peak times without burning their budget. This guide shows you exactly how it's done — from choosing the base image to JVM tuning, with copy-paste examples.

Why are standard Java Docker images too heavy?

The most common mistake? Starting from an openjdk:latest image or, worse, a full distribution with the entire operating system. Take a 300 MB JDK, add a 200 MB Linux distro, put your application on top, and you get a 600-800 MB image. For what? To run code that, in the end, uses only a fraction of those libraries.

The problem isn't just disk space. Every heavy layer slows down push and pull from the registry, increases container startup time, and in a Kubernetes cluster, multiplies network and storage costs. A smaller image means faster deploys, less bandwidth consumed, and a reduced attack surface.

The solution: minimal base images and JRE instead of JDK

The first rule is to use a JRE (Java Runtime Environment) instead of the full JDK in production. The javac compiler is not needed at runtime. The second rule is to choose a minimalist base image like eclipse-temurin:17-jre-alpine or eclipse-temurin:21-jre-jammy. Alpine Linux is ultra-light, but beware: it uses musl libc, which can cause issues with some native libraries. In that case, the jammy variant (Ubuntu LTS) is a safer choice, still much slimmer than a full desktop.

Sponsored Protocol

Here's an example of an optimized Dockerfile for a Spring Boot application:

# Build stage: use full JDK only to compile
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

# Runtime stage: only JRE, minimal image
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Copy only the executable jar, not the whole project
COPY --from=builder /app/target/*.jar app.jar
# Non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

This multi-stage build approach takes you from 700 MB down to about 200 MB. The difference is felt: the container starts in seconds, not minutes.

How to optimize Docker layers for a Java application?

Docker builds images in layers. Every RUN, COPY, or ADD instruction creates a layer. If a layer changes, Docker rebuilds only that one and all subsequent ones. Leveraging this mechanism is the key to fast builds.

With Spring Boot, the final jar is an executable that contains all dependencies. But if you copy it as a single block, any code change invalidates the entire layer. The solution is to leverage Spring Boot's automatic layering, which separates dependencies from application code.

Sponsored Protocol

Use Spring Boot layering for faster builds

Spring Boot 2.3+ generates a jar with predefined layers: dependencies, spring-boot-loader, snapshot-dependencies, and application. You can extract them in the Dockerfile to copy them separately:

FROM eclipse-temurin:17-jre-alpine AS builder
WORKDIR /app
COPY target/*.jar app.jar
# Extract jar layers
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Copy layers in order: dependencies rarely change
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

Now, when you only modify your code, Docker rebuilds only the last layer. Image push and pull become much faster, and production deploys benefit too. If you use a remote registry, the bandwidth savings are substantial.

Which JVM parameters are needed for containers?

The JVM was designed for physical machines with lots of memory. In a container, if you don't configure it, it risks allocating more heap than available, and the Linux kernel kills it with an OOMKilled. The first step is to use the -XX:MaxRAMPercentage and -XX:InitialRAMPercentage flags, which set the heap as a percentage of the container's total memory, not the host machine's.

The second step is to disable features designed for physical servers, like ergonomics which selects the garbage collector based on hardware. In a container, you want predictable behavior.

Sponsored Protocol

Recommended JVM configuration for Docker

Here are the parameters we use for Java containers in production:

java \
  -XX:MaxRAMPercentage=75.0 \
  -XX:InitialRAMPercentage=50.0 \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+UseStringDeduplication \
  -XX:TieredStopAtLevel=1 \
  -jar app.jar
  • MaxRAMPercentage=75.0: leave 25% of memory for the OS and network buffers.
  • UseG1GC: the default garbage collector, balanced between throughput and pauses.
  • MaxGCPauseMillis=200: limits GC pauses, critical for APIs that must respond quickly.
  • TieredStopAtLevel=1: disables C2 compilation after startup, reduces startup time. Use only if you don't need extreme peak performance at runtime.

Note: TieredStopAtLevel=1 speeds up startup but reduces peak performance. If your application does intensive calculations, leave full compilation. For most web APIs, the difference is negligible.

How to reduce startup time with CDS and AppCDS?

Class Data Sharing (CDS) is a JVM feature that saves loaded classes to a file, so on the next execution the JVM loads them faster. With Java 17, you can use AppCDS to include your application's classes too, not just system ones.

The result? A startup that goes from 5-6 seconds down to 2-3 seconds. In an environment with many microservices that scale often, this translates to faster response to traffic spikes.

Generate the CDS file in the Dockerfile

Here's how to integrate AppCDS into your build process:

Sponsored Protocol

FROM eclipse-temurin:17-jre-alpine AS builder
WORKDIR /app
COPY target/*.jar app.jar
# Generate the class list file
RUN java -XX:ArchiveClassesAtExit=app.jsa \
    -Dspring.context.exit=onRefresh \
    -jar app.jar

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/app.jar .
COPY --from=builder /app/app.jsa .
ENTRYPOINT ["java", "-XX:SharedArchiveFile=app.jsa", "-jar", "app.jar"]

The trick is using spring.context.exit=onRefresh to make the application exit right after the Spring context starts, without having to stop it manually. This generates the app.jsa file with all loaded classes. At runtime, the JVM uses that file and skips the class parsing phase.

Note: the CDS file is tied to the JVM version and libraries. If you upgrade a dependency, you must regenerate it. It's an extra step, but the startup time gain is significant.

How to manage security and native dependencies in containers?

A secure Java container isn't just a small image. It's an image that doesn't run as root, doesn't contain debug tools, and has verified dependencies. The first step is to create a dedicated user in the Dockerfile, as we did in the initial example. The second is to use only official, signed base images.

Native libraries (JNI) are the Achilles' heel of Java containers. If your application uses a library like OpenCV or a JDBC driver with native components, you must ensure the base image contains the necessary system dependencies. Alpine, with its musl libc, can cause mysterious errors. In that case, switch to an Ubuntu or Debian slim base.

Sponsored Protocol

Vulnerability scanning and reproducible builds

Never publish an image without scanning it. Tools like trivy or grype integrate into your CI pipeline and tell you if there are known CVEs in your dependencies. We do this for every project: it's a check that takes a minute and prevents you from going to production with a known vulnerability.

For reproducible builds, use fixed versions of base images (e.g., eclipse-temurin:17-jre-alpine@sha256:... ) and lock dependency versions in your pom.xml or build.gradle. A container that behaves the same way every time is a container you can debug with confidence.

In summary

Optimizing Docker with Java is not a luxury, it's an operational necessity. Smaller images, faster startups, and a JVM configured for containers mean reduced cloud costs and applications that respond when needed. Here are the immediate actions to take:

  • Rewrite the Dockerfile with a multi-stage build: JDK to compile, JRE to run.
  • Configure the JVM with MaxRAMPercentage and UseG1GC to avoid OOMKilled.
  • Generate the CDS file to cut startup time by 30-50%.
  • Scan the image with Trivy before every registry push.
  • Never use the root user in the container: create a dedicated user.

If you want to dive deeper into how these techniques fit into a broader architecture, check out our guide on modern Java and JVM for scalable code. We at Meteora Web use these same principles every day for clients who need to handle traffic without burning budget. Your Java code deserves a container that's up to the task.

> share
Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()