Container Builds

Optimal Dockerfile for Java with Maven

Below is an example Dockerfile that we recommend at Depot for building images for Java applications with Maven.

# syntax=docker/dockerfile:1

FROM eclipse-temurin:21-jdk AS build

ENV JAVA_OPTS="-XX:+UseContainerSupport \
    -XX:MaxRAMPercentage=75.0 \
    -XX:InitialRAMPercentage=50.0 \
    -XX:+UseG1GC \
    -XX:+UseStringDeduplication" \
    MAVEN_HOME=/opt/maven \
    MAVEN_CONFIG=/root/.m2 \
    MAVEN_OPTS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"

ARG MAVEN_VERSION=3.9.11
RUN wget -q https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
    && tar -xzf apache-maven-${MAVEN_VERSION}-bin.tar.gz -C /opt \
    && ln -s /opt/apache-maven-${MAVEN_VERSION} /opt/maven \
    && rm apache-maven-${MAVEN_VERSION}-bin.tar.gz

ENV PATH="${MAVEN_HOME}/bin:${PATH}"

WORKDIR /app

COPY pom.xml ./

RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B -q

COPY src/ src/

RUN --mount=type=cache,target=/root/.m2 \
    mvn clean package -B -DskipTests

FROM eclipse-temurin:21-jre AS runtime

RUN groupadd -g 1001 appgroup && \
    useradd -u 1001 -g appgroup -m -d /app -s /bin/false appuser

WORKDIR /app

COPY --from=build --chown=appuser:appgroup /app/target/*.jar app.jar

ENV JAVA_OPTS="-server \
    -XX:+UseContainerSupport \
    -XX:MaxRAMPercentage=75.0 \
    -XX:+UseG1GC \
    -Djava.security.egd=file:/dev/./urandom"

USER appuser

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Explanation of the Dockerfile

At a high level, here are the things we're optimizing in our Docker build for a Java application with Maven:

  • Multi-stage builds for smaller final images
  • Maven cache mounts for dependency caching
  • JVM performance tuning for containers
  • Security optimizations with non-root users

Stage 1: FROM eclipse-temurin:21-jdk AS build

FROM eclipse-temurin:21-jdk AS build

ENV JAVA_OPTS="-XX:+UseContainerSupport \
    -XX:MaxRAMPercentage=75.0 \
    -XX:InitialRAMPercentage=50.0 \
    -XX:+UseG1GC \
    -XX:+UseStringDeduplication" \
    MAVEN_HOME=/opt/maven \
    MAVEN_CONFIG=/root/.m2 \
    MAVEN_OPTS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"

We use Eclipse Temurin 21 JDK for the build stage and configure JVM options for optimal build performance:

  • UseContainerSupport enables container-aware memory settings
  • MaxRAMPercentage=75.0 limits heap to 75% of container memory
  • UseG1GC enables the G1 garbage collector for better performance
  • TieredCompilation with TieredStopAtLevel=1 speeds up build times

Installing Maven

ARG MAVEN_VERSION=3.9.11
RUN wget -q https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
    && tar -xzf apache-maven-${MAVEN_VERSION}-bin.tar.gz -C /opt \
    && ln -s /opt/apache-maven-${MAVEN_VERSION} /opt/maven \
    && rm apache-maven-${MAVEN_VERSION}-bin.tar.gz

ENV PATH="${MAVEN_HOME}/bin:${PATH}"

We install a specific Maven version for reproducible builds and clean up the downloaded archive to keep the layer small.

Dependency resolution and caching

WORKDIR /app

COPY pom.xml ./

RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B -q

We copy only the pom.xml first to leverage Docker layer caching. The dependency:go-offline goal downloads all dependencies to the local repository, with a cache mount to persist between builds.

Building the application

COPY src/ src/

RUN --mount=type=cache,target=/root/.m2 \
    mvn clean package -B -DskipTests

After copying the source code, we build the application with the same cache mount. The -B flag enables batch mode, and -DskipTests skips running tests during the build (tests should be run in CI/CD pipeline).

Stage 2: FROM eclipse-temurin:21-jre AS runtime

FROM eclipse-temurin:21-jre AS runtime

RUN groupadd -g 1001 appgroup && \
    useradd -u 1001 -g appgroup -m -d /app -s /bin/false appuser

WORKDIR /app

COPY --from=build --chown=appuser:appgroup /app/target/*.jar app.jar

The runtime stage uses Eclipse Temurin JRE for a reliable runtime environment. We create a non-root user for security and copy only the built JAR file from the build stage.

Runtime JVM configuration

ENV JAVA_OPTS="-server \
    -XX:+UseContainerSupport \
    -XX:MaxRAMPercentage=75.0 \
    -XX:+UseG1GC \
    -Djava.security.egd=file:/dev/./urandom"

USER appuser

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

We configure production JVM settings:

  • -server enables server mode for better long-running performance
  • UseContainerSupport and MaxRAMPercentage for container-aware memory management
  • UseG1GC enables the G1 garbage collector for better performance
  • java.security.egd uses /dev/urandom for faster startup

Understanding BuildKit Cache Mounts

Cache mounts in this Dockerfile speed up builds by persisting the package manager cache. This means that even when a layer needs to be rebuilt, your package manager only fetches what's new or updated. This Dockerfile uses the following cache mount syntax:

RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B -q

Cache Mount Parameters Explained

  • type=cache: Specifies this is a cache mount that persists across builds.
  • target=/root/.m2: The mount point for Maven's local repository where all downloaded JARs, POMs, and metadata are stored.

For more information regarding Maven cache mounts, please visit the official Maven documentation.