When you try to containerize a Spring Boot app, you often hit walls like “I don’t know how to write a Dockerfile” or “I can’t get PostgreSQL to connect via Docker Compose.”

This article walks you through the practical essentials: writing a working Dockerfile, optimizing your image with multi-stage builds, and integrating a database with Docker Compose.


Writing a Minimal Dockerfile

Let’s get a working Dockerfile up and running as quickly as possible. Since a Spring Boot app just runs a JAR file, the Dockerfile is quite simple.

We’ll use eclipse-temurin:21-jre-alpine as the base image. Since build tools aren’t needed at runtime, choosing JRE over JDK keeps the image size down.

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

You can build and run it with the following commands:

./mvnw package -DskipTests
docker build -t myapp:latest .
docker run -p 8080:8080 myapp:latest

Reducing Image Size with Multi-Stage Builds

The Dockerfile above requires you to run mvn package beforehand. With multi-stage builds, everything from build to execution is handled inside the Dockerfile itself.

Using an image that includes Maven and JDK as-is would result in a 500MB+ production image, but separating the build and runtime stages brings it down to around 80MB.

# Build stage
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace

# Copy pom.xml first to resolve dependencies (cache optimization)
COPY pom.xml .
RUN mvn dependency:go-offline -B

COPY src ./src
RUN mvn package -DskipTests -B

# Runtime stage
FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
COPY --from=build /workspace/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

The key here is COPY --from=build. Only the JAR from the build stage is copied into the runtime image — Maven and JDK are not included in the final image.

Also, by copying pom.xml first and resolving dependencies before copying source code, dependency downloads can be skipped on subsequent builds when only your code changes.


Further Speedup with Layered JAR

Spring Boot 3.x supports a feature called Layered JAR. By splitting the JAR contents into three layers — dependencies, spring-boot-loader, and application — and copying them separately, Docker can reuse the dependency layers from cache even when only the application code changes.

FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted

FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
COPY --from=build /workspace/target/extracted/dependencies/ ./
COPY --from=build /workspace/target/extracted/spring-boot-loader/ ./
COPY --from=build /workspace/target/extracted/snapshot-dependencies/ ./
COPY --from=build /workspace/target/extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Running Spring Boot + PostgreSQL Together with Docker Compose

Starting your app and database separately during local development is tedious. With Docker Compose, you can bring up multiple services with a single command.

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

  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/myapp_db
      SPRING_DATASOURCE_USERNAME: myapp_user
      SPRING_DATASOURCE_PASSWORD: myapp_password
    depends_on:
      db:
        condition: service_healthy

volumes:
  postgres_data:

Using depends_on alone starts the next service as soon as the PostgreSQL container is up — not necessarily ready. Combining it with healthcheck and condition: service_healthy ensures Spring Boot starts only after PostgreSQL is ready to accept connections.

Use the following commands to start, tail logs, and stop:

docker compose up -d
docker compose logs -f app
docker compose down

Externalizing Configuration with Environment Variables

In containerized environments, best practice is to inject settings like database credentials via environment variables rather than hardcoding them.

spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/myapp_db}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:myapp_user}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:myapp_password}

The ${VARIABLE_NAME:default_value} syntax lets you specify a fallback value when the environment variable is not set.

Keep sensitive values in a .env file and make sure not to commit it to Git. Docker Compose automatically loads a .env file in the same directory.

For combining this with Spring profiles, see also How to Safely Switch Environment-Specific Configuration Using Spring Boot Profiles.


Security Checklist Before Moving to Production

Once everything works locally, go through these security points before moving to production.

Running as a non-root user is strongly recommended. By default, processes inside a Docker container run as root. Creating a dedicated user and switching to it reduces risk.

FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup --from=build /workspace/target/*.jar app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

For production, also consider using Docker Secrets or a secrets management service like AWS Secrets Manager instead of plain environment variables.

Make vulnerability scanning of your images a regular habit:

docker scout cves myapp:latest

Summary

This article covered how to containerize a Spring Boot application with Docker.

Start with a minimal Dockerfile to get things running, reduce image size with multi-stage builds, and then speed up builds further with Layered JAR — that’s the progression to keep in mind.

Docker Compose makes local development significantly easier, and when moving to production, don’t forget security checks like running as a non-root user and performing vulnerability scans.

Containerization is also the entry point to cloud deployment and CI/CD pipelines. Start by getting a Docker Compose environment running locally, then level up to building images in CI.