
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Shipping Java applications requires more than just copying a JAR file to a server; you need a repeatable, secure, and observable pipeline. This article on how to deploy Spring Boot to production covers the essential layers from optimized container images to runtime configuration and zero-downtime strategies. Whether you are running on AWS EKS, Azure AKS, or bare metal, these patterns ensure your application survives real-world traffic and passes compliance audits.
How do you optimize Docker images when you deploy Spring Boot to production?
The most common mistake I see teams make is shipping bloated Java containers that waste memory and increase attack surface. When you reduce Docker image size with multi-stage builds, you directly improve startup time and security posture. For Spring Boot 3.x and Java 21+, always separate the build environment from the runtime environment.
Multi-stage Dockerfile for Spring Boot
This pattern uses Eclipse Temurin for both stages to ensure binary compatibility while keeping the final image under 250MB. We also extract layers to leverage Docker caching effectively.
# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootJar --no-daemon
# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
# Extract layers for better caching
RUN java -Djarmode=layertools -jar app.jar extract
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"org.springframework.boot.loader.launch.JarLauncher"] - Alpine Base: Reduces OS-level vulnerabilities and image size by ~60% compared to Debian.
- Layer Extraction: Spring Boot’s layer tools separate dependencies from application code, meaning code-only changes don’t invalidate the dependency cache.
- No Root User: Always add a non-root user in production containers to limit blast radius if compromised.
What JVM settings are critical when you deploy Spring Boot to production?
Java inside a container behaves differently than on bare metal. Without explicit tuning, the JVM may misinterpret cgroup limits, leading to OOM kills even when heap usage appears low. Modern JVMs (Java 17+) handle container awareness automatically, but you must still configure memory ceilings explicitly.
Recommended JVM Flags for Containers
Avoid hardcoded -Xmx values. If your pod limit changes but the flag doesn't, you risk immediate crashes. Use percentage-based allocation instead.
JAVA_OPTS="\
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-XX:+ExitOnOutOfMemoryError \
-Djava.security.egd=file:/dev/./urandom" The ExitOnOutOfMemoryError flag is non-negotiable in Kubernetes. Without it, a JVM might stay alive in a broken state after an OOM event, passing liveness checks while failing every request. Let the orchestrator restart the pod cleanly. For deeper performance analysis, refer to the four golden signals of monitoring to track saturation and errors effectively.
How do you manage configuration and secrets securely?
Never bake credentials into your Docker image or commit application-prod.yml to Git. When you deploy Spring Boot to production, configuration should be injected at runtime. This separates the artifact from the environment, allowing the same verified image to move through dev, staging, and prod.
| Method | Best For | Security Level | Complexity |
|---|---|---|---|
| Environment Variables | Simple configs, feature flags | Moderate | Low |
| Kubernetes ConfigMaps | Non-sensitive app properties | Moderate | Medium |
| Kubernetes Secrets | DB passwords, API keys | High (with encryption) | Medium |
| HashiCorp Vault / AWS Secrets Manager | Dynamic secrets, rotation, audit | Highest | High |
Spring Cloud Kubernetes Integration
For Kubernetes-native deployments, use property sources that map directly to ConfigMaps and Secrets without extra sidecars.
# application.yml
spring:
config:
import:
- optional:kubernetes:
cloud:
kubernetes:
config:
enabled: true
sources:
- name: my-app-config
secrets:
enabled: true
sources:
- name: my-app-secrets This approach keeps your Spring Boot application decoupled from infrastructure specifics while maintaining proper Kubernetes secrets hygiene. Always enable RBAC restrictions so pods can only read their own namespace's secrets.
How do you implement health checks and graceful shutdown?
Load balancers and orchestrators need accurate signals to route traffic safely. Spring Boot Actuator provides standardized endpoints, but default configurations often cause issues during rolling deployments. You must distinguish between "alive" and "ready to serve traffic."
Actuator Configuration for Production
management:
endpoint:
health:
probes:
enabled: true
show-details: never
endpoints:
web:
exposure:
include: health,info,prometheus
server:
port: 8081 Separating the management port (8081) from the application port (8080) prevents external users from accessing internal metrics. The probes.enabled=true setting exposes /actuator/health/liveness and /actuator/health/readiness specifically for Kubernetes.
Graceful Shutdown Settings
Without graceful shutdown, in-flight requests fail during deployments. Enable this to allow active connections to complete before the JVM terminates.
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s Pair this with a preStop hook in Kubernetes that sleeps for 5–10 seconds. This accounts for the delay between the pod being marked terminating and the load balancer actually stopping traffic. Skipping this step causes intermittent 502 errors during every deployment.
How do you ensure observability after deployment?
You cannot fix what you cannot see. Production Spring Boot applications require structured logging, metrics, and distributed tracing from day one. Relying solely on console logs makes debugging impossible at scale.
OpenTelemetry Integration
Modern Spring Boot versions support OpenTelemetry natively. This gives you vendor-neutral instrumentation that works with Jaeger, Tempo, or Datadog.
# build.gradle.kts
implementation("io.micrometer:micrometer-tracing-bridge-otel")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
# application.yml
management:
otlp:
metrics:
export:
url: http://otel-collector:4318/v1/metrics
tracing:
export:
url: http://otel-collector:4318/v1/traces
tracing:
sampling:
probability: 0.1 Start with 10% sampling in production to control costs, then adjust based on traffic volume. Combine this with structured logging best practices to correlate log entries with trace IDs automatically. Every log line should include trace_id and span_id fields for seamless debugging across microservices.
Deploy Spring Boot to Production With Confidence
Successfully running Spring Boot in production demands attention to container optimization, memory management, secure configuration, and observability. By following these patterns—multi-stage builds, percentage-based JVM tuning, externalized secrets, proper health probes, and native OpenTelemetry—you build systems that are resilient, auditable, and maintainable. Start with the Dockerfile and JVM flags today; they deliver immediate stability gains. If your team needs help designing a compliant, production-grade Java platform, reach out to discuss your architecture.