By WebNest Studio Engineering Team
Spring Boot Tutorial
Docker and Deployment Concepts for Spring Boot
Packaging a Spring Boot application as a Docker image makes it runnable identically on any machine with Docker installed — a developer's laptop, a CI pipeline, or a production Kubernetes cluster — without worrying about differing Java versions or OS-level dependencies. Spring Boot's default packaging (a self-contained "fat jar" via the Maven/Gradle plugin) makes this especially straightforward.
Packaging as an Executable Jar
Running mvn package (or ./gradlew build) with the Spring Boot plugin produces a single executable jar containing your compiled classes plus every dependency, runnable directly with java -jar app.jar — no separate application server or classpath setup required. This is exactly what gets copied into a Docker image.
A Multi-Stage Dockerfile
A multi-stage build uses one stage with a full JDK and build tool to compile and package the application, then copies only the resulting jar into a second, much smaller stage based on a minimal JRE (not JDK) base image. This keeps the final image small (no build tools, no source code, no Maven cache) and reduces the attack surface, since the shipped image only needs a runtime, not a development toolchain.
Layers: Why the Order of COPY Lines Matters
Docker caches each instruction as a layer and rebuilds only from the first layer that changed. A Spring Boot jar mixes two very different things: dependencies (tens of megabytes that change maybe once a month) and your own classes (a few kilobytes that change on every commit). Copying the whole jar as one layer means every code change re-uploads and re-downloads all of it. Spring Boot can split the jar for you: java -Djarmode=tools -jar app.jar extract --layers --destination extracted writes four folders, dependencies, spring-boot-loader, snapshot-dependencies and application. Copy them in that order and a typical code change only touches the last, tiny layer. For a small web + JPA application the split was 78 jars and 54 MB in dependencies against one 8 KB jar in application.
Memory, Users and Signals
Three details decide whether a container behaves well in production. Memory: the JVM reads the container's memory limit, but by default it only uses 25% of it for the heap. Set -XX:MaxRAMPercentage=75 so the heap uses most of the limit while leaving room for metaspace, threads and buffers. User: images run as root unless told otherwise, so create a normal user and switch to it with USER. Signals: use the exec form ENTRYPOINT ["java", ...], not the shell form. In the shell form, a shell is process 1 and does not pass SIGTERM on to Java, so the orchestrator kills the application after its timeout instead of letting it shut down cleanly.
Deployment Considerations
In production, containers are typically run under an orchestrator (Kubernetes, ECS, etc.) that expects the container to expose a health check (Actuator's /actuator/health fits this role well), receive configuration through environment variables rather than baked-in files, and shut down gracefully on a SIGTERM signal so in-flight requests can finish. Spring Boot's embedded web server does this by default: it stops accepting new requests and waits up to 30 seconds (spring.lifecycle.timeout-per-shutdown-phase) for active ones to complete.
Building Without a Dockerfile
If you do not need full control over the image, ./mvnw spring-boot:build-image (or ./gradlew bootBuildImage) builds an OCI image with Cloud Native Buildpacks. It picks a JRE, applies the layering above, runs as a non-root user and sets memory flags for you. A Dockerfile is still the better choice when you need a specific base image or extra operating-system packages.
Example
A multi-stage Dockerfile for a Spring Boot application
# --- Stage 1: build (the Maven image has both JDK and Maven) ---
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -q dependency:go-offline # cached until pom.xml changes
COPY src ./src
RUN mvn -q -DskipTests package && cp target/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted
# --- Stage 2: run (JRE only, no build tools) ---
FROM eclipse-temurin:21-jre
RUN useradd --system --create-home spring
USER spring
WORKDIR /app
COPY --from=build /app/extracted/dependencies/ ./
COPY --from=build /app/extracted/spring-boot-loader/ ./
COPY --from=build /app/extracted/snapshot-dependencies/ ./
COPY --from=build /app/extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
# Build and run:
# docker build -t webnest-app .
# docker run -p 8080:8080 -m 512m -e SPRING_PROFILES_ACTIVE=prod webnest-app
extract --layers produces (checked on Spring Boot 4.1.1):
extracted/dependencies/lib/ 78 jars, 54 MB -> changes rarely
extracted/spring-boot-loader/ empty for this layout
extracted/snapshot-dependencies/ empty unless you use -SNAPSHOT versions
extracted/application/app.jar 8 KB -> changes on every commit
The four folders copied into one directory give app.jar + lib/, and
java -jar app.jar starts normally from there:
... Started App in 5.593 seconds (process running for 5.906)
GET /actuator/health -> {"groups":["liveness","readiness"],"status":"UP"}
(The exec-form ENTRYPOINT makes java process 1, so it receives SIGTERM directly.)
Common Mistakes
- Using a single-stage Dockerfile based on a full JDK image for the final runtime, producing a needlessly large image that also ships the build toolchain.
- Running mvn in an eclipse-temurin JDK image. Those images contain Java but not Maven; use a maven:… image or the project's ./mvnw wrapper.
- Baking environment-specific configuration (database URLs, secrets) directly into the image instead of injecting them at runtime via environment variables or a secrets manager.
- Copying the whole jar as a single layer, so every one-line code change pushes and pulls all the dependencies again.
- Using the shell form of ENTRYPOINT (ENTRYPOINT java -jar app.jar). The shell does not forward SIGTERM, so graceful shutdown never runs.
- Leaving the JVM at its default heap of 25% of the container limit, or setting -Xmx equal to the limit so the container is OOM-killed. MaxRAMPercentage=75 is a common middle ground.
- Running the container as root when nothing in the application needs it.
Exercise
A teammate is about to make this mistake: Using a single-stage Dockerfile based on a full JDK image for the final runtime, producing a needlessly large image that also ships the build toolchain. Use Packaging as an Executable Jar to review the decision. Write a corrected implementation or design sketch that follows Spring Boot packages a self-contained executable jar, which is exactly what a Docker image needs to run the app., then name one edge case you would check.
Show hints
- Start with Packaging as an Executable Jar and compare your approach with A multi-stage Dockerfile for a Spring Boot application. Identify the decision or behavior the lesson recommends.
- Walk through your scenario step by step. Confirm that it follows Spring Boot packages a self-contained executable jar, which is exactly what a Docker image needs to run the app. and that it would catch the failure described by Using a single-stage Dockerfile based on a full JDK image for the final runtime, producing a needlessly large image that also ships the build toolchain..
Quick quiz
1. Which choice best describes the recommended approach in Docker and Deployment Concepts for Spring Boot?
- A. Using a single-stage Dockerfile based on a full JDK image for the final runtime, producing a needlessly large image that also ships the build toolchain.
- B. Running mvn in an eclipse-temurin JDK image. Those images contain Java but not Maven; use a maven:… image or the project's ./mvnw wrapper.
- C. Spring Boot packages a self-contained executable jar, which is exactly what a Docker image needs to run the app.
Show answer
C. This is the recommended approach: Spring Boot packages a self-contained executable jar, which is exactly what a Docker image needs to run the app. Avoid: Using a single-stage Dockerfile based on a full JDK image for the final runtime, producing a needlessly large image that also ships the build toolchain.
Interview questions
Why does this principle matter in Docker and Deployment Concepts for Spring Boot: Spring Boot packages a self-contained executable jar, which is exactly what a Docker image needs to run the app.
Show answer
Spring Boot packages a self-contained executable jar, which is exactly what a Docker image needs to run the app. Apply this in Packaging as an Executable Jar; the example A multi-stage Dockerfile for a Spring Boot application shows one way to do it. Avoid Using a single-stage Dockerfile based on a full JDK image for the final runtime, producing a needlessly large image that also ships the build toolchain. and explain how your implementation or design prevents that failure.
Key Points to Remember
- Spring Boot packages a self-contained executable jar, which is exactly what a Docker image needs to run the app.
- A multi-stage Dockerfile builds with a JDK + Maven image, then ships only the application on a minimal JRE base image for a smaller, safer runtime image.
- java -Djarmode=tools -jar app.jar extract --layers splits the jar so dependencies and code are cached as separate Docker layers.
- Configuration should be injected at container run time (environment variables, profiles) rather than baked into the image.
- Use the exec form of ENTRYPOINT, a non-root user and -XX:MaxRAMPercentage; graceful shutdown on SIGTERM is on by default.
- spring-boot:build-image builds a well-configured image without a Dockerfile.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering