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.