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:
| Piece | What it is | Why you need it |
|---|---|---|
| ECR repository | Elastic Container Registry, AWS’s private Docker image store | Fargate pulls your image from here |
| ECS cluster | A logical grouping for your services and tasks | A namespace; Fargate clusters hold no servers |
| Task definition | A JSON blueprint for one running container set | Defines image, CPU, memory, ports, logging |
| ECS service | A controller that keeps N tasks running | Replaces crashed tasks, registers them with the ALB |
| ALB + target group | Load balancer and the list of healthy task IPs | Gives 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:
- Open the Amazon ECR console and choose Repositories > Create repository.
- Leave it Private, name it
my-app, and click Create repository. - Select the repo and click View push commands to see the exact
dockercommands 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 from0.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 withCannotPullContainerErroror 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/0rule 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
cpuandmemory; 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.