
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
To dockerize a Spring Boot application effectively, you must move beyond basic tutorials and adopt multi-stage builds that separate compilation from runtime. This approach produces minimal, secure container images suitable for Kubernetes and compliance audits like SOC 2 or ISO 27001. In this guide, I will walk you through the exact production-grade configuration I use for enterprise Java workloads, ensuring your containers are lightweight, reproducible, and secure by default.
How do you dockerize a Spring Boot application with multi-stage builds?
The most common mistake when teams first containerize applications is shipping the entire build toolchain into production. For Java, this means including Maven or Gradle plus a full JDK in your final image, resulting in 600MB+ artifacts filled with unnecessary attack surface. Multi-stage builds solve this by isolating the build environment from the runtime environment.
The production Dockerfile
This Dockerfile targets Java 21 LTS and uses Eclipse Temurin, which provides consistent OpenJDK builds across platforms. Note the explicit version pinning — never use latest tags in production as they break reproducibility and make incident investigation nearly impossible.
# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn clean package -DskipTests -B
# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"] The --mount=type=cache directive leverages BuildKit to persist Maven dependencies between builds. Without this, every CI run re-downloads hundreds of megabytes of dependencies, adding 2–5 minutes to your pipeline. This single optimization often yields the biggest time savings for teams learning to reduce Docker image size with multi-stage builds.
What are the best practices for securing a Spring Boot Docker container?
Security is not optional when you dockerize a Spring Boot application, especially if you operate under compliance frameworks. Running containers as root is the most frequent vulnerability I find during infrastructure audits. A compromised process running as root can escape the container namespace far more easily than one running as an unprivileged user.
- Non-root execution: Always create a dedicated user and group. The Alpine-based JRE image uses
adduser/addgroup; Debian-based images useuseradd/groupadd. - Read-only filesystem: Set
readOnlyRootFilesystem: truein Kubernetes or--read-onlyin Docker. Your application should write only to explicitly mounted tmpfs volumes for logs or temp files. - No shell access: Use the
execform of ENTRYPOINT (JSON array syntax) rather than shell form. This prevents signal handling issues and removes the need for a shell binary in the final image. - Dependency scanning: Integrate Trivy or Grype into your CI pipeline to scan both the base image and your application dependencies before pushing to any registry.
- Minimal base images: Prefer Alpine or distroless variants over full Debian/Ubuntu bases. Fewer packages mean fewer CVEs to track and patch.
For secrets management, never bake credentials into the image. Inject them at runtime via environment variables, mounted secret files, or external vaults. If you are deploying to Kubernetes, review Kubernetes secrets management done right to avoid common pitfalls like unencrypted etcd storage.
How do you configure JVM memory settings inside Docker?
JVM memory configuration inside containers has evolved significantly. Older guides recommend -XX:+UseCGroupMemoryLimitForHeap, but this flag was removed in Java 12+ because container awareness is now enabled by default. On Java 21, the JVM automatically detects cgroup memory limits and sets heap accordingly.
However, automatic detection does not mean you should skip explicit configuration. Use -XX:MaxRAMPercentage instead of fixed -Xmx values. This allows the same image to scale across different instance sizes without rebuilding:
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] Setting MaxRAMPercentage to 75% leaves headroom for metaspace, thread stacks, direct buffers, and native memory. Setting it to 90% or higher risks OOM kills because the JVM's non-heap memory consumption is unpredictable under load. For high-throughput services, pair this with G1GC (default on Java 21) or ZGC for sub-millisecond pauses. Monitor actual usage with Prometheus metrics before tuning further — see Prometheus metrics monitoring fundamentals for instrumentation patterns.
How do you implement health checks and graceful shutdown in Docker?
Docker and orchestrators rely on health probes to route traffic and manage lifecycle. Spring Boot Actuator exposes /actuator/health and /actuator/health/liveness endpoints specifically for this purpose. Configure your Dockerfile or Kubernetes deployment to use these rather than generic TCP checks.
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health/liveness || exit 1 The --start-period is critical for Spring Boot applications. Java startup can take 20–40 seconds depending on bean initialization and classloading. Without a start period, Docker marks the container unhealthy during warmup and may restart it in a crash loop. Set this value based on your actual cold-start measurements, not guesses.
Graceful shutdown ensures in-flight requests complete before the container terminates. Enable it in application.properties:
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s Pair this with Docker's STOPSIGNAL SIGTERM (default) and ensure your stop timeout exceeds the shutdown phase timeout. In Kubernetes, set terminationGracePeriodSeconds to at least 35 seconds. This prevents dropped requests during rolling deployments and blue-green transitions.
How does a production Spring Boot Docker setup compare to development configurations?
Development convenience often conflicts with production requirements. Understanding these trade-offs prevents costly rework when moving from local Docker Compose to managed Kubernetes clusters.
| Aspect | Development Setup | Production Setup |
|---|---|---|
| Base Image | Full JDK (debug tools included) | JRE Alpine or distroless |
| User | Root (convenience) | Dedicated non-root user |
| Build Cache | Local volume mounts | BuildKit cache mounts in CI |
| Secrets | .env files, docker-compose | Vault, K8s Secrets, SSM Parameter Store |
| Logging | Console output, stdout | Structured JSON, external aggregation |
| Image Size | 600–900 MB acceptable | Target <200 MB |
| Health Checks | Optional or basic | Liveness + readiness probes mandatory |
In development, bind-mounting source code enables hot reload and fast iteration. In production, the image must be immutable — no mounts, no writable layers except designated tmpfs paths. This immutability is foundational to reliable rollbacks and audit trails. If you need to debug production issues, use ephemeral debug containers (kubectl debug) rather than baking debug tools into your release artifact.
Next steps for your Spring Boot containerization journey
When you dockerize a Spring Boot application correctly, you gain reproducible deployments, faster scaling, and a smaller attack surface. Start with the multi-stage Dockerfile provided above, enforce non-root execution from day one, and validate your memory settings under realistic load before going live. Treat your Dockerfile as production code — version it, review it, and scan it in every pipeline run.
If your team needs help establishing secure container workflows, implementing CI/CD pipelines for Java microservices, or preparing infrastructure for compliance audits, reach out to discuss your specific requirements. I help organizations build production-grade container platforms that pass security reviews and scale reliably under real-world traffic.