Deploying Spring Boot on AWS: A Production-Ready Guide
Production Spring Boot on AWS: layered Docker images, container-aware JVM flags, ECS vs EKS, graceful shutdown, autoscaling and keeping the bill sane.
Introduction
Deploying a Spring Boot application to AWS involves more than just uploading a JAR to an EC2 instance. Production-ready deployment requires careful consideration of containerization, orchestration, monitoring, security, and cost optimization.
This guide shares the practices we've refined over years of running enterprise Spring Boot applications on AWS, including the Olympus Mobility platform.
Containerization with Docker
Start with a multi-stage Dockerfile that produces a minimal, secure image. Use Eclipse Temurin as your base image and take advantage of Spring Boot's layered JAR support for faster builds.
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
COPY . .
RUN ./mvnw -q -DskipTests package && \
java -Djarmode=layertools -jar target/app.jar extract
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
USER app
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
The layer ordering matters: dependencies change rarely and application classes change every build, so putting them in separate layers means a code-only change re-uploads kilobytes instead of the whole artifact.
Keep your images small - our production images are typically under 200MB. Run as a non-root user, as above; it costs nothing and it is the first thing a security review will ask about.
JVM Settings That Actually Matter in Containers
The single most common production misconfiguration we see is a JVM that does not know it is in a container.
JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=75 -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError
Use MaxRAMPercentage rather than a fixed -Xmx, so the heap follows the task size instead of being re-tuned every time you resize. Leave headroom - the JVM needs memory for metaspace, thread stacks and direct buffers, and a heap sized at 100% of the container limit gets the process OOM-killed by the runtime rather than throwing a diagnosable Java error. ExitOnOutOfMemoryError matters too: a container that has died is replaced, while one limping along after an OOM just fails health checks intermittently.
ECS vs EKS: Choosing Orchestration
For most Spring Boot applications, Amazon ECS with Fargate is the right choice. It's simpler to manage than EKS, and Fargate eliminates the need to manage EC2 instances entirely.
We use EKS only when we need advanced scheduling, custom operators, or have teams with Kubernetes expertise.
The honest framing is that EKS is worth it when you already have Kubernetes skills on the team. If you don't, ECS gets you to production sooner and the cluster stops being something you have to maintain.
Health Checks and Graceful Shutdown
Spring Boot Actuator gives you two distinct probes, and load balancers need both:
management.endpoint.health.probes.enabled=true
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
Point your ALB target group at /actuator/health/readiness, not at /actuator/health. Liveness answers "is this process broken"; readiness answers "should this instance receive traffic". Wiring the load balancer to liveness means instances take traffic before their connection pool is warm.
Set the ECS task stopTimeout above your shutdown phase timeout. If ECS sends SIGKILL after 30s while Spring is still draining for 30s, you drop in-flight requests on every deploy.
Auto-Scaling Strategy
Configure target tracking scaling policies based on CPU and memory utilization. For Spring Boot applications, we typically set:
- CPU target: 60-70%
- Memory target: 70-80%
- Minimum tasks: 2 (for high availability)
- Scale-in cooldown: 300 seconds
Note that JVM memory utilization is a poor scaling signal on its own - a healthy heap sits near its ceiling by design, because the collector has no reason to run earlier. CPU is usually the more honest trigger, with request count per target as a good second.
Database Considerations
Use Amazon RDS for PostgreSQL with Multi-AZ deployment for production. Key settings:
- Enable Performance Insights for query analysis
- Configure connection pooling with HikariCP
- Set up automated backups with appropriate retention
- Use read replicas for read-heavy workloads
On pool sizing, resist the temptation to make it large. Total connections across all tasks must stay under the instance limit, so a pool of 10 across 20 autoscaled tasks is already 200 connections. Small pools with a short connectionTimeout fail fast and recover; large pools mostly move the queue from your application into the database.
Secrets and Configuration
Keep secrets out of task definitions. Reference AWS Secrets Manager or SSM Parameter Store by ARN so values are injected at task start and never appear in your CloudFormation or Terraform state, in the console, or in docker inspect output. Rotating a password then becomes a Secrets Manager operation and a task restart, not a redeploy.
Monitoring and Observability
A production deployment needs three pillars of observability:
- Metrics: Use CloudWatch with custom metrics from Spring Boot Actuator
- Logs: Centralize with CloudWatch Logs, use structured JSON logging
- Traces: Implement distributed tracing with AWS X-Ray or OpenTelemetry
Micrometer is already in your Spring Boot application, so exporting to CloudWatch is configuration rather than code. Log in JSON from the start - retrofitting structured logging across a live service is far more tedious than enabling it on day one.
Deployment Safety
Use CodeDeploy blue/green with ECS so a bad release shifts traffic back automatically instead of relying on someone noticing. Pair it with a CloudWatch alarm on 5xx rate as the rollback trigger, and keep database migrations backwards-compatible for one release - during a blue/green cut-over both versions are briefly live against the same schema.
Cost Optimization
AWS costs can spiral quickly. Our top tips:
- Use Savings Plans for predictable workloads
- Right-size your Fargate tasks
- Implement proper JVM memory settings
- Use S3 Intelligent-Tiering for storage
- Set up billing alarms and budgets
The two biggest wins in practice are almost always right-sizing over-provisioned tasks and setting a log retention policy - CloudWatch Logs kept forever quietly becomes a real line item.
Conclusion
A well-architected AWS deployment of Spring Boot applications provides reliability, scalability, and cost efficiency. Start simple, measure everything, and optimize based on real data.
If you would rather not own this yourself, our support and maintenance team runs production Spring Boot workloads on AWS as a service.
