Spring Boot Dockerfile: Multi-Stage Maven Build on JRE 21

This Spring Boot Dockerfile builds your executable jar with Maven on maven:3.9-eclipse-temurin-21 and runs it on the much smaller eclipse-temurin:21-jre-alpine runtime as a dedicated non-root user. The embedded Tomcat server listens on port 8080. It suits typical Spring Web, Data and Actuator services.

CI status
Build passing
Image size (uncompressed)
227.6 MB
Build time
54 s
Runtime image
eclipse-temurin:21-jre-alpine
Port
8080
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -B -q dependency:go-offline
COPY src ./src
RUN mvn -B -q package -DskipTests

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
COPY --from=build /app/target/*.jar app.jar
USER spring:spring
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
.dockerignore
.git
target/
.idea/
*.iml
.env
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM maven:3.9-eclipse-temurin-21 AS build

    The build stage has Maven and a full JDK 21. Both are needed to compile but would only add weight at runtime.

  2. COPY pom.xml . RUN mvn -B -q dependency:go-offline

    Resolving dependencies from pom.xml alone creates a cached layer, so later builds that only change Java sources skip the slow download step.

  3. COPY src ./src RUN mvn -B -q package -DskipTests

    Compiles and packages the application in batch mode. Tests are skipped here because they should already have run in your CI pipeline.

  4. FROM eclipse-temurin:21-jre-alpine

    The runtime stage only needs a Java runtime, not a compiler. The JRE on Alpine is far smaller than the JDK build image.

  5. RUN addgroup -S spring && adduser -S spring -G spring

    Creates a system group and user named spring. Alpine uses addgroup and adduser rather than the useradd tool found on Debian images.

  6. COPY --from=build /app/target/*.jar app.jar

    Copies the executable jar under a predictable name, so the entry point doesn't depend on your artifact's version number.

  7. USER spring:spring EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]

    Drops root privileges, documents the Tomcat port, and starts the JVM directly so stop signals trigger Spring's graceful shutdown.

Build and run

terminal
docker build -t spring-boot-app .
docker run --rm -p 8080:8080 spring-boot-app

Pass profiles and secrets at runtime, for example -e SPRING_PROFILES_ACTIVE=prod. Then open http://localhost:8080, or the Actuator health endpoint if it's enabled.

Other Spring Boot Dockerfile variants

A Maven build plus an Alpine JRE is a simple, widely compatible default. Gradle users need a Gradle-based variant, and layered-jar or native-image variants trade build complexity for faster rebuilds or startup. They appear in the table below once they pass CI.

VariantRuntime imageImage sizeCI
Layered jar (Maven + JRE 21)eclipse-temurin:21-jre-alpine227.8 MBPassing
Maven build + Temurin 21 JRE Alpineeclipse-temurin:21-jre-alpine227.6 MBPassing

Troubleshooting

no main manifest attribute, in /app/app.jar
The jar is not executable. Make sure spring-boot-maven-plugin is declared in pom.xml so package repackages it with the Spring Boot launcher.
COPY failed because the target jar wildcard matched more than one file
Your build produces several jars, for example a sources jar. Copy the exact file name, or set finalName in pom.xml to produce one predictable artifact.
The container is killed for exceeding its memory limit
Container-aware JVMs size the heap from the limit, but metaspace and threads add overhead. Set JAVA_TOOL_OPTIONS with MaxRAMPercentage to leave headroom.

FAQ

Why skip tests inside the Docker build?
Tests belong in CI, where failures are reported properly and can use services like databases. Running them again during the image build only slows every deploy.
Should I use layered jars?
Layered jars split dependencies from application classes so Docker can cache them separately. They speed up rebuilds and pushes for large apps, at the cost of a slightly longer Dockerfile.
Is Alpine fine for Java 21?
Eclipse Temurin publishes official Alpine JRE builds for Java 21. If a native library in your stack expects glibc, switch the runtime stage to the Ubuntu-based JRE tag instead.