Tutorials Search / Shipping & infrastructure / Deploy a container to ECS Fargate
πŸ“ Written ● Advanced Updated 2026-05-13

Deploy a container to ECS Fargate

Five AWS services collaborating to run one container: ECR holds the image, ECS schedules tasks, Fargate runs them without you managing servers, an ALB routes traffic, Route 53 + ACM front it with a custom domain and HTTPS. Production-grade, autoscaling, AWS-native. Worth the setup if you're staying β€” overkill if you're not.

"Why Fargate over a Lightsail box?" is the first question worth asking, because if you don't have a real answer, the answer is don't. Fargate's reasons-to-exist are autoscaling without managing EC2, container-native deploys that fit a CI pipeline, and integration with the rest of AWS (IAM task roles, VPC networking, ALB health checks, CloudWatch). For a small Node API behind a single domain, a $5 Lightsail instance is genuinely the right answer and Fargate is overkill. For a service that needs to scale from 1 to 100 instances based on traffic, run multiple versions in parallel, talk to private VPC resources, and survive its own infrastructure rolling β€” Fargate exists for exactly that.

The mental model that makes the AWS docs comprehensible: ECR (Elastic Container Registry) is where container images live, like Docker Hub but private and inside your AWS account. ECS (Elastic Container Service) is the orchestrator β€” the thing that says "I want 3 instances of this container running, replace any that die, route traffic to healthy ones." Fargate is the compute mode β€” instead of running ECS on EC2 instances you manage, Fargate is "AWS runs the containers wherever; you don't see the servers." A task definition is the spec ("run image X with these env vars on these ports"). A service says "keep N tasks running based on this task definition behind this load balancer." An ALB (Application Load Balancer) is the public entrypoint that distributes traffic across the running tasks. ACM + Route 53 are TLS and DNS, same as the other tutorials.

This tutorial walks the end-to-end first deploy: dockerize the app, push to ECR, write a task definition, create a Fargate service behind an ALB, wire DNS and a cert, deploy. Every step is mechanical once you've done it once; the first time has a lot of moving parts. Budget two hours for the first deploy; subsequent deploys (Step 9) are one command.

What you'll learn

Prerequisites: Docker installed locally, AWS CLI configured, a containerizable web app (Node, Python, Go, anything that listens on a port), a domain with DNS at Route 53 (strongly recommended β€” non-Route-53 works but is more manual), and a working Dockerfile in your project.

Step 1: Dockerize the app

1

Build a small, working image first

If you don't have a Dockerfile yet, here's a reasonable Node starter:

# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV PORT=8080
EXPOSE 8080
CMD ["node", "server.js"]

Build and test locally:

docker build -t mybrand-api:latest .
docker run -p 8080:8080 mybrand-api:latest
curl http://localhost:8080/health

If it doesn't work locally, it won't work on Fargate. Get the local image healthy before going anywhere near AWS.

Step 2: Create an ECR repository and push

2

One repo per image; private by default

# Create the repo
aws ecr create-repository --repository-name mybrand-api --region us-east-1

# Authenticate Docker to ECR
aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin \
    <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com

# Tag the local image with the ECR URL
docker tag mybrand-api:latest \
  <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mybrand-api:latest

# Push
docker push <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mybrand-api:latest

The full image URI β€” <ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/<REPO>:<TAG> β€” is what the task definition will reference. Note the account ID and region; you'll paste them often.

Step 3: Create the IAM roles ECS needs

3

Two roles: execution and task

ECS needs two distinct IAM roles, and confusing them is the #1 first-deploy failure:

  • Task execution role (ecsTaskExecutionRole) β€” used by ECS/Fargate itself to pull the image from ECR and write logs to CloudWatch. AWS provides a managed policy (AmazonECSTaskExecutionRolePolicy) that's exactly what you need.
  • Task role β€” used by your application code when it calls AWS APIs (e.g., reading from S3). Empty by default; add policies as your app needs them.

Create the execution role once per account:

aws iam create-role --role-name ecsTaskExecutionRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]
  }'
aws iam attach-role-policy --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

The task role is project-specific; for a service that doesn't call AWS APIs, you can omit it.

Step 4: Write the task definition

4

JSON that describes one running instance

Save as task-definition.json:

{
  "family": "mybrand-api",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
  "containerDefinitions": [{
    "name": "api",
    "image": "<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mybrand-api:latest",
    "portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/mybrand-api",
        "awslogs-region": "us-east-1",
        "awslogs-stream-prefix": "ecs",
        "awslogs-create-group": "true"
      }
    }
  }]
}

Smallest Fargate task is 256 CPU units (ΒΌ vCPU) + 512 MB RAM. Most APIs run fine at that size. Register the task definition:

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

Step 5: Create the cluster, target group, and ALB

5

The networking layer that puts traffic in front of your tasks

This part is genuinely faster in the console. AWS Console β†’ ECS β†’ Create cluster β†’ name it mybrand-prod β†’ AWS Fargate (serverless) infrastructure β†’ Create.

Then, separately, create the Application Load Balancer: EC2 β†’ Load Balancers β†’ Create load balancer β†’ Application Load Balancer:

  • Scheme: internet-facing. IP address type: IPv4.
  • VPC: your default VPC. Mappings: at least two subnets in different AZs (Fargate requires this).
  • Security groups: create a new one allowing inbound 80 + 443 from anywhere.
  • Listeners: HTTP:80 forwarding to a new target group called mybrand-api-tg with target type IP, protocol HTTP, port 8080, health-check path /health (or wherever your app responds with 200).

After creation, the ALB has a DNS name like mybrand-prod-12345.us-east-1.elb.amazonaws.com. Note it; Step 8 points your domain at it.

Health-check path matters. If your app doesn't have a /health endpoint returning 200, the ALB will mark every task unhealthy and Fargate will kill them in a loop. Add a one-line health endpoint to your app before deploying. app.get('/health', (req, res) => res.send('ok')) is sufficient.

Step 6: Create the ECS service

6

"Run N tasks, behind this ALB, forever"

ECS console β†’ your cluster β†’ Create service:

  • Launch type: FARGATE.
  • Task definition: mybrand-api (latest revision).
  • Service name: mybrand-api.
  • Desired tasks: 2 for redundancy, 1 if you're testing.
  • Networking: your default VPC, the same two subnets the ALB uses, the security group you created in Step 5.
  • Load balancing: attach to the ALB. Target group: mybrand-api-tg. Container to load balance: api:8080.

Create. Fargate pulls the image, starts the tasks, registers them with the target group. After 2–3 minutes the service shows "ACTIVE" and the target group shows healthy targets. Test:

curl http://<ALB_DNS_NAME>/health
# Expected: ok

Step 7: Request an ACM cert

7

Different from CloudFront: ALBs use the cert from their own region

Unlike CloudFront (which only reads certs from us-east-1), the ALB uses the cert from the same region the ALB lives in. If your ALB is in us-east-1, the cert needs to be in us-east-1; if it's in eu-west-2, the cert needs to be there.

aws acm request-certificate \
  --domain-name api.mybrand.com \
  --validation-method DNS \
  --region <SAME_REGION_AS_ALB>

If DNS is at Route 53 in the same account, the console offers "Create records in Route 53" for the validation CNAME. Otherwise add the CNAME at your DNS provider. Wait for ACM to show "Issued" β€” usually 5–15 minutes.

Step 8: Add HTTPS listener + route DNS

8

One ALB listener for HTTPS; one Route 53 Alias

EC2 β†’ Load Balancers β†’ your ALB β†’ Listeners tab β†’ Add listener:

  • Protocol: HTTPS. Port: 443.
  • Default action: forward to the same target group (mybrand-api-tg).
  • SSL/TLS certificate: the ACM cert from Step 7.

Optionally edit the HTTP:80 listener to redirect to HTTPS:443. Save.

Now point DNS at the ALB. In Route 53 β†’ your hosted zone β†’ Create record:

  • Name: api.
  • Type: A. Alias: on. Route traffic to: Alias to Application and Classic Load Balancer β†’ region β†’ your ALB.

Save. Test: curl -I https://api.mybrand.com/health. You should see HTTP/2 200.

Step 9: The deploy loop

9

Build, push, force new deployment

After all the setup, every subsequent deploy is three commands:

# Build the new image
docker build -t mybrand-api:latest .

# Push to ECR (auth refresh might be needed first)
docker tag mybrand-api:latest \
  <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mybrand-api:latest
docker push <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/mybrand-api:latest

# Tell ECS to roll out new tasks with the same task definition (which references :latest)
aws ecs update-service \
  --cluster mybrand-prod \
  --service mybrand-api \
  --force-new-deployment

ECS performs a rolling deploy: starts new tasks, waits for them to pass the ALB health check, drains connections from the old tasks, kills them. Zero-downtime by default. Watch the deploy in the ECS console under your service's Deployments tab.

To roll back: ECS keeps prior task-definition revisions forever. Update the service to use the previous revision: aws ecs update-service --cluster mybrand-prod --service mybrand-api --task-definition mybrand-api:<PREVIOUS_REVISION>. Same rolling-deploy mechanics in reverse.

Avoid :latest in production task definitions for serious workloads. A rolling deploy started before a push completes could pull an old or partial image. Use immutable tags (mybrand-api:<git-sha>) and update the task definition with each push. Slightly more work; significantly less foot-gun.

What this actually costs

For 2 always-on Fargate tasks at the smallest size (0.25 vCPU + 0.5 GB RAM each), 24/7:

Realistic monthly bill for a small API with 2 always-on tasks: ~$35–40/month. A comparable Lightsail box is $5–10/month. The 4–8Γ— cost difference is what you're paying for autoscaling, rolling deploys, ALB health checks, and the rest of the production-grade machinery. Worth it when you need them, expensive when you don't.

When Fargate is the wrong call

Single instance, no scaling. Lightsail wins on cost and complexity. Move up only when traffic forces it.

Bursty workloads with idle periods. Fargate bills per second but always-on tasks bill always. If your service is mostly idle, look at Lambda (pay only for invocations) or App Runner (Fargate with simpler ergonomics and idle scale-to-zero).

You don't want to operate AWS networking. ECS Fargate hides Fargate-the-compute but exposes you to VPCs, subnets, security groups, target groups, listeners. If that's too much, AWS App Runner is a thinner abstraction over the same machinery β€” fewer knobs, similar capabilities, slightly less control.

What's next