Build a Java app
A Java service does not need a full JDK to run, only a JRE. So build the jar with the toolchain in one stage, then run it on the slim jre base in the next. The compiler, the build tool, and the dependency cache stay in the build stage, and the final image carries the runtime and your jar.
These build on ghcr.io/quenchworks/images/maven:3.9 or ghcr.io/quenchworks/images/gradle:9, then run on ghcr.io/quenchworks/images/jre:21. Match the JRE line to the Java version you compiled for.
The multi-stage Dockerfile
# Build stage: resolve dependencies, then package the jar.FROM ghcr.io/quenchworks/images/maven:3.9 AS buildWORKDIR /app
# Resolve dependencies first so the layer caches when only code changes.COPY pom.xml ./RUN ["mvn", "-B", "dependency:go-offline"]COPY src ./srcRUN ["mvn", "-B", "-o", "package", "-DskipTests"]
# Runtime stage: run the jar on the slim JRE base, nonroot.FROM ghcr.io/quenchworks/images/jre:21 AS runtimeWORKDIR /appCOPY --from=build /app/target/*.jar /app/app.jarUSER 1001EXPOSE 8080ENTRYPOINT ["java", "-jar", "/app/app.jar"]# Build stage: build the jar with Gradle.FROM ghcr.io/quenchworks/images/gradle:9 AS buildWORKDIR /app
# Cache dependencies before copying the full source.COPY build.gradle.kts settings.gradle.kts ./RUN ["gradle", "dependencies", "--no-daemon"]COPY src ./srcRUN ["gradle", "bootJar", "--no-daemon", "-x", "test"]
FROM ghcr.io/quenchworks/images/jre:21 AS runtimeWORKDIR /appCOPY --from=build /app/build/libs/*.jar /app/app.jarUSER 1001EXPOSE 8080ENTRYPOINT ["java", "-jar", "/app/app.jar"]What each stage does
- Build. The
mavenorgradleimage carries the JDK and the build tool. Resolve dependencies before copying the source so that layer caches across builds, then package the jar. The dependency cache and the compiler stay in this stage. - Runtime. Start from
jre:21, copy the one jar in, drop touid 1001, and run it. No JDK, no build tool, no source.
Next
- Building a self-contained jar (Spring Boot, Quarkus, a shaded jar) keeps the runtime stage to a single
COPY; multi-jar layouts work too, just copy the dependency and app layers. - Pin by digest so each build runs exactly the base that was scanned and signed.