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-jre contains 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.0 sizes the heap relative to the container's memory limit. Modern JVMs detect cgroup limits by default, so -Xmx hard-coding is not needed.
  • -XX:+ExitOnOutOfMemoryError makes the JVM exit on an OutOfMemoryError so Docker or the orchestrator can restart a clean process.
  • exec java ... replaces the shell with the JVM so docker stop signals 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 USER instruction.
  • Using the shell form ENTRYPOINT java -jar .... The app may not receive SIGTERM, causing a 10-second kill on docker stop.
  • Ignoring memory limits. An unconstrained heap inside a limited container leads to OOM kills. Use MaxRAMPercentage and set a container memory limit.
  • Baking credentials into application.properties in 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 HEALTHCHECK or 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:MaxRAMPercentage and exec for 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.