Dockerize Spring Boot App
Containerize a Spring Boot application with a production-ready Dockerfile: JRE base image, non-root user, health check, and JVM memory flags.
Spring Boot applications package neatly into a single executable jar, which makes them a natural fit for containers. A good Docker setup runs the jar on a small Java runtime image, as a non-root user, with sensible memory settings and a health check. This lesson builds that Dockerfile step by step and runs it with a database through Docker Compose.
Project Layout
Assume a standard Maven project with the Spring Boot Actuator dependency:
demo/
pom.xml
mvnw
src/
Dockerfile
.dockerignore
compose.yaml
Add Actuator so the container can report its health:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.properties
management.endpoints.web.exposure.include=health
management.endpoint.health.probes.enabled=true
/actuator/health then returns {"status":"UP"} when the application is healthy.
A Production-Ready Dockerfile
This Dockerfile expects the jar to be built first (for example by CI). The next lesson shows how to build it inside Docker with a multi-stage build.
FROM eclipse-temurin:21-jre
# curl is used by the HEALTHCHECK
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# Run as an unprivileged user
RUN groupadd --system app && useradd --system --gid app --no-create-home app
WORKDIR /app
COPY target/*.jar app.jar
# Container-aware JVM settings; override at runtime with -e JAVA_OPTS=...
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS http://localhost:8080/actuator/health || exit 1
# "exec" makes java PID 1 so it receives SIGTERM for graceful shutdown
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
Build and run:
./mvnw -DskipTests package
docker build -t demo-app:1.0 .
docker run -d --name demo -p 8080:8080 demo-app:1.0
curl http://localhost:8080/actuator/health
# {"status":"UP"}
docker ps
# STATUS: Up 45 seconds (healthy)
Explanation of the choices:
eclipse-temurin:21-jrecontains only a Java runtime, not the full JDK, so the image is smaller and has less attack surface.- A system user without a home directory runs the process instead of root.
-XX:MaxRAMPercentage=75.0sizes the heap relative to the container's memory limit. Modern JVMs detect cgroup limits by default, so-Xmxhard-coding is not needed.-XX:+ExitOnOutOfMemoryErrormakes the JVM exit on anOutOfMemoryErrorso Docker or the orchestrator can restart a clean process.exec java ...replaces the shell with the JVM sodocker stopsignals reach Spring Boot, which then shuts down gracefully.
The .dockerignore File
.git
.idea
.mvn/wrapper/maven-wrapper.jar
target/classes
target/test-classes
*.log
.env
Keep target/*.jar available to the build, and exclude everything else that is not needed.
Layered Jars for Faster Rebuilds
A Spring Boot fat jar changes whenever your code changes, so the whole jar layer (dependencies included) is re-pushed and re-pulled. Spring Boot jars contain a layer index that separates dependencies from your application classes. Extracting those layers lets Docker cache the stable parts:
java -Djarmode=layertools -jar target/demo.jar extract
# creates dependencies/, spring-boot-loader/, snapshot-dependencies/, application/
Newer Spring Boot versions (3.3 and later) also provide a -Djarmode=tools mode for this purpose, so check the documentation for your version. The extracted directories are then copied into the image in separate COPY instructions, with the frequently changing application/ last. Alternatively, Spring Boot can build an optimized image without a Dockerfile:
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=demo-app:1.0
Running with PostgreSQL via Compose
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/appdb
SPRING_DATASOURCE_USERNAME: app
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
JAVA_OPTS: "-XX:MaxRAMPercentage=70.0"
depends_on:
db:
condition: service_healthy
deploy:
resources:
limits:
memory: 768m
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 10
volumes:
pgdata:
Spring Boot's relaxed binding maps SPRING_DATASOURCE_URL to spring.datasource.url, so configuration comes from the environment and the same image works in every environment. Use db as the hostname because that is the Compose service name.
echo "DB_PASSWORD=change-me" > .env
docker compose up -d --build
docker compose logs -f app
Common Mistakes
- Using the full JDK image to run the app. It is larger and ships compilers you do not need at runtime.
- Running as root. Add a dedicated user and
USERinstruction. - Using the shell form
ENTRYPOINT java -jar .... The app may not receiveSIGTERM, causing a 10-second kill ondocker stop. - Ignoring memory limits. An unconstrained heap inside a limited container leads to OOM kills. Use
MaxRAMPercentageand set a container memory limit. - Baking credentials into
application.propertiesin the image. Pass them as environment variables or secrets at runtime.
Best Practices
- Use a JRE base image with a pinned version such as
eclipse-temurin:21-jre. - Externalize configuration with environment variables and Spring profiles.
- Expose Actuator health endpoints and wire them into
HEALTHCHECKor your orchestrator's probes. - Leave headroom between the heap and the container limit for metaspace, threads, and native memory.
- Build the jar in CI or in a multi-stage Dockerfile so builds are reproducible.
Interview Questions
1. How do you containerize a Spring Boot application?
Build the executable jar with Maven or Gradle, then write a Dockerfile that starts from a Java runtime image, copies the jar, exposes port 8080, and runs java -jar. Add a non-root user, a health check, and JVM options appropriate for containers.
2. Why use a JRE image instead of a JDK image?
The JRE includes only what is needed to run Java applications. The image is smaller and contains fewer tools an attacker could misuse. The JDK is only needed at build time.
3. How does the JVM behave with container memory limits?
Modern JVMs read cgroup limits and size the heap from them. -XX:MaxRAMPercentage sets the heap as a percentage of the container's memory, leaving room for non-heap memory. Without a container limit, the JVM sees the host's memory.
4. How do you pass configuration, such as database credentials, to the container?
Through environment variables such as SPRING_DATASOURCE_URL, which Spring Boot maps to properties, or via mounted config files and secrets. The image stays environment-independent and contains no credentials.
5. Why should the container start with exec java?
exec makes the JVM PID 1 instead of a child of sh, so SIGTERM from docker stop reaches the JVM. Spring Boot can then run its graceful shutdown rather than being killed after the timeout.
6. How do you make rebuilds faster for Spring Boot images?
Order Dockerfile instructions so dependencies are cached, or use Spring Boot's layered jar support to place dependencies in separate layers from application classes. That way a code change only rebuilds and re-pushes a small layer.
Summary
- Run Spring Boot on a pinned JRE base image as a non-root user.
- Use container-aware JVM flags such as
-XX:MaxRAMPercentageandexecfor graceful shutdown. - Add Actuator health and a
HEALTHCHECK; pass configuration via environment variables. - Use Compose with a database healthcheck for local development.
Continue Learning
Previous: Docker Compose | Next: Docker Multi Stage Build
Was this guide helpful?
CodeWithVenu is free to read, with no paywall. If this guide saved you time or helped you prepare, a small voluntary contribution helps me write more practical tutorials, architecture guides, and interview-prep content.