DEV Community

Sohana Akbar
Sohana Akbar

Posted on

The 2-Week Journey to Next.js Zero-Downtime on ECS Fargate

Zero-downtime isn't just a buzzword we throw around in standups. It's the result of an Application Load Balancer and a graceful shutdown config that took me two weeks to dial in perfectly. And honestly? It was worth every minute.

We deploy to ECS Fargate 15 times a day. Fifteen. That's not a flex—it's a necessity for our team's velocity. And with that many deployments, even a few seconds of downtime per deploy adds up fast. Here's how we cracked the code.

Why We Chose ECS Fargate
Let me save you the comparison analysis paralysis: we evaluated App Runner, Lambda with Web Adapter, and even EKS. App Runner looked tempting—dead simple setup—but it doesn't support our preferred region. Lambda? Cold starts and RDS connection management made it a non-starter for our Next.js SSR workloads.

ECS Fargate hit the sweet spot: container-based (so our Docker workflow stayed intact), no EC2 management, and we're only paying for what the containers actually use. Plus, it plays beautifully with RDS, which we were already using for production data.

The Architecture That Finally Worked
Here's what our stack looks like after two weeks of tweaking:

Route 53 handling DNS with ALIAS records pointing to our ALB

Application Load Balancer with TLS 1.3 (because 2026, people) and HSTS headers

ECS Fargate running our Next.js tasks in private subnets

Secrets Manager + SSM Parameter Store for all environment variables (more on this later)

CloudWatch Logs for centralized logging—non-negotiable for debugging

The critical piece? Standalone output mode in Next.js. The docs aren't great, but once you figure out what to copy over, it's magic:

text
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/public ./public
COPY --from=build /app/.next/static ./.next/static
Without this, your Docker images balloon in size and your startup times tank. With it? Lean, mean containers that spin up in seconds.

The 2-Week Journey to Zero-Downtime
Week 1: The Pain

Our first attempt at zero-downtime was... optimistic. We thought the ALB's built-in health checks would handle everything. They didn't.

We learned the hard way that ECS sends a SIGTERM signal when it scales down tasks. If your app doesn't handle it properly, it dies mid-request. Users see 503 errors. You see angry Slack messages.

We started seeing unhealthy targets in the ALB—all of them timing out on health checks. The classic 503 Service Unavailable dance. After debugging networking layers, security groups, and container health check paths, we realized the problem was deeper.

Week 2: The Fix

The breakthrough came when we stopped thinking about the ALB as the solution and started thinking about it as part of the solution.

Here's what finally worked:

  1. Proper Graceful Shutdown Configuration Your app needs to finish serving the current request before shutting down. No shortcuts. We configured:

javascript
// In your server setup
process.on('SIGTERM', () => {
server.close(() => {
process.exit(0);
});
});

  1. ECS stopTimeout
    Set this in your task definition. Give your app enough time to gracefully handle in-flight requests before ECS force-kills it. We settled on 30 seconds—enough for most SSR operations to complete.

  2. ALB Health Check Tuning
    The default health check configuration won't cut it. We had to:

Set a reasonable healthCheckGracePeriodSeconds in the ECS service (60-120 seconds to let tasks warm up)

Tune the ALB target group's health check path to something lightweight but representative of a healthy app

Ensure the health check path returns a 200 within 5 seconds—no heavy rendering for health checks

  1. Environment Variable Strategy This one took time to get right. We eventually adopted a 5-category classification system:

Category Storage Injected At Example
Non-sensitive build-time Task def environment Container start NODE_ENV, PORT
Non-sensitive runtime SSM Standard Container start API_URLs
Semi-sensitive SSM SecureString Container start Client IDs
Highly sensitive Secrets Manager Container start DATABASE_URL, API keys
Client-bundled SSM → Docker build-arg Build time NEXT_PUBLIC_*
This separation means secrets are never in the task definition JSON. They're referenced by ARN only, which makes the ECS console not a security liability.

The Deploy Pipeline That Runs 15x Daily
Our GitHub Actions workflow uses OIDC (no long-lived AWS keys) with environment-based trust policies:

yaml
name: Deploy to ECS
on:
push:
branches: [main, develop]

jobs:
deploy:
environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
runs-on: ubuntu-latest
steps:
- Build Docker image with platform linux/amd64
- Push to ECR with tag: ${{ env }}-${{ sha }}
- Render and register new task definition
- Update ECS service with forced new deployment
- Wait for stability (with 10-minute timeout)
The wait-for-service-stability step is crucial. Without it, we'd deploy, assume everything was fine, and only discover issues five minutes later when the health checks failed.

What I'd Tell My Past Self
If I could go back two weeks and give myself one piece of advice? Start with health checks, then add graceful shutdowns, then tune them together.

They're not separate concerns. The ALB needs to know when a task is healthy enough to receive traffic. The task needs to know how to become healthy and how to become not healthy gracefully. They're a system, not independent configs.

Also: use --platform linux/amd64 in your Docker builds if you're on an ARM Mac. I spent a day wondering why local builds worked but Fargate tasks crashed. Different architectures. Oops.

The Results
Fifteen deployments a day. Zero detected downtime for users.

Is it over-engineered? Maybe. But when you're shipping multiple times a day and your product is critical to your business, "over-engineered" starts to look a lot like "properly engineered."

The two weeks of configuration pain paid off in user trust. No more "we're deploying, give us 30 seconds" messages. No more 503 errors during peak traffic. Just seamless updates, every single time.

And honestly? That's what zero-downtime should mean. Not a bullet point on a marketing page, but a real, measurable property of your deployment pipeline.

Now if you'll excuse me, I have my 14th deployment of the day to trigger.

Top comments (0)