DEV Community

Cover image for How to Deploy a Docker Container on AWS ECS Fargate
Parag Agrawal
Parag Agrawal

Posted on • Originally published at turbodeploy.dev

How to Deploy a Docker Container on AWS ECS Fargate

You've containerized your app with Docker. Now what?

If you're on a PaaS like Railway or Render, you git push and they handle the rest. That simplicity is real but as we covered in The Hidden Costs of Vercel, Railway, and Render, it comes at a 3–5x markup over running the same workload on AWS.

AWS Elastic Container Service (ECS) with Fargate is the alternative. Fargate runs your Docker containers on managed infrastructure, no EC2 instances to patch, no clusters to manage. You define what you want to run, and AWS handles the "how."

The catch? The documentation is scattered across dozens of AWS pages, and most tutorials stop at "Hello World" without covering HTTPS, custom domains, load balancers, or auto-scaling. The things you actually need for a production deployment.

This guide is different. We'll deploy a real web application from zero to production-ready, including every step that other tutorials skip.

By the end of this guide, you'll have:

  • ✅ A Docker image stored in Amazon ECR
  • ✅ An ECS Fargate service running your container 24/7
  • ✅ An Application Load Balancer with HTTPS
  • ✅ A custom domain pointed at your service
  • ✅ Auto-scaling that handles traffic spikes
  • ✅ CloudWatch logging for debugging
  • ✅ A CI/CD pipeline for automated deployments

Let's build it.

8 Steps to Deploy on ECS Fargate


Prerequisites

Before starting, you'll need:

Requirement How to Get It
AWS Account Sign up the free tier includes some ECS resources
AWS CLI v2 brew install awscli (Mac) or install guide
Docker Docker Desktop or OrbStack
A Dockerized app If you need one, follow our Docker for Web Developers guide
A domain (optional) For HTTPS setup in Step 7

Configure AWS CLI:

aws configure
# AWS Access Key ID: <your-access-key>
# AWS Secret Access Key: <your-secret-key>
# Default region name: us-east-1
# Default output format: json
Enter fullscreen mode Exit fullscreen mode

💡 Tip: For production, use IAM Identity Center (SSO) instead of long-lived access keys. We'll cover this in our IAM Roles Explained post.


How It All Fits Together

Before we start clicking buttons, here's the full architecture we're building:

ECS Fargate Deployment Architecture

The flow:

  1. User hits your domain (e.g., app.yourdomain.com)
  2. Route 53 (DNS) resolves it to your ALB
  3. Application Load Balancer terminates HTTPS (via ACM certificate) and routes traffic
  4. ECS Service maintains the desired number of running tasks
  5. Fargate Tasks pull your Docker image from ECR and run your containers
  6. CloudWatch Logs captures container stdout/stderr for debugging
  7. IAM Roles grant your containers permission to access AWS services

Now let's build each piece.


Step 1: Push Your Docker Image to ECR

Amazon Elastic Container Registry (ECR) is AWS's private Docker registry. It's where ECS pulls your images from.

Create an ECR Repository

aws ecr create-repository \
  --repository-name my-web-app \
  --region us-east-1 \
  --image-scanning-configuration scanOnPush=true \
  --encryption-configuration encryptionType=AES256
Enter fullscreen mode Exit fullscreen mode

scanOnPush=true enables automatic vulnerability scanning on every push, free and highly recommended.

Build, Tag, and Push

# 1. Get your AWS account ID
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)

# 2. Authenticate Docker with ECR
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin \
  ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com

# 3. Build the image (for x86 architecture, what Fargate uses by default)
docker build --platform linux/amd64 -t my-web-app .

# 4. Tag for ECR
docker tag my-web-app:latest \
  ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest

# 5. Push
docker push \
  ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest
Enter fullscreen mode Exit fullscreen mode

⚠️ Apple Silicon users (M1/M2/M3): The --platform linux/amd64 flag is critical. Without it, you'll build an ARM image that may crash on x86 Fargate. Alternatively, use --platform linux/arm64 and configure your Fargate task for ARM (Graviton) which is 20% cheaper.

Verify the Push

aws ecr describe-images \
  --repository-name my-web-app \
  --region us-east-1
Enter fullscreen mode Exit fullscreen mode

You should see your image with its digest and size.


Step 2: Create an ECS Cluster

An ECS cluster is a logical grouping of tasks and services. With Fargate, the cluster is essentially a namespace there are no EC2 instances to manage.

aws ecs create-cluster \
  --cluster-name my-app-cluster \
  --setting name=containerInsights,value=enabled
Enter fullscreen mode Exit fullscreen mode

Container Insights adds CPU, memory and network metrics to CloudWatch invaluable for production debugging.

That's it. One command. The cluster is ready.


Step 3: Create the Task Execution IAM Role

ECS needs an IAM role to pull images from ECR and send logs to CloudWatch on your behalf.

Create the Trust Policy

cat > ecs-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
Enter fullscreen mode Exit fullscreen mode

Create the Role

# Create the role
aws iam create-role \
  --role-name ecsTaskExecutionRole \
  --assume-role-policy-document file://ecs-trust-policy.json

# Attach the managed policy
aws iam attach-role-policy \
  --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Enter fullscreen mode Exit fullscreen mode

This grants ECS permission to:

  • Pull images from ECR
  • Create and write to CloudWatch log groups
  • (Optionally) Read secrets from AWS Secrets Manager or SSM Parameter Store

Step 4: Create the Task Definition

The task definition is the blueprint for your container. It defines what image to run, how much CPU/memory to allocate, which ports to expose, and where to send logs.

Create the Task Definition JSON

cat > task-definition.json << EOF
{
  "family": "my-web-app",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "environment": [
        {
          "name": "NODE_ENV",
          "value": "production"
        },
        {
          "name": "PORT",
          "value": "3000"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-web-app",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs",
          "awslogs-create-group": "true"
        }
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}
EOF
Enter fullscreen mode Exit fullscreen mode

Register It

aws ecs register-task-definition \
  --cli-input-json file://task-definition.json
Enter fullscreen mode Exit fullscreen mode

Understanding the Config

Setting Value Why
cpu: "256" 0.25 vCPU Smallest Fargate size; fine for most APIs at low traffic
memory: "512" 512 MB Must be compatible with CPU (see valid combos)
networkMode: "awsvpc" Required for Fargate Each task gets its own ENI (network interface)
logConfiguration CloudWatch Logs Streams container stdout/stderr to /ecs/my-web-app log group
healthCheck HTTP check on /health ECS replaces unhealthy tasks automatically

Valid CPU/Memory Combinations

CPU (vCPU) Memory Options
256 (.25 vCPU) 512 MB, 1 GB, 2 GB
512 (.5 vCPU) 1 GB – 4 GB (in 1 GB increments)
1024 (1 vCPU) 2 GB – 8 GB (in 1 GB increments)
2048 (2 vCPU) 4 GB – 16 GB (in 1 GB increments)
4096 (4 vCPU) 8 GB – 30 GB (in 1 GB increments)

Start small. You can change CPU/memory at any time by registering a new task definition revision and updating the service. Start with 0.25 vCPU / 512 MB and scale up based on actual usage.


Step 5: Create the Application Load Balancer (ALB)

The ALB sits in front of your ECS service, distributes traffic, terminates HTTPS, and enables health-check-based routing.

Get Your VPC and Subnets

# Get default VPC ID
VPC_ID=$(aws ec2 describe-vpcs \
  --filters "Name=isDefault,Values=true" \
  --query "Vpcs[0].VpcId" --output text)

# Get at least 2 subnets (for high availability)
SUBNETS=$(aws ec2 describe-subnets \
  --filters "Name=vpc-id,Values=${VPC_ID}" \
  --query "Subnets[0:2].SubnetId" --output text)

SUBNET_1=$(echo $SUBNETS | awk '{print $1}')
SUBNET_2=$(echo $SUBNETS | awk '{print $2}')

echo "VPC: $VPC_ID"
echo "Subnet 1: $SUBNET_1"
echo "Subnet 2: $SUBNET_2"
Enter fullscreen mode Exit fullscreen mode

Create a Security Group for the ALB

ALB_SG_ID=$(aws ec2 create-security-group \
  --group-name my-app-alb-sg \
  --description "Allow HTTP/HTTPS to ALB" \
  --vpc-id ${VPC_ID} \
  --query "GroupId" --output text)

# Allow HTTP (port 80) from anywhere
aws ec2 authorize-security-group-ingress \
  --group-id ${ALB_SG_ID} \
  --protocol tcp --port 80 --cidr 0.0.0.0/0

# Allow HTTPS (port 443) from anywhere
aws ec2 authorize-security-group-ingress \
  --group-id ${ALB_SG_ID} \
  --protocol tcp --port 443 --cidr 0.0.0.0/0
Enter fullscreen mode Exit fullscreen mode

Create a Security Group for ECS Tasks

TASK_SG_ID=$(aws ec2 create-security-group \
  --group-name my-app-task-sg \
  --description "Allow traffic from ALB to ECS tasks" \
  --vpc-id ${VPC_ID} \
  --query "GroupId" --output text)

# Only allow traffic FROM the ALB (not from the public internet)
aws ec2 authorize-security-group-ingress \
  --group-id ${TASK_SG_ID} \
  --protocol tcp --port 3000 \
  --source-group ${ALB_SG_ID}
Enter fullscreen mode Exit fullscreen mode

🔒 Security note: The task security group only accepts traffic from the ALB, not from the internet directly. This is important as your containers should never be directly exposed.

Create the ALB

ALB_ARN=$(aws elbv2 create-load-balancer \
  --name my-app-alb \
  --subnets ${SUBNET_1} ${SUBNET_2} \
  --security-groups ${ALB_SG_ID} \
  --scheme internet-facing \
  --type application \
  --query "LoadBalancers[0].LoadBalancerArn" --output text)

echo "ALB ARN: $ALB_ARN"
Enter fullscreen mode Exit fullscreen mode

Create a Target Group

The target group tells the ALB where to forward requests (to your ECS tasks).

TG_ARN=$(aws elbv2 create-target-group \
  --name my-app-tg \
  --protocol HTTP \
  --port 3000 \
  --vpc-id ${VPC_ID} \
  --target-type ip \
  --health-check-path /health \
  --health-check-interval-seconds 30 \
  --health-check-timeout-seconds 5 \
  --healthy-threshold-count 2 \
  --unhealthy-threshold-count 3 \
  --query "TargetGroups[0].TargetGroupArn" --output text)

echo "Target Group ARN: $TG_ARN"
Enter fullscreen mode Exit fullscreen mode

Important: --target-type ip is required for Fargate (not instance).

Create an HTTP Listener

aws elbv2 create-listener \
  --load-balancer-arn ${ALB_ARN} \
  --protocol HTTP \
  --port 80 \
  --default-actions Type=forward,TargetGroupArn=${TG_ARN}
Enter fullscreen mode Exit fullscreen mode

We'll add HTTPS in Step 7. For now, HTTP lets you verify everything works.


Step 6: Create the ECS Service

The service maintains your desired number of running tasks. If a task crashes, ECS automatically replaces it. If a task fails health checks, ECS drains and replaces it.

aws ecs create-service \
  --cluster my-app-cluster \
  --service-name my-web-app-service \
  --task-definition my-web-app \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={
    subnets=[${SUBNET_1},${SUBNET_2}],
    securityGroups=[${TASK_SG_ID}],
    assignPublicIp=ENABLED
  }" \
  --load-balancers "targetGroupArn=${TG_ARN},containerName=web,containerPort=3000" \
  --deployment-configuration "maximumPercent=200,minimumHealthyPercent=100" \
  --enable-execute-command
Enter fullscreen mode Exit fullscreen mode

Understanding the Flags

Flag Value Why
--desired-count 2 2 tasks High availability across 2 AZs
--launch-type FARGATE Serverless compute No EC2 instances to manage
assignPublicIp=ENABLED Public IP for tasks Needed to pull images from ECR (unless using VPC endpoints)
maximumPercent=200 During deploys ECS spins up new tasks before killing old ones (zero-downtime)
minimumHealthyPercent=100 During deploys All existing tasks stay running until new ones are healthy
--enable-execute-command ECS Exec Lets you SSH-like into running containers for debugging

Verify the Deployment

# Watch the service stabilize
aws ecs describe-services \
  --cluster my-app-cluster \
  --services my-web-app-service \
  --query "services[0].{status:status,running:runningCount,desired:desiredCount,deployments:deployments[0].rolloutState}"
Enter fullscreen mode Exit fullscreen mode

Wait 2–3 minutes until runningCount matches desiredCount and rolloutState is COMPLETED.

Get the ALB URL

ALB_DNS=$(aws elbv2 describe-load-balancers \
  --load-balancer-arns ${ALB_ARN} \
  --query "LoadBalancers[0].DNSName" --output text)

echo "Your app is live at: http://${ALB_DNS}"
Enter fullscreen mode Exit fullscreen mode

Open that URL - your Dockerized app is now running on AWS! 🎉


Step 7: Add HTTPS and a Custom Domain

Request an SSL Certificate (ACM)

CERT_ARN=$(aws acm request-certificate \
  --domain-name app.yourdomain.com \
  --validation-method DNS \
  --query "CertificateArn" --output text)

echo "Certificate ARN: $CERT_ARN"
Enter fullscreen mode Exit fullscreen mode

Validate the Certificate

ACM will give you a CNAME record to add to your DNS. You can retrieve it:

aws acm describe-certificate \
  --certificate-arn ${CERT_ARN} \
  --query "Certificate.DomainValidationOptions[0].ResourceRecord"
Enter fullscreen mode Exit fullscreen mode

Add this CNAME to your DNS provider (Route 53, Cloudflare, etc.). Validation usually takes 5–30 minutes.

Add an HTTPS Listener

Once the certificate is validated:

# Create HTTPS listener
aws elbv2 create-listener \
  --load-balancer-arn ${ALB_ARN} \
  --protocol HTTPS \
  --port 443 \
  --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
  --certificates CertificateArn=${CERT_ARN} \
  --default-actions Type=forward,TargetGroupArn=${TG_ARN}
Enter fullscreen mode Exit fullscreen mode

Redirect HTTP → HTTPS

# Get the HTTP listener ARN
HTTP_LISTENER_ARN=$(aws elbv2 describe-listeners \
  --load-balancer-arn ${ALB_ARN} \
  --query "Listeners[?Port==\`80\`].ListenerArn" --output text)

# Modify it to redirect to HTTPS
aws elbv2 modify-listener \
  --listener-arn ${HTTP_LISTENER_ARN} \
  --default-actions '[{
    "Type": "redirect",
    "RedirectConfig": {
      "Protocol": "HTTPS",
      "Port": "443",
      "StatusCode": "HTTP_301"
    }
  }]'
Enter fullscreen mode Exit fullscreen mode

Point Your Domain to the ALB

If using Route 53:

# Create a hosted zone (skip if you already have one)
aws route53 create-hosted-zone --name yourdomain.com --caller-reference $(date +%s)

# Add an alias record pointing to the ALB
aws route53 change-resource-record-sets \
  --hosted-zone-id YOUR_ZONE_ID \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "app.yourdomain.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "'${ALB_DNS}'",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'
Enter fullscreen mode Exit fullscreen mode

If using Cloudflare or another DNS provider: Create a CNAME record pointing app.yourdomain.com to your ALB DNS name.


Step 8: Enable Auto-Scaling

Auto-scaling adjusts the number of running tasks based on CPU utilization, memory, or request count.

Register the Scaling Target

aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/my-app-cluster/my-web-app-service \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 10
Enter fullscreen mode Exit fullscreen mode

Create a CPU-Based Scaling Policy

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/my-app-cluster/my-web-app-service \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name cpu-scaling \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 70.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleInCooldown": 300,
    "ScaleOutCooldown": 60
  }'
Enter fullscreen mode Exit fullscreen mode

What This Does

Metric Threshold Action
Average CPU > 70% For 60 seconds Scale out (add tasks)
Average CPU < 70% For 5 minutes Scale in (remove tasks)
Minimum tasks 2 Always running (high availability)
Maximum tasks 10 Cost ceiling

💡 Pro tip: The asymmetric cooldowns (60s out, 300s in) are intentional. Scale out fast to handle traffic spikes; scale in slowly to avoid flapping.


What This Actually Costs

ECS Fargate Monthly Cost Breakdown

Let's calculate the real cost of running this setup 24/7 in us-east-1 with 2 tasks at 0.25 vCPU / 512 MB:

Fargate Compute (Linux/x86)

Per task:
  vCPU: 0.25 × $0.000011244/sec × 86,400 sec/day × 30 days = $7.29/mo
  Memory: 0.5 GB × $0.000001235/sec × 86,400 sec/day × 30 days = $1.60/mo
  Per task total: $8.89/mo

2 tasks: $17.78/mo
Enter fullscreen mode Exit fullscreen mode

ARM/Graviton (20% Cheaper)

Per task:
  vCPU: 0.25 × $0.0000089944/sec × 86,400 sec/day × 30 days = $5.83/mo
  Memory: 0.5 GB × $0.0000009889/sec × 86,400 sec/day × 30 days = $1.28/mo
  Per task total: $7.11/mo

2 tasks: $14.22/mo
Enter fullscreen mode Exit fullscreen mode

Full Monthly Cost Breakdown

Component x86 Cost Graviton Cost
Fargate compute (2 tasks) $17.78 $14.22
Application Load Balancer ~$22.00 ~$22.00
ECR storage (~500 MB) ~$0.05 ~$0.05
CloudWatch Logs (5 GB) ~$2.50 ~$2.50
Data transfer (10 GB) ~$0.90 ~$0.90
Monthly total ~$43 ~$40

Compare this to PaaS pricing: Railway ~$100/mo, Render ~$85/mo for equivalent resources. That's a 50–60% saving by running on Fargate directly. And if you use Compute Savings Plans (1-year commit), Fargate compute drops by another 50%.


Deploying Updates (Zero Downtime)

When you push a new version, ECS performs a rolling deployment:

# 1. Build and push the new image
docker build --platform linux/amd64 -t my-web-app .
docker tag my-web-app:latest \
  ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest
docker push \
  ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest

# 2. Force a new deployment (pulls the latest image)
aws ecs update-service \
  --cluster my-app-cluster \
  --service my-web-app-service \
  --force-new-deployment
Enter fullscreen mode Exit fullscreen mode

What Happens During a Deploy

  1. ECS launches new tasks with the updated image (alongside old tasks)
  2. The ALB starts health-checking the new tasks
  3. Once healthy, the ALB shifts traffic to new tasks
  4. Old tasks are drained (existing connections finish) and stopped
  5. Zero downtime users never see an error

Debugging with ECS Exec

ECS Exec lets you shell into a running container like docker exec but for production:

aws ecs execute-command \
  --cluster my-app-cluster \
  --task <task-id> \
  --container web \
  --interactive \
  --command "/bin/sh"
Enter fullscreen mode Exit fullscreen mode

Get the task ID:

aws ecs list-tasks \
  --cluster my-app-cluster \
  --service-name my-web-app-service \
  --query "taskArns[0]" --output text
Enter fullscreen mode Exit fullscreen mode

View Logs

# Recent logs
aws logs tail /ecs/my-web-app --follow

# Search logs for errors
aws logs filter-log-events \
  --log-group-name /ecs/my-web-app \
  --filter-pattern "ERROR" \
  --start-time $(date -d '1 hour ago' +%s000)
Enter fullscreen mode Exit fullscreen mode

Bonus: CI/CD with GitHub Actions

Automate the entire build → push → deploy pipeline:

# .github/workflows/deploy.yml
name: Deploy to ECS

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsECSRole
          aws-region: us-east-1

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

      - name: Build, tag, and push image
        env:
          ECR_REGISTRY: ${{ steps.ecr-login.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/my-web-app:$IMAGE_TAG .
          docker build -t $ECR_REGISTRY/my-web-app:latest .
          docker push $ECR_REGISTRY/my-web-app:$IMAGE_TAG
          docker push $ECR_REGISTRY/my-web-app:latest

      - name: Update ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: web
          image: ${{ steps.ecr-login.outputs.registry }}/my-web-app:${{ github.sha }}

      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: my-web-app-service
          cluster: my-app-cluster
          wait-for-service-stability: true
Enter fullscreen mode Exit fullscreen mode

Why OIDC, Not Access Keys

This workflow uses OIDC federation instead of storing AWS access keys in GitHub secrets:

  • ✅ No long-lived credentials - temporary tokens, auto-rotated
  • ✅ No secrets to leak - the identity is tied to the GitHub repo
  • ✅ Audit trail - shows up in CloudTrail as GitHubActionsECSRole

Common Errors and Fixes

Error Cause Fix
CannotPullContainerError ECR auth expired or wrong region Re-run aws ecr get-login-password and check --region
ResourceNotFoundException Task definition not found Check aws ecs list-task-definitions
service is not ACTIVE Service failed to stabilize Check aws ecs describe-services for events field
Essential container exited App crashed at startup Check aws logs tail /ecs/my-web-app for stack trace
HealthCheck failed /health endpoint not responding Verify endpoint exists and returns 200 within timeout
port 3000 is not accessible Security group misconfigured Ensure task SG allows ingress from ALB SG on port 3000
OutOfMemoryError App exceeds memory allocation Increase memory in task definition
unable to place a task No subnets have available IPs or wrong AZ Use multiple subnets across 2+ AZs

Cleanup (If You're Done Testing)

# 1. Delete the service
aws ecs update-service \
  --cluster my-app-cluster \
  --service my-web-app-service \
  --desired-count 0
aws ecs delete-service \
  --cluster my-app-cluster \
  --service my-web-app-service --force

# 2. Delete the ALB and target group
aws elbv2 delete-load-balancer --load-balancer-arn ${ALB_ARN}
aws elbv2 delete-target-group --target-group-arn ${TG_ARN}

# 3. Delete the cluster
aws ecs delete-cluster --cluster my-app-cluster

# 4. Delete the ECR repository
aws ecr delete-repository --repository-name my-web-app --force

# 5. Clean up security groups
aws ec2 delete-security-group --group-id ${ALB_SG_ID}
aws ec2 delete-security-group --group-id ${TASK_SG_ID}
Enter fullscreen mode Exit fullscreen mode

TL;DR

Step What You Did Time
1. ECR Created a private registry and pushed your Docker image ~5 min
2. Cluster Created an ECS cluster (one command) ~1 min
3. IAM Created the task execution role ~2 min
4. Task Definition Defined your container's blueprint ~3 min
5. ALB Created a load balancer with security groups ~5 min
6. Service Launched your tasks behind the ALB ~3 min
7. HTTPS/Domain Added SSL certificate and custom domain ~10 min
8. Auto-Scaling Configured CPU-based scaling (2–10 tasks) ~2 min
Total Production-ready deployment ~30 min

Skip All 8 Steps

If this guide felt like a lot of work for "deploy my app", you're right. This is the exact problem TurboDeploy solves.

With TurboDeploy, you:

  1. Connect your GitHub repo
  2. We detect your framework and generate an optimized Dockerfile
  3. We create ALL of the above - ECR, cluster, task definition, ALB, HTTPS, auto-scaling in your AWS account
  4. Push to main, and we deploy automatically

Same infrastructure. Same AWS pricing. Zero configuration.

Join the TurboDeploy waitlist → Ship to production in 5 minutes, not 30.

Top comments (0)