Article Content

About the Author

AWS 6 min read July 4, 2026

Zero-Downtime Docker Migrations on AWS

A step-by-step guide to migrating legacy containers to AWS ECS without dropping client requests.

Docker AWS ECS Production
Share article: LinkedIn X (Twitter)

Introduction

Migrating containerized workloads in production is one of those tasks that sounds straightforward on paper but can quickly unravel into dropped connections, failed health checks, and 3 AM alerts. When the goal is zero downtime, the margin for error shrinks to nearly nothing.

In this guide, we'll walk through a battle-tested approach for migrating legacy Docker containers to AWS Elastic Container Service (ECS) without dropping a single client request. Whether you're moving from a bare-metal Docker host, an older ECS configuration, or another container orchestrator, the principles here apply.


Why Zero-Downtime Migrations Are Hard

Before diving into the how, it's worth understanding the why — specifically, why naïve approaches fail:

  • Abrupt container stops terminate in-flight requests
  • DNS propagation delays cause clients to hit dead endpoints
  • Health check misconfigurations drain traffic before new containers are ready
  • Database schema mismatches between old and new container versions
  • State stored in-container (logs, sessions, uploads) that doesn't survive a restart

ECS with Application Load Balancer (ALB) solves most of these problems — but only if configured correctly.


Prerequisites

Before starting, make sure you have:

  • An existing Docker application with a working Dockerfile
  • AWS CLI configured with appropriate IAM permissions
  • An Application Load Balancer (ALB) set up in your VPC
  • Amazon ECR repository to host your container images
  • Basic familiarity with ECS task definitions and services

Step 1 — Containerize and Push to ECR

If your application already runs in Docker, this step may be mostly done. The key is ensuring your image is production-hardened:

docker
# Use a specific version tag — never 'latest' in production
FROM node:20.11-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

# Run as non-root
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Tag and push to ECR:

bash
# Authenticate
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com

# Build, tag, push
docker build -t my-app .
docker tag my-app:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.2.0
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.2.0
Always use immutable image tags (e.g., v1.2.0 or a Git SHA) — never latest. This makes rollbacks deterministic.

Step 2 — Define Your ECS Task Definition

The task definition is the blueprint for your container. Pay close attention to the health check and stop timeout settings:

json
{
  "family": "my-app",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "containerDefinitions": [
    {
      "name": "my-app",
      "image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.2.0",
      "portMappings": [{ "containerPort": 3000 }],
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
        "interval": 10,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 30
      },
      "stopTimeout": 60,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-app",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}

Key settings to highlight:

SettingValueWhy It Matters
startPeriod30sGives app time to boot before health checks begin
stopTimeout60sAllows in-flight requests to complete before shutdown
interval10sCatches failures quickly without overwhelming the app

Step 3 — Configure ALB Target Group with Connection Draining

The ALB target group is where zero-downtime magic actually happens. Deregistration delay (connection draining) tells the load balancer to wait before fully removing an instance, giving it time to finish serving active requests.

bash
aws elbv2 modify-target-group-attributes \
  --target-group-arn arn:aws:elasticloadbalancing:... \
  --attributes Key=deregistration_delay.timeout_seconds,Value=60

Also ensure your ALB health check path matches your app's actual health endpoint:

bash
aws elbv2 modify-target-group \
  --target-group-arn arn:aws:elasticloadbalancing:... \
  --health-check-path /health \
  --health-check-interval-seconds 10 \
  --healthy-threshold-count 2

Step 4 — Deploy With a Rolling Update Strategy

ECS supports rolling deployments natively. The key parameters are minimumHealthyPercent and maximumPercent:

bash
aws ecs update-service \
  --cluster my-cluster \
  --service my-app-service \
  --task-definition my-app:2 \
  --deployment-configuration \
    minimumHealthyPercent=100,maximumPercent=200
  • minimumHealthyPercent=100 — ECS will never go below 100% capacity, meaning new tasks must become healthy before old ones are stopped
  • maximumPercent=200 — ECS can spin up to 2× the desired count during the rollout, ensuring new tasks start before old ones drain

This guarantees at least one healthy instance is always serving traffic.


Step 5 — Handle Graceful Shutdown in Your Application

ECS sends a SIGTERM signal before stopping a container. Your application must handle this signal and finish processing in-flight requests before exiting.

Here's an example in Node.js:

javascript
const server = app.listen(3000);

process.on('SIGTERM', () => {
  console.log('SIGTERM received. Closing HTTP server...');
  server.close(() => {
    console.log('All connections closed. Exiting.');
    process.exit(0);
  });

  // Force exit after stopTimeout if connections linger
  setTimeout(() => process.exit(1), 55000);
});
Set your forced exit timeout slightly under the stopTimeout value in your task definition to avoid being killed mid-cleanup.

Step 6 — Validate the Migration

Before treating the migration as complete, verify the following:

New tasks pass health checks and reach RUNNING state
ALB target group shows all new targets as healthy
Old tasks drain gracefully (check CloudWatch for connection errors)
Application logs show no error spikes during rollout
Run a synthetic load test during the deployment to confirm zero dropped requests
bash
# Watch ECS service events in real time
watch -n 5 'aws ecs describe-services \
  --cluster my-cluster \
  --services my-app-service \
  --query "services[0].events[:5]" \
  --output table'

Step 7 — Plan for Rollback

Even with careful preparation, you need a fast rollback path. With immutable image tags, rolling back is a single command:

bash
aws ecs update-service \
  --cluster my-cluster \
  --service my-app-service \
  --task-definition my-app:1

ECS will perform the same rolling deployment in reverse. Since you used a pinned image tag for the previous version, the rollback is deterministic and fast.


Common Pitfalls

Don't skip the startPeriod — Without it, ECS may mark containers as unhealthy before they've finished booting, causing a failed deployment loop.

Don't use latest as your image tag — When ECS scales or replaces a task, it may pull a different image than you intended, making debugging nearly impossible.

Don't forget stateful data — Any logs, uploads, or session state stored inside the container will be lost when it stops. Use S3, EFS, or an external session store (e.g., Redis) instead.


Summary

Zero-downtime Docker migrations on AWS ECS come down to a handful of coordinated configurations:

  1. Immutable image tags for predictable deployments and rollbacks
  2. Container health checks with a sufficient startPeriod
  3. stopTimeout + graceful SIGTERM handling to drain in-flight requests
  4. ALB deregistration delay to hold traffic during shutdown
  5. Rolling deployment with minimumHealthyPercent=100 to never drop below capacity

Get these five things right, and your migration — and every future deployment — will be invisible to your users.


Have questions or ran into a different edge case? Drop a comment or reach out on the community forum.

SA

Sahil Aghara

DevOps & Cloud Infrastructure Engineer

Specializing in AWS architecture, Kubernetes orchestration, CI/CD automation, and zero-downtime migrations. Transforming complex cloud challenges into scalable, cost-optimized infrastructure.

AWS CertifiedKubernetesTerraformCI/CD