Container Builds

Optimal Dockerfile for Java with Gradle

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

# syntax=docker/dockerfile:1

FROM eclipse-temurin:21-jdk AS build

ENV GRADLE_HOME=/opt/gradle \
    GRADLE_USER_HOME=/cache/.gradle \
    GRADLE_OPTS="-Dorg.gradle.daemon=false \
    -Dorg.gradle.parallel=true \
    -Dorg.gradle.caching=true \
    -Xmx2g"

ARG GRADLE_VERSION=8.10
RUN apt-get update && apt-get install -y --no-install-recommends unzip \
    && wget -q https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip \
    && unzip gradle-${GRADLE_VERSION}-bin.zip -d /opt \
    && ln -s /opt/gradle-${GRADLE_VERSION} /opt/gradle \
    && rm gradle-${GRADLE_VERSION}-bin.zip \
    && apt-get remove -y unzip \
    && rm -rf /var/lib/apt/lists/*

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

WORKDIR /app

COPY build.gradle ./

RUN --mount=type=cache,target=/cache/.gradle \
    gradle dependencies --no-daemon --stacktrace

COPY src/ src/

RUN --mount=type=cache,target=/cache/.gradle \
    gradle build -x test --no-daemon --stacktrace --build-cache

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/build/libs/*.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 Gradle:

  • Multi-stage builds for smaller final images
  • Gradle cache mounts for dependency and build caching
  • Gradle build optimizations for container environments
  • Security optimizations with non-root users

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

FROM eclipse-temurin:21-jdk AS build

ENV GRADLE_HOME=/opt/gradle \
    GRADLE_USER_HOME=/cache/.gradle \
    GRADLE_OPTS="-Dorg.gradle.daemon=false \
    -Dorg.gradle.parallel=true \
    -Dorg.gradle.caching=true \
    -Xmx2g"

We use Eclipse Temurin 21 JDK and configure Gradle with optimized settings:

  • GRADLE_USER_HOME=/cache/.gradle points to our cache mount location
  • gradle.daemon=false disables the daemon (not beneficial in containers)
  • gradle.parallel=true enables parallel execution for faster builds
  • gradle.caching=true enables Gradle's build cache
  • -Xmx2g sets maximum heap size for Gradle

Installing Gradle

ARG GRADLE_VERSION=8.10
RUN apt-get update && apt-get install -y --no-install-recommends unzip \
    && wget -q https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip \
    && unzip gradle-${GRADLE_VERSION}-bin.zip -d /opt \
    && ln -s /opt/gradle-${GRADLE_VERSION} /opt/gradle \
    && rm gradle-${GRADLE_VERSION}-bin.zip \
    && apt-get remove -y unzip \
    && rm -rf /var/lib/apt/lists/*

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

We install a specific Gradle version for reproducible builds and clean up build tools afterward to keep the layer small.

Dependency resolution and caching

WORKDIR /app

COPY build.gradle ./

RUN --mount=type=cache,target=/cache/.gradle \
    gradle dependencies --no-daemon --stacktrace

We copy only the build.gradle first to leverage Docker layer caching. The dependencies task downloads all dependencies, with a cache mount to persist between builds.

Building the application

COPY src/ src/

RUN --mount=type=cache,target=/cache/.gradle \
    gradle build -x test --no-daemon --stacktrace --build-cache

After copying the source code, we build the application with the same cache mount. Key options:

  • -x test excludes tests from the build (run in CI/CD pipeline)
  • --no-daemon ensures no daemon process is left running
  • --build-cache enables Gradle's build cache for faster incremental builds

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/build/libs/*.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"]

The runtime stage uses Eclipse Temurin 21 JRE for a reliable runtime environment. We create a non-root user for security and copy the built JAR file. The JVM is configured with production 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=/cache/.gradle \
    gradle dependencies --no-daemon --stacktrace

Cache Mount Parameters Explained

  • type=cache: Specifies this is a cache mount that persists across builds.
  • target=/cache/.gradle: The mount point for Gradle's cache directory (configured via GRADLE_USER_HOME).

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