Why ECS Fargate Over EC2?
ECS Fargate removes the need to manage EC2 instances entirely. You define a task (CPU + memory + container image) and AWS handles the underlying compute. For microservices, this means:
- ▸No patching, no SSH, no autoscaling groups to configure
- ▸Per-task billing — idle services cost nothing
- ▸Native integration with ALB, CloudWatch, Secrets Manager, and IAM roles
- ▸Rolling deployments with zero downtime out of the box
Step 1 — Dockerfile for Node.js
Use a multi-stage build to keep the production image lean.
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER appuser
EXPOSE 3000
CMD ["node", "dist/main.js"]Build and test locally before pushing:
docker build -t my-service:latest .
docker run -p 3000:3000 --env-file .env my-service:latestStep 2 — Push to Amazon ECR
ECR is AWS's private container registry — it integrates natively with ECS and IAM.
aws ecr create-repository --repository-name my-service --region ap-south-1
aws ecr get-login-password --region ap-south-1 | \
docker login --username AWS --password-stdin \
123456789.dkr.ecr.ap-south-1.amazonaws.com
docker tag my-service:latest \
123456789.dkr.ecr.ap-south-1.amazonaws.com/my-service:latest
docker push 123456789.dkr.ecr.ap-south-1.amazonaws.com/my-service:latestStep 3 — ECS Task Definition
A task definition is the blueprint for your container — CPU, memory, image, environment, and IAM role.
{
"family": "my-service",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole",
"containerDefinitions": [{
"name": "my-service",
"image": "123456789.dkr.ecr.ap-south-1.amazonaws.com/my-service:latest",
"portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
"secrets": [{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:ap-south-1:123456789:secret:prod/db-url"
}],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-service",
"awslogs-region": "ap-south-1",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
"interval": 30, "timeout": 5, "retries": 3
}
}]
}aws ecs register-task-definition --cli-input-json file://task-definition.jsonStep 4 — ECS Service with ALB
Create the service with a rolling deployment config. minimumHealthyPercent=100 + maximumPercent=200 means new tasks start before old ones stop — zero downtime.
aws ecs create-service \
--cluster production \
--service-name my-service \
--task-definition my-service:1 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-abc],securityGroups=[sg-xyz],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=arn:aws:...,containerName=my-service,containerPort=3000" \
--deployment-configuration "minimumHealthyPercent=100,maximumPercent=200"Step 5 — GitHub Actions CI/CD Pipeline
Automate the full build → push → deploy cycle on every push to main.
# .github/workflows/deploy.yml
name: Deploy to ECS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-south-1
- name: Login to ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/my-service:$IMAGE_TAG .
docker push $ECR_REGISTRY/my-service:$IMAGE_TAG
echo "image=$ECR_REGISTRY/my-service:$IMAGE_TAG" >> $GITHUB_OUTPUT
- 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: my-service
image: ${{ steps.build-image.outputs.image }}
- name: Deploy to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: my-service
cluster: production
wait-for-service-stability: trueProduction Checklist
- ▸Never put secrets in environment variables in the task definition — use Secrets Manager
- ▸Tag images with git SHA, not 'latest' — makes rollbacks trivial
- ▸Enable Container Insights in CloudWatch for CPU/memory metrics per task
- ▸Set up CloudWatch alarms on 5xx ALB errors to catch broken deployments
- ▸Use a VPC with private subnets — Fargate tasks should never have public IPs
- ▸Add a /health endpoint that returns 200 — ECS uses it to determine task health
Wrapping Up
This pipeline — multi-stage Docker build, ECR, ECS Fargate task definition with Secrets Manager, and GitHub Actions — is the setup I use for production microservices. Fully serverless on the infrastructure side and zero-downtime from day one.
Next up: auto-scaling ECS services based on ALB request count and SQS queue depth.