From a naive 900MB image to a lean 180MB multi-stage build — Dockerfile best practices, environment config, health checks, and Docker Compose for local dev.
Most Spring Boot Docker tutorials produce a 900MB image that takes 3 minutes to start. Here's how I build production images that are under 200MB and start in under 10 seconds.
1# Stage 1: Build
2FROM eclipse-temurin:17-jdk-alpine AS builder
3WORKDIR /app
4COPY mvnw pom.xml ./
5COPY .mvn .mvn
6RUN ./mvnw dependency:go-offline -B
7COPY src ./src
8RUN ./mvnw package -DskipTests -B
9
10# Stage 2: Extract layers for better caching
11FROM builder AS extractor
12RUN java -Djarmode=layertools -jar target/*.jar extract
13
14# Stage 3: Runtime (JRE only, not JDK)
15FROM eclipse-temurin:17-jre-alpine
16WORKDIR /app
17COPY --from=extractor /app/dependencies/ ./
18COPY --from=extractor /app/spring-boot-loader/ ./
19COPY --from=extractor /app/snapshot-dependencies/ ./
20COPY --from=extractor /app/application/ ./
21EXPOSE 8080
22ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]Layer tools split the JAR into dependencies, Spring Boot loader, and application code. Since dependencies change rarely, Docker reuses their layer on rebuilds — cutting CI build time from 4 minutes to 45 seconds.
1management:
2 endpoint:
3 health:
4 show-details: always
5 endpoints:
6 web:
7 exposure:
8 include: health,info,metrics
9
10server:
11 shutdown: graceful # wait for in-flight requests before stopping
12
13spring:
14 lifecycle:
15 timeout-per-shutdown-phase: 30s1services:
2 app:
3 build: .
4 ports: ["8080:8080"]
5 environment:
6 SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/myapp
7 SPRING_DATASOURCE_USERNAME: postgres
8 SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
9 depends_on:
10 db:
11 condition: service_healthy
12
13 db:
14 image: postgres:15-alpine
15 environment:
16 POSTGRES_DB: myapp
17 POSTGRES_PASSWORD: ${DB_PASSWORD}
18 healthcheck:
19 test: ["CMD", "pg_isready", "-U", "postgres"]
20 interval: 5s
21 retries: 51target/
2*.iml
3.idea/
4.git/
5*.log
6.env
7.env.*
8node_modules/
9*.mdWithout a .dockerignore, Docker COPY sends the entire project including your target/ folder (hundreds of MB of compiled classes) to the Docker daemon as the build context. This alone can add 30+ seconds to every build.
1# Tell the JVM it's running in a container (Java 11+ does this automatically)
2# But still set sensible limits
3ENTRYPOINT ["java",
4 "-XX:+UseContainerSupport",
5 "-XX:MaxRAMPercentage=75.0",
6 "-XX:+ExitOnOutOfMemoryError",
7 "-Djava.security.egd=file:/dev/./urandom",
8 "org.springframework.boot.loader.JarLauncher"]Don't set -Xmx manually in containers. Use -XX:MaxRAMPercentage=75.0 instead — it reads the container's memory limit (from Docker/Kubernetes cgroups) and sets the heap to 75% of it. Hardcoded -Xmx values break when the container limit changes.
More in Backend Engineering