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
# 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"].git
target/
.idea/
*.iml
.env
Dockerfile
.dockerignoreBefore you build
- Use spring-boot-maven-plugin so mvn package produces a single executable jar in target/.
- Run your tests in CI before building the image, because the Docker build skips them.
- Commit pom.xml together with your source in src/.
How this Dockerfile works
FROM maven:3.9-eclipse-temurin-21 AS buildThe build stage has Maven and a full JDK 21. Both are needed to compile but would only add weight at runtime.
COPY pom.xml . RUN mvn -B -q dependency:go-offlineResolving dependencies from pom.xml alone creates a cached layer, so later builds that only change Java sources skip the slow download step.
COPY src ./src RUN mvn -B -q package -DskipTestsCompiles and packages the application in batch mode. Tests are skipped here because they should already have run in your CI pipeline.
FROM eclipse-temurin:21-jre-alpineThe runtime stage only needs a Java runtime, not a compiler. The JRE on Alpine is far smaller than the JDK build image.
RUN addgroup -S spring && adduser -S spring -G springCreates a system group and user named spring. Alpine uses addgroup and adduser rather than the useradd tool found on Debian images.
COPY --from=build /app/target/*.jar app.jarCopies the executable jar under a predictable name, so the entry point doesn't depend on your artifact's version number.
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
docker build -t spring-boot-app .
docker run --rm -p 8080:8080 spring-boot-appPass 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.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Layered jar (Maven + JRE 21) | eclipse-temurin:21-jre-alpine | 227.8 MB | Passing |
| Maven build + Temurin 21 JRE Alpine | eclipse-temurin:21-jre-alpine | 227.6 MB | Passing |
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.