lamine.cloud ← back
// writing · aws · containers

Deploying on AWS ECS, and what you gain over plain EC2

Mouhamadou Lamine Gueye · August 2026 · 8 min read · English

I run several production workloads on ECS Fargate today: a public services platform, a production SaaS, internal tooling. Before that, like most people, I deployed the classic way: an EC2 instance, Docker or PM2 on top, an nginx in front, and a deploy script over SSH. This article is the comparison I wish I had read at the time: what ECS actually changes, how a real deployment looks, and the cases where a plain EC2 box is still the right call.

The EC2 baseline: what you really sign up for

Running your app on an EC2 instance looks simple on day one. It stops being simple the day you own it in production, because the instance is yours to operate:

None of this is hard individually. The point is that it is your undifferentiated work, forever, for every box.

What ECS is, in four objects

ECS has a small mental model. Four objects cover almost everything:

The launch type decides who owns the machines. With the EC2 launch type you still manage instances (you gain orchestration, not ops freedom). With Fargate, AWS runs the task on infrastructure you never see: no AMI, no patching, no SSH. Everything below assumes Fargate, because in my experience that is where the trade actually pays off.

A real deployment, end to end

Three steps: push the image, describe the task, create the service.

1. Build and push to ECR

# once: create the repository
aws ecr create-repository --repository-name myapp

# each deploy: build, tag, push
aws ecr get-login-password | docker login --username AWS \
  --password-stdin 123456789012.dkr.ecr.eu-west-3.amazonaws.com
docker build -t myapp:1.4.2 .
docker tag myapp:1.4.2 123456789012.dkr.ecr.eu-west-3.amazonaws.com/myapp:1.4.2
docker push 123456789012.dkr.ecr.eu-west-3.amazonaws.com/myapp:1.4.2

2. The task definition

{
  "family": "myapp",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "512", "memory": "1024",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/myapp-task-role",
  "containerDefinitions": [{
    "name": "myapp",
    "image": "123456789012.dkr.ecr.eu-west-3.amazonaws.com/myapp:1.4.2",
    "portMappings": [{"containerPort": 8080}],
    "secrets": [{"name": "DB_PASSWORD",
      "valueFrom": "arn:aws:secretsmanager:...:secret:myapp/db"}],
    "logConfiguration": {"logDriver": "awslogs", "options": {
      "awslogs-group": "/ecs/myapp", "awslogs-region": "eu-west-3",
      "awslogs-stream-prefix": "app"}}
  }]
}

Two details in there do a lot of quiet work. taskRoleArn gives this app its own IAM permissions, not the whole machine. And secrets.valueFrom injects credentials from Secrets Manager at start time, so nothing sensitive lives in the definition itself.

3. The service, behind a load balancer

aws ecs create-service --cluster prod-cluster \
  --service-name myapp-service \
  --task-definition myapp \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-a,subnet-b],
      securityGroups=[sg-myapp],assignPublicIp=DISABLED}" \
  --load-balancers "targetGroupArn=arn:...:targetgroup/myapp/abc,
      containerName=myapp,containerPort=8080" \
  --deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true}"

From then on, a deploy is one line in CI: register a new task definition revision with the new image tag, then aws ecs update-service --force-new-deployment. ECS starts the new tasks, waits for the ALB health checks to pass, shifts traffic, and drains the old tasks. If the new version never becomes healthy, the circuit breaker rolls back on its own. Nobody SSHes anywhere.

What you actually gain

ConcernPlain EC2ECS Fargate
OS patching / AMIsYours, foreverGone
Deploy & rollbackCustom scriptsRolling update + automatic rollback, built in
Self-healingSystemd / hopeService replaces unhealthy tasks
ScalingASG + AMI bakingOne number (or auto scaling on CPU/RPS)
Per-app IAMShared instance profileIAM role per task
Secrets.env files on diskInjected from Secrets Manager / SSM
LogsFiles, logrotateCloudWatch out of the box
Blast radiusThe whole boxOne task

The one I underestimated most is self-healing plus circuit breaker. A bad release at 2am stops itself and rolls back before the monitoring even finishes waking you up. On an EC2 box, that same night ends with you inside an SSH session.

Where plain EC2 still wins

Honesty section. I still run and recommend EC2 for some workloads:

Pitfalls I hit so you don't have to

Closing

ECS is not the fancy choice (that reputation belongs to Kubernetes) and that is exactly why I like it for straightforward web workloads on AWS: the entire orchestration surface fits in four concepts, deploys are safe by default, and the platform work you delete is precisely the work that pages you at night. Start with Fargate, keep the task definitions in your IaC, and reach for EC2 only when the numbers or the workload demand it.

Questions, or want a second pair of eyes on your setup? Work with me.

written late at night, watched over by Nox · ← lamine.cloud · more writing