Docker for Spring Boot Developers: Complete Guide

Containerizing a Spring Boot application looks simple at first, but doing it right — small image, fast startup, Kubernetes-ready — requires several technical decisions that this article covers in a practical way.

Why Docker for Java Projects

Java has a reputation for producing large, slow Docker images. The reason is usually the same mistake: copying the full JAR onto a 600 MB JDK image with nothing else.

With the techniques you’ll see here, you can go from 650 MB → under 100 MB and reduce startup time to under 3 seconds in production.

Dockerfile with Multi-Stage Build

The key is separating the build phase (you need the full JDK) from the runtime phase (you only need the JRE).

# ── Stage 1: compilation ─────────────────────────────────────
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app

# Copy dependency files first (leverages Docker layer cache)
COPY gradle/ gradle/
COPY gradlew build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon

# Now copy source code and build
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test

# ── Stage 2: production image ─────────────────────────────────
FROM eclipse-temurin:21-jre-alpine AS production

# Unprivileged user — never use root in production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

WORKDIR /app

# Copy only the final JAR
COPY --from=build /app/build/libs/*.jar app.jar

# Health check for Docker Swarm / Kubernetes
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1

EXPOSE 8080

# JVM flags optimized for containers
ENTRYPOINT ["java", \
  "-XX:+UseContainerSupport", \
  "-XX:MaxRAMPercentage=75.0", \
  "-XX:+ExitOnOutOfMemoryError", \
  "-Djava.security.egd=file:/dev/./urandom", \
  "-jar", "app.jar"]

The .dockerignore Trick

Without this file, Docker copies build/, .git and node_modules to the build context, slowing down every build:

# .dockerignore
.git
.gradle
build/
out/
*.md
.env
.env.*

Docker Compose for Local Development

Locally you need Spring Boot + PostgreSQL + Redis without installing anything on your machine:

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: dev
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mydb
      SPRING_DATASOURCE_USERNAME: dev
      SPRING_DATASOURCE_PASSWORD: dev
      SPRING_DATA_REDIS_HOST: redis
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

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

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

volumes:
  postgres_data:

With this, docker compose up brings up the entire stack in seconds.

Spring Boot Profiles per Environment

Use environment variables to distinguish configuration between environments without changing code:

# application.yml
spring:
  datasource:
    url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/mydb}
    username: ${SPRING_DATASOURCE_USERNAME:dev}
    password: ${SPRING_DATASOURCE_PASSWORD:dev}
    hikari:
      maximum-pool-size: ${DB_POOL_SIZE:10}
      minimum-idle: 2

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: when-authorized

Faster Builds with Layered JARs

Spring Boot 2.3+ supports layered JARs, which speeds up image rebuilds when you only change your code (without touching dependencies):

FROM eclipse-temurin:21-jre-alpine AS production
WORKDIR /app

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

# Extract JAR layers
RUN java -Djarmode=layertools -jar app.jar extract

# Order: layers that change less frequently go first (better cache)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=production /app/dependencies/ ./
COPY --from=production /app/spring-boot-loader/ ./
COPY --from=production /app/snapshot-dependencies/ ./
COPY --from=production /app/application/ ./

ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

With this strategy, if you only change business code, Docker only rebuilds the last layer (application/) in seconds instead of minutes.

Kubernetes-Ready Configuration

When your container runs in Kubernetes, add these settings to application.yml:

management:
  endpoint:
    health:
      probes:
        enabled: true  # Enables /actuator/health/liveness and /actuator/health/readiness
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

And in your Kubernetes Deployment:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 20
  periodSeconds: 5

Conclusion

A well-built Docker image for Spring Boot combines:

  • Multi-stage build for small images
  • Layered JARs for fast CI/CD builds
  • Correct JVM flags for containerized environments
  • Health checks for orchestrators like Kubernetes
  • Unprivileged user for security

With these practices, your Spring Boot application will be ready for any modern production environment.