Skip to content
AWS aws containers 6 min read

Deploying a Container to ECS Fargate (Step by Step)

This page walks you through deploying a real container to AWS, from building the image to a running web service you can hit in a browser. We’ll use ECS (Elastic Container Service, AWS’s container orchestrator) with the Fargate launch type, which runs your containers without you ever managing a server. By the end you’ll have an image in a registry, a task definition that describes how to run it, and a service that keeps it alive behind a load balancer. We’ll cover both the Console (point-and-click) and the AWS CLI (command line) for every step.

What you’re building

The goal is a small web app (an Nginx container listening on port 80) reachable over the internet through an Application Load Balancer (ALB, a layer-7 load balancer that routes HTTP traffic). Here’s the chain of pieces and why each exists:

PieceWhat it isWhy you need it
ECR repositoryElastic Container Registry, AWS’s private Docker image storeFargate pulls your image from here
ECS clusterA logical grouping for your services and tasksA namespace; Fargate clusters hold no servers
Task definitionA JSON blueprint for one running container setDefines image, CPU, memory, ports, logging
ECS serviceA controller that keeps N tasks runningReplaces crashed tasks, registers them with the ALB
ALB + target groupLoad balancer and the list of healthy task IPsGives you one stable public URL

When to use Fargate: choose it when you want to run containers without patching or scaling EC2 (Elastic Compute Cloud) instances yourself, and you’re fine paying a small premium for that convenience. Pick ECS on EC2 instead when you need GPUs, very large instances, or the cheapest possible compute at steady high utilization.

Step 1 — Push your image to ECR

First create a private repository, then build and push your image into it.

Console:

  1. Open the Amazon ECR console and choose Repositories > Create repository.
  2. Leave it Private, name it my-app, and click Create repository.
  3. Select the repo and click View push commands to see the exact docker commands for your account and Region.

CLI:

aws ecr create-repository --repository-name my-app --region us-east-1

# Log Docker in to your private registry
aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com

# Build, tag, and push
docker build -t my-app .
docker tag my-app:latest 111122223333.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
docker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/my-app:latest

Output:

The push refers to repository [111122223333.dkr.ecr.us-east-1.amazonaws.com/my-app]
5f70bf18a086: Pushed
latest: digest: sha256:9b1c... size: 1778

Cost note: ECR storage is about $0.10 per GB-month. A few small images cost pennies. You also pay for data transfer when pulling across Regions, so keep your repo in the same Region as your cluster.

Step 2 — Create a cluster

A Fargate cluster is just a name; no servers are provisioned until tasks run.

Console: Open the Amazon ECS console > Clusters > Create cluster, name it my-cluster, ensure AWS Fargate (serverless) is selected, and create it.

CLI:

aws ecs create-cluster --cluster-name my-cluster

Step 3 — Register a Fargate task definition

The task definition tells Fargate which image to run, how much CPU and memory to give it, which port it listens on, and where to send logs. Create a CloudWatch log group first so logging works.

aws logs create-log-group --log-group-name /ecs/my-app

Save this as task-def.json:

{
  "family": "my-app",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::111122223333:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/my-app:latest",
      "portMappings": [{ "containerPort": 80, "protocol": "tcp" }],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-app",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "web"
        }
      }
    }
  ]
}

The executionRoleArn points to an IAM (Identity and Access Management) role that lets Fargate pull the image and write logs. If you don’t have ecsTaskExecutionRole yet, the ECS console offers to create it for you, or attach the AmazonECSTaskExecutionRolePolicy managed policy to a new role.

aws ecs register-task-definition --cli-input-json file://task-def.json

Output:

{
  "taskDefinition": {
    "taskDefinitionArn": "arn:aws:ecs:us-east-1:111122223333:task-definition/my-app:1",
    "family": "my-app",
    "revision": 1,
    "status": "ACTIVE"
  }
}

Step 4 — Set up the ALB and security groups

Networking is where most first-time Fargate deployments fail, so read this carefully. You need two security groups (virtual firewalls):

  • ALB security group (sg-0a1b2c3d): allow inbound TCP 80 from 0.0.0.0/0 (the internet).
  • Task security group (sg-0b2c3d4e): allow inbound TCP 80 only from the ALB security group, not the whole internet. This is how the load balancer reaches your container.

Create the ALB, a target group of type ip (Fargate registers task IPs, not instance IDs), and a listener:

aws elbv2 create-target-group --name my-app-tg --protocol HTTP --port 80 \
  --vpc-id vpc-0a1b2c3d --target-type ip --health-check-path /

aws elbv2 create-load-balancer --name my-app-alb \
  --subnets subnet-0a1b2c3d subnet-0e5f6a7b \
  --security-groups sg-0a1b2c3d --scheme internet-facing

Then create a listener that forwards port 80 to the target group (use the ARNs returned above).

Step 5 — Create the service

The service launches your tasks and keeps them registered with the target group.

CLI:

aws ecs create-service \
  --cluster my-cluster \
  --service-name my-app-svc \
  --task-definition my-app:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --load-balancers targetGroupArn=arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-app-tg/abc123,containerName=web,containerPort=80 \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-0a1b2c3d,subnet-0e5f6a7b],securityGroups=[sg-0b2c3d4e],assignPublicIp=ENABLED}"

Console: From your cluster, click Create under Services, choose launch type FARGATE, pick the my-app:1 task definition, set Desired tasks to 2, attach the existing ALB and target group, and select your subnets and the task security group.

The #1 gotcha — tasks stuck in PENDING or stopping immediately: Fargate must reach ECR and CloudWatch over the network to pull the image and write logs. In a public subnet, set assignPublicIp=ENABLED (as above). In a private subnet, there is no public IP, so you need either a NAT gateway (a managed device that lets private resources reach the internet) or VPC endpoints for ECR, S3, and CloudWatch Logs. Without one of these, the task fails with CannotPullContainerError or quietly stays in PENDING. VPC endpoints avoid NAT data-processing charges and are usually the cheaper, more secure choice for production.

Step 6 — Verify

Console: Open the service, check the Tasks tab until status shows RUNNING, then open the Target groups in EC2 > Load Balancing and confirm targets are healthy. Copy the ALB’s DNS name and open it in a browser.

CLI:

aws ecs describe-services --cluster my-cluster --services my-app-svc \
  --query 'services[0].{running:runningCount,desired:desiredCount}'

aws elbv2 describe-load-balancers --names my-app-alb \
  --query 'LoadBalancers[0].DNSName' --output text

Output:

{ "running": 2, "desired": 2 }
my-app-alb-1234567890.us-east-1.elb.amazonaws.com

Visit that DNS name and you should see your app. If a task keeps restarting, check CloudWatch Logs group /ecs/my-app and the task’s Stopped reason in the console.

Best practices

  • Pin a specific image tag or digest in the task definition rather than latest, so deploys are repeatable and rollbacks are clean.
  • Run tasks in private subnets with VPC endpoints; expose only the ALB publicly. Never put the wide-open 0.0.0.0/0 rule on the task security group.
  • Configure a meaningful health check path on the target group so the ALB only routes to ready containers.
  • Set desired count to at least 2 across two Availability Zones for high availability, and add a target-tracking autoscaling policy for traffic spikes.
  • Right-size cpu and memory; Fargate bills per vCPU-second and GB-second, so a 0.25 vCPU / 0.5 GB task is far cheaper than an oversized one (roughly $9/month if run continuously).
  • Use a fresh task-definition revision for every deploy and let the service do a rolling update with --force-new-deployment.
Last updated June 15, 2026
Was this helpful?