Dockerize a Spring Boot Application

Khimananda Oli 7 min read Programming and Languages
Dockerize a Spring Boot Application

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.

Source Codepom.xml + src/Build Stageeclipse-temurin:21-jdkmvn package -DskipTestsapp.jar (35 MB)Runtime Stageeclipse-temurin:21-jre-alpineNon-root userFinal: ~180 MB
Multi-stage build architecture: compile in heavy JDK, deploy only the artifact on slim JRE

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 use useradd/groupadd.
  • Read-only filesystem: Set readOnlyRootFilesystem: true in Kubernetes or --read-only in Docker. Your application should write only to explicitly mounted tmpfs volumes for logs or temp files.
  • No shell access: Use the exec form 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.

Container Memory Limit: 1024 MiJava Heap (~75% = 768 Mi)-XX:MaxRAMPercentage=75.0Metaspace + Code Cache~128 Mi typicalThread Stacks + GC Overhead~128 Mi bufferOOM Kill Zone — Never exceed container limit
JVM memory layout inside a 1 GiB container: heap respects cgroup limits automatically on Java 21

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.

AspectDevelopment SetupProduction Setup
Base ImageFull JDK (debug tools included)JRE Alpine or distroless
UserRoot (convenience)Dedicated non-root user
Build CacheLocal volume mountsBuildKit cache mounts in CI
Secrets.env files, docker-composeVault, K8s Secrets, SSM Parameter Store
LoggingConsole output, stdoutStructured JSON, external aggregation
Image Size600–900 MB acceptableTarget <200 MB
Health ChecksOptional or basicLiveness + 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.

DevelopmentFull JDK + Debug Tools (850 MB)Root User + Writable FSBind-Mounted Source Code.env Secrets + Console LogsProductionSlim JRE Alpine (180 MB)Non-Root + Read-Only FSImmutable Artifact OnlyVault Secrets + Structured JSON LogsCI/CD Pipeline
Development prioritizes convenience; production prioritizes security, size, and immutability

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.

Frequently Asked Questions

Eclipse Temurin 21 JRE Alpine is currently the standard for production. It provides a minimal footprint under 80MB while maintaining full compatibility with modern Spring Boot 3.x releases and GraalVM native binaries.

Enable Spring AOT processing during the Maven build to generate optimized bytecode. Alternatively, compile to a GraalVM native image using the native-maven-plugin, reducing cold starts from seconds to milliseconds in containerized environments.

Configure the actuator health endpoint on the management port and map it correctly in your Dockerfile HEALTHCHECK instruction to ensure Kubernetes probes receive valid HTTP 200 responses during initialization.

Never bake credentials into layers. Inject them at runtime via environment variables or mount external secret stores like HashiCorp Vault, ensuring sensitive data never persists in the immutable container image filesystem.

Use -XX:MaxRAMPercentage=75.0 instead of fixed heap sizes. This allows the JVM to dynamically respect container cgroup memory limits without triggering OOM kills or wasting allocated resources.

Yes, Spring Boot 3.x supports layered jars natively. Extract layers in your Dockerfile to separate dependencies from application code, drastically reducing rebuild times when only business logic changes between deployments.

Expose port 5005 and add -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 to JAVA_OPTS. Connect your IDE debugger to localhost:5005 after mapping the port in docker-compose or Kubernetes service definitions.

Absolutely required for production security. Compile with JDK in the first stage, then copy only the executable jar to a JRE-only final stage, eliminating build tools and source code from the deployable artifact.

Output JSON structured logs to stdout using Logback's json-encoder. Avoid file appenders entirely since container orchestrators collect standard output streams directly into centralized observability platforms like OpenTelemetry collectors.

Usually missing container-awareness flags or incorrect MaxRAMPercentage values. Verify cgroup v2 detection works by checking /sys/fs/cgroup/memory.max inside the container and adjust percentage accordingly.

Distroless images enhance security by removing shells and package managers. They work well for Spring Boot but complicate debugging; reserve them for hardened production deployments where attack surface minimization is critical.

Set TZ environment variable and install tzdata package in Alpine, or use -Duser.timezone=UTC JVM flag. Mismatched host and container timezones cause subtle scheduling and timestamp serialization bugs.

Use 8080 as default unless organizational standards dictate otherwise. Always declare EXPOSE in Dockerfile for documentation, though actual port mapping happens at runtime through orchestration tooling configuration.

Execute Flyway or Liquibase migrations during application startup rather than as separate init containers. This ensures schema version matches deployed code atomically, preventing mismatched state during rolling updates.

Containers add negligible overhead compared to VMs. Cost savings come from higher density packing and faster scaling. Monitor actual resource utilization with Prometheus to right-size requests and avoid over-provisioning.