WarpBuild LogoWarpBuild Docs

Java with Gradle

Best practices for Dockerfile for Java with Gradle

🐳 Annotated Dockerfile for Java with Gradle:

# Stage 1: Build the application
FROM eclipse-temurin:21 AS builder
 
# Set working directory
WORKDIR /app
 
# Install Gradle
RUN apt-get update && apt-get install -y --no-install-recommends \
    gradle \
    && rm -rf /var/lib/apt/lists/*
 
# Copy Gradle files for dependency resolution
COPY settings.gradle build.gradle ./
 
# Download dependencies and cache them
# Using Gradle cache mount to speed up builds
RUN --mount=type=cache,target=/root/.gradle \
    gradle dependencies --no-daemon
 
# Copy source code after dependencies for better caching
COPY src/ ./src/
 
# Build the application (skipping tests for faster builds)
RUN --mount=type=cache,target=/root/.gradle \
    gradle build --no-daemon -x test
 
# Stage 2: Create a minimal runtime image
FROM eclipse-temurin:21-alpine
 
# Create a non-root user to run the application
# Alpine has different syntax for user/group creation
RUN addgroup -S -g 1001 javauser && \
    adduser -S -u 1001 -G javauser javauser
 
# Set working directory
WORKDIR /app
 
# Copy the built JAR from the builder stage
COPY --from=builder /app/build/libs/*.jar app.jar
 
# Set ownership to the non-root user
RUN chown -R javauser:javauser /app
 
# Switch to non-root user
USER javauser
 
# Expose the application port
EXPOSE 8080
 
# Configure container-optimized JVM settings
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom"
 
# Run the application
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar app.jar" ]

🔍 Why these are best practices:

✅ Multi-stage builds

  • Reduces final image size dramatically.
  • Eliminates build tools and source code from the runtime image.
  • Improves security by minimizing the attack surface.

✅ Gradle dependency caching

  • Uses Docker's cache mount feature to cache Gradle dependencies.
  • Significantly speeds up build times on iterative builds.
  • Avoids redundant downloads by preserving the Gradle cache between builds.

✅ Separation of dependency resolution from builds

  • Resolves dependencies separately from the build step.
  • Takes advantage of Docker layer caching for unchanged dependencies.
  • Speeds up rebuilds when only application code changes.

✅ JRE-only runtime image

  • Uses a minimal JRE instead of full JDK for the runtime image.
  • Reduces container size by eliminating compilation tools.
  • Improves security by removing unnecessary components.

✅ Container-optimized Java options

  • XX:+UseContainerSupport ensures JVM respects container memory limits.
  • XX:MaxRAMPercentage=75.0 prevents excessive memory usage.
  • Improves application stability in containerized environments.

🚀 Additional Dockerfile best practices you can adopt:

Enable Spring Boot layered JARs

For improved caching and smaller image updates:

# Extract layered JAR (for Spring Boot applications)
COPY --from=builder /app/build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
 
# Create optimal layer order
FROM eclipse-temurin:21-alpine
WORKDIR /app
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"]

Use Gradle's --no-daemon option for containers

Optimize Gradle for container builds:

# Ensure no daemons are running for containerized builds
RUN --mount=type=cache,target=/root/.gradle \
    ./gradlew build --no-daemon -x test

Add health checks

Monitor container health for better orchestration:

HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

Use .dockerignore

Exclude unnecessary files from your Docker build context:

.gradle/
build/
!build/libs/*.jar
.git/
.github/
.gitignore
README.md
Dockerfile
docker-compose.yml

Fine-tune garbage collection for containers

Optimize memory usage in containerized environments:

ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+ExplicitGCInvokesConcurrent -XX:+ParallelRefProcEnabled -XX:+UseStringDeduplication"

Set up for GraalVM native images

Create ultra-fast startup and smaller footprint:

FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY . .
RUN --mount=type=cache,target=/root/.gradle \
    ./gradlew nativeCompile -x test
 
FROM oraclelinux:8-slim
COPY --from=builder /app/build/native/nativeCompile/application /app/application
ENTRYPOINT ["/app/application"]

Configure JVM for reliable container operation

Advanced JVM settings for containerized applications:

ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0 -XX:+UseG1GC -XX:G1HeapRegionSize=4M -XX:+UseStringDeduplication -XX:+ExitOnOutOfMemoryError"

Enable JVM flight recorder for diagnostics

For production troubleshooting capabilities:

ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+FlightRecorder -XX:StartFlightRecording=disk=true,dumponexit=true,filename=/tmp/recording.jfr,maxsize=1024m,settings=profile"

By following these practices, you'll create Docker images for your Java Gradle applications that are secure, efficient, and optimized for both development and production environments. These approaches minimize build times, reduce image sizes, and ensure consistent behavior across different deployment environments.

Last updated on