DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Implementing Asynchronous Communication Between Microservices Using Kafka and Spring Boot
  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • End-to-End Event Streaming With Kafka, Spring Boot and AWS SQS/SNS (Production-Ready Code Guide)
  • Caching Mechanisms Using Spring Boot With Redis or AWS ElastiCache

Trending

  • Engineering Complexity: Implied vs. Induced Complexity
  • Method injection with Spring
  • Scaling Teams, Scaling Systems: Unlocking Developer Productivity With Platform Engineering
  • Jeffrey Microscope for Generating Flame Graphs in Java
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Deployment
  4. Deploying a Spring Boot Microservice on AWS Fargate: Lessons From the Outage That Forced Me to Get It Right

Deploying a Spring Boot Microservice on AWS Fargate: Lessons From the Outage That Forced Me to Get It Right

Deploy a production-ready Spring Boot microservice on AWS Fargate with Docker, ECS, ALB health checks, private subnets, secrets, CI/CD, and autoscaling.

By 
Vishal Rameshchandra Shah user avatar
Vishal Rameshchandra Shah
·
Jul. 31, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
160 Views

Join the DZone community and get the full member experience.

Join For Free

My first attempt to deploy a Spring Boot microservice on AWS Fargate didn’t fail loudly. It failed quietly — in a loop. ECS kept launching tasks, the Application Load Balancer kept marking them unhealthy, and the service never stabilized. The logs looked fine, the container looked fine, but the ALB replaced every task within seconds.

The root cause was painfully simple: Spring Boot needed 45 seconds to start, and my ALB health‑check timeout was 5 seconds. The tasks never had a chance.

That night changed how I build and deploy microservices. It forced me to rethink startup behavior, JVM sizing, networking, task definitions, and the entire CI/CD pipeline. This article is the guide I wish I had before that incident — a practitioner’s walkthrough of deploying a production‑ready Spring Boot service on AWS Fargate, with real artifacts and the details that matter when things go wrong.

The Architecture That Finally Worked

Once the health‑check issue was fixed, the architecture settled into a predictable, cloud‑native flow:

  • Developers push code to GitHub
  • GitHub Actions builds the JAR
  • Docker image is built and pushed to Amazon ECR
  • ECS service runs AWS Fargate tasks
  • Traffic enters through an Application Load Balancer
  • Tasks run in private subnets
  • Configuration comes from Parameter Store and Secrets Manager
  • Logs and metrics flow to CloudWatch

It’s the standard modern microservice pipeline — but the difference between “standard” and “production‑ready” is in the details.

The Spring Boot Service

The microservice itself was simple — a REST API with a few endpoints. The real complexity wasn’t the controller logic; it was everything around it: startup time, health checks, configuration management, and container behavior under load.

A Dockerfile Built for Production

My first Dockerfile looked like the one many tutorials start with: a single‑stage build running as root with no JVM tuning. It worked locally but failed under real load. Fargate tasks with default JVM heap sizing inside a 2GB container are a classic OOM story.

Here’s the hardened version that finally stabilized deployments:

Dockerfile
 
FROM eclipse-temurin:21-jre

# Create non-root user
RUN useradd -u 1001 springuser

WORKDIR /app

# Layer extraction for faster builds
COPY target/*.jar app.jar

# JVM tuning for Fargate
ENV JAVA_OPTS="\
  -XX:MaxRAMPercentage=75 \
  -XX:+UseContainerSupport \
  -XX:+ExitOnOutOfMemoryError \
"

USER springuser

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]


This eliminated the OOMKilled events I saw on 2GB tasks and made startup time predictable. 

Pushing to Amazon ECR With Real Commands

The first time I wrote down my ECR commands, they were placeholders. In production, they need to be exact:

C
 
aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com

docker build -t employee-service:1.0.3 .

docker tag employee-service:1.0.3 \
  <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/employee-service:1.0.3

docker push \
  <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/employee-service:1.0.3


Immutable semantic version tags make rollbacks predictable and prevent “latest‑tag roulette.” 

The ECS Task Definition That Actually Runs in Production

A real Fargate deployment lives or dies by its task definition. Here’s the JSON I use today — including secrets pulled from Parameter Store and Secrets Manager:

JSON
 
{
  "family": "employee-service",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/employeeServiceRole",
  "containerDefinitions": [
    {
      "name": "employee-service",
      "image": "<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/employee-service:1.0.3",
      "portMappings": [
        { "containerPort": 8080, "protocol": "tcp" }
      ],
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:ssm:us-east-1:<ACCOUNT_ID>:parameter/db/password"
        },
        {
          "name": "API_KEY",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:<ACCOUNT_ID>:secret:thirdparty/api"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/employee-service",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}


The ALB Health Check That Stopped the Outage

My outage happened because the ALB was impatient. Here’s the configuration that finally stabilized deployments:

setting value

Path

/actuator/health

Interval

20 seconds

Timeout

10 seconds

Healthy threshold

3

Unhealthy threshold

3

 

Spring Boot startup time + ALB patience = stable deployments. 

Why Fargate Tasks Belong in Private Subnets

Early on, I deployed tasks in public subnets because it felt simpler. It wasn’t. Public IPs meant the containers were directly reachable from the internet — port scans, bot traffic, and noisy logs.

Moving tasks to private subnets solved several problems at once:

Reduced Attack Surface 

No public IPs. No direct inbound traffic. Only the ALB can reach the tasks.

A Single Secure Entry Point

The ALB handles TLS termination, redirects HTTP→HTTPS, performs health checks, and integrates with WAF. Clients never bypass it.

Cleaner Security Groups 

  • ALB SG: inbound 443 from the internet
  • Task SG: inbound only from ALB SG

Nothing else touches the containers.

Compliance Alignment

PCI, SOC 2, HIPAA — all prefer minimizing public exposure.

Controlled Outbound Access

Tasks use a NAT Gateway for outbound calls (updates, third‑party APIs) without exposing themselves.

Better Scalability

ALB target groups automatically track tasks across AZs as ECS scales.

The architecture becomes simple and predictable:

Internet → ALB (public subnets) → Fargate tasks (private subnets)

It’s quieter, safer, and easier to operate.

The GitHub Actions Workflow That Deploys Automatically

Here’s the pipeline that builds, tests, pushes, and deploys the service:

YAML
 
name: Deploy to Fargate

on:
  push:
    branches: ["main"]

jobs:
  build-deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: "21"

      - name: Build JAR
        run: mvn -B clean package

      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and Push Image
        run: |
          docker build -t employee-service:1.0.3 .
          docker tag employee-service:1.0.3 ${{ env.ECR_REGISTRY }}/employee-service:1.0.3
          docker push ${{ env.ECR_REGISTRY }}/employee-service:1.0.3

      - name: Deploy ECS Service
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ecs-task.json
          service: employee-service
          cluster: prod-cluster


Auto Scaling With Real Target Tracking JSON

Target tracking is the simplest and most reliable scaling strategy for Fargate:

JSON
 
{
  "TargetValue": 50.0,
  "PredefinedMetricSpecification": {
    "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
  },
  "ScaleOutCooldown": 30,
  "ScaleInCooldown": 60
}


I use 50% as the target because it balances cost and responsiveness.

What I Learned

Every failure taught me something:

  • ALB timeouts taught me to respect startup time
  • OOMKilled tasks taught me to tune the JVM
  • Public subnets taught me to isolate workloads
  • Manual deployments taught me to automate everything

AWS Fargate really does deliver on its promise — no servers to manage, automatic scaling, and clean integration with ECS — but only after you learn the hard parts.

If you’re deploying Spring Boot on Fargate, I hope you learn those lessons from this article instead of from your own outage.

AWS microservice Spring Boot

Opinions expressed by DZone contributors are their own.

Related

  • Implementing Asynchronous Communication Between Microservices Using Kafka and Spring Boot
  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • End-to-End Event Streaming With Kafka, Spring Boot and AWS SQS/SNS (Production-Ready Code Guide)
  • Caching Mechanisms Using Spring Boot With Redis or AWS ElastiCache

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook