Spring Boot: Dockerizing a Spring Boot App

Packaging a Spring Boot application as a Docker image makes it portable and easy to deploy anywhere. Spring Boot supports multiple approaches: a simple Dockerfile, a multi-stage Dockerfile for smaller images, docker-compose for local development with dependencies, and the Buildpacks integration that builds an OCI image without a Dockerfile.

Contents

Building a JAR with Maven/Gradle

Before building a Docker image you need a runnable JAR. Spring Boot's Maven and Gradle plugins produce a fat JAR (also called an executable JAR or ΓΌber-JAR) that bundles the application classes, all dependencies, and the embedded Tomcat server into a single file. It can be run anywhere with java -jar app.jar: no separate server installation needed.

Shell
# Maven β€” produces a fat JAR in target/
./mvnw clean package -DskipTests

# Gradle β€” produces in build/libs/
./gradlew bootJar

Writing a Dockerfile (FROM eclipse-temurin, COPY, ENTRYPOINT)

A Dockerfile is a text recipe that tells Docker how to build your image layer by layer. For a Spring Boot app the essentials are: pick a base JRE image, copy the fat JAR, expose the HTTP port, and set the startup command. The example below also creates a non-root system user: a security best practice that prevents the JVM process from running as root inside the container.

Dockerfile
# Simple single-stage Dockerfile
FROM eclipse-temurin:21-jre-alpine

# Non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

WORKDIR /app

# Copy the fat JAR built by Maven/Gradle
COPY target/*.jar app.jar

# Expose the port Spring Boot listens on
EXPOSE 8080

# JVM tuning for containers
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75"

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Use eclipse-temurin (the official Adoptium JDK/JRE image) over openjdk which is no longer actively maintained. Prefer -jre or -alpine variants for smaller images.

Building and Running the Image

Once the Dockerfile is in place, use docker build to create the image and docker run to start a container from it. The -p flag maps a host port to the container port, and -e passes environment variables that Spring Boot reads as property overrides, letting you switch profiles or change the database URL without rebuilding the image.

Shell
# Build the image β€” tag it with the app name and version
docker build -t my-spring-app:1.0.0 .

# Run the container
docker run -p 8080:8080 my-spring-app:1.0.0

# Pass environment variables (e.g., override Spring profile and DB URL)
docker run -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb \
  my-spring-app:1.0.0

# Run in background
docker run -d --name my-app -p 8080:8080 my-spring-app:1.0.0

# View logs
docker logs -f my-app

Multi-Stage Build for Smaller Images

A multi-stage build compiles and packages the JAR in one stage, then copies only the result into a minimal runtime image, removing the JDK, Maven, source code, and build artifacts from the final image.

Dockerfile
# Stage 1 β€” build
FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /build
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline -q

COPY src/ src/
RUN ./mvnw clean package -DskipTests -q

# Stage 2 β€” runtime (JRE only β€” no compiler, no Maven)
FROM eclipse-temurin:21-jre-alpine

RUN addgroup -S app && adduser -S app -G app
USER app

WORKDIR /app

# Use Spring Boot's layered JAR extraction for better layer caching
COPY --from=builder /build/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75",\
            "org.springframework.boot.loader.launch.JarLauncher"]

docker-compose with App + Database

Running the app container in isolation is fine for smoke-testing, but real applications depend on a database, cache, or message broker. docker compose lets you define all of those services in a single docker-compose.yml file and start them together with one command. The depends_on + healthcheck combination ensures the database is fully ready before Spring Boot attempts to connect, avoiding the common race condition on cold starts.

YAML
# docker-compose.yml
services:

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydb
      SPRING_DATASOURCE_USERNAME: myuser
      SPRING_DATASOURCE_PASSWORD: mypassword
      SPRING_JPA_HIBERNATE_DDL_AUTO: update
      SPRING_PROFILES_ACTIVE: prod
    depends_on:
      db:
        condition: service_healthy

volumes:
  postgres_data:

The most common docker compose commands you'll use day-to-day:

Shell
# Start all services
docker compose up --build

# Start in background
docker compose up -d --build

# Stop all services
docker compose down

# Stop and remove volumes (wipes database)
docker compose down -v

Spring Boot Buildpacks (./mvnw spring-boot:build-image)

Spring Boot 2.3+ integrates with Cloud Native Buildpacks to build a Docker image directly from your source code or JAR: no Dockerfile required. The resulting image follows best practices automatically (layering, non-root user, etc.).

Shell
# Build an OCI image using Buildpacks (Docker must be running)
./mvnw spring-boot:build-image -DskipTests

# Run the resulting image
docker run -p 8080:8080 my-spring-app:0.0.1-SNAPSHOT
XML
<!-- Customise image name and JVM settings in pom.xml -->
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <name>myregistry/my-spring-app:${project.version}</name>
            <env>
                <BP_JVM_VERSION>21</BP_JVM_VERSION>
            </env>
        </image>
    </configuration>
</plugin>
Buildpacks produce images with excellent layer caching and security defaults but require Docker to be running locally. For CI/CD pipelines without Docker, consider using the multi-stage Dockerfile approach instead.