ECS task networking confuses nearly every startup engineer the first time. You've got containers, you've got a VPC, and somewhere in between there's a security group configuration that either works or silently blocks everything. This guide explains how it actually works โ and the three mistakes that will waste your afternoon.
Why ECS Networking Is Different From What You're Used To
On Railway or Render, networking is invisible. Your container gets an IP, traffic flows, you move on. On AWS ECS with Fargate, you're explicitly wiring up four things:
- Which VPC your tasks live in
- Which subnets they run in (public vs private โ this matters)
- Which security groups control inbound and outbound traffic
- Whether tasks get a public IP (or route through a NAT Gateway)
None of this is optional. Get any one wrong and your containers deploy but can't be reached โ or can't reach anything else.
The awsvpc Mode: Each Task Gets Its Own Network Interface
Fargate only supports one networking mode: awsvpc. In this mode, each ECS task gets its own Elastic Network Interface (ENI) โ essentially its own private network card with its own IP address inside your VPC.
This is different from how plain Docker works. In Docker, containers share the host's network and you map ports (-p 3000:3000). In ECS awsvpc mode, there's no host to share. Each task is its own network endpoint.
What this means practically:
- Two tasks can both listen on port 8080 without conflicting โ they have different IPs
- Security groups attach to the task, not the EC2 host
- Each task uses a private IP from whichever subnet it's placed in
- Tasks can talk directly to each other by IP (or via Service Connect โ see our ECS Service Connect guide)
This model is cleaner than Docker bridge networking, but it has one gotcha if you're using EC2 launch type instead of Fargate: each EC2 instance has an ENI limit. A t3.medium supports 3 ENIs total โ one for the instance itself, leaving 2 for ECS tasks. That's why you might see tasks stuck in PROVISIONING on a seemingly healthy cluster. Fargate doesn't have this problem.
The Three Security Groups You Need (And Why They're Separate)
This is where most startup engineers get stuck. You end up with tasks that deploy successfully but return connection refused, or an ALB that shows healthy but your container never receives traffic.
The reason: ECS with an Application Load Balancer requires three separate security groups, each with specific rules that reference the others:
Internet โ [ALB Security Group] โ [Task Security Group] โ [Database Security Group]
1. ALB Security Group
The ALB faces the public internet. Its security group should allow:
- Inbound: TCP 80 and 443 from anywhere (0.0.0.0/0)
- Outbound: TCP on your container port, only to the task security group
Inbound:
Port 80 from 0.0.0.0/0
Port 443 from 0.0.0.0/0
Outbound:
Port 8080 to [task-security-group-id]
2. Task Security Group
Your ECS tasks should only accept traffic from the ALB โ not from the open internet. This is the security group that attaches to each task's ENI.
Inbound:
Port 8080 from [alb-security-group-id] โ reference by SG ID, not CIDR
Outbound:
All traffic to 0.0.0.0/0 โ tasks need to reach ECR, Secrets Manager, etc.
The critical detail: use the security group ID as the source, not a CIDR range. If you use a CIDR (like 10.0.0.0/16), your tasks become reachable from anything in your VPC. Security group references are more precise.
3. Database Security Group
Your RDS or ElastiCache security group should only accept traffic from the task security group:
Inbound:
Port 5432 from [task-security-group-id]
That's it. The database has no reason to accept traffic from anywhere else.
Private Subnets vs Public Subnets: Which One to Use
Where you place your tasks determines how they reach the internet (for pulling Docker images, calling external APIs, writing to S3, etc.).
Public subnet + assign_public_ip = true
- Tasks get a public IP and can reach the internet directly
- Costs less (no NAT Gateway)
- Works fine for development or non-sensitive services
- Problem: your task's IP is publicly reachable โ only the security group stands between it and the internet
Private subnet + assign_public_ip = false
- Tasks only have private IPs
- Internet traffic routes through a NAT Gateway
- The right choice for production workloads
For production at a startup, use private subnets. The NAT Gateway costs roughly $32/month plus data transfer โ annoying but worth it for the security posture.
The NAT Gateway vs VPC Endpoint Trade-Off
Here's the thing nobody tells you upfront: if your tasks are in private subnets, they can't pull Docker images from ECR without either a NAT Gateway or VPC endpoints.
ECR is an AWS service. Normally, traffic to it leaves your VPC, hits the internet, and comes back in. A NAT Gateway handles this. But you can also create VPC Interface Endpoints for ECR, which keeps that traffic entirely inside AWS's network.
NAT Gateway approach:
- Simpler to set up (one NAT Gateway per AZ)
- Handles all internet-bound traffic: ECR, Secrets Manager, S3, external APIs
- ~$32/month base + $0.045/GB data transfer
- Good starting point for most startups
VPC Endpoints approach:
- Interface endpoints for
ecr.apiandecr.dkr+ a Gateway endpoint for S3 - Keeps ECR traffic off the internet entirely
- Better for compliance requirements (SOC 2, HIPAA)
- More moving parts to configure and debug
If you're building toward SOC 2 compliance, VPC endpoints are worth the setup. If you just need things to work now, start with a NAT Gateway and revisit later.
What PROVISIONING Stuck Actually Means
When a task gets stuck in PROVISIONING and won't move to RUNNING, the problem is almost always one of three things:
1. Subnet is out of IP addresses
Each task consumes one private IP from its subnet. A /24 subnet gives you 251 usable IPs. Check with:
aws ec2 describe-subnets \
--subnet-ids subnet-abc123 \
--query 'Subnets[0].AvailableIpAddressCount'
If it returns 0, you need to use a larger subnet or add a second subnet.
2. VPC endpoint misconfiguration (private subnet only)
If tasks are in private subnets with VPC endpoints but no NAT Gateway, the endpoint security groups might be blocking the connection. Ensure the endpoint security groups allow inbound HTTPS (443) from the task security group.
3. EC2 ENI limit hit (EC2 launch type only)
As mentioned above, each EC2 instance has an ENI limit. Add more instances or enable ENI trunking. Fargate doesn't have this problem โ which is one reason to prefer Fargate for most startup workloads.
The Outbound Rule You Always Need
Every task security group needs an outbound rule allowing all traffic:
Outbound: All TCP to 0.0.0.0/0
Without this, your task can't:
- Pull environment variables from Secrets Manager
- Push logs to CloudWatch
- Pull the latest image from ECR on restart
- Call any external API
Tasks that deploy fine on first launch but fail on restart (after a rollback or redeploy) often have a missing or overly restrictive outbound rule.
How NoahOps Wires This Up
Setting up awsvpc networking manually means coordinating three security groups, two subnet types, NAT Gateway routing, and ENI allocation โ before you've written a single line of application code.
NoahOps handles this at provisioning time. When you create a new environment (production, staging, preview), we provision:
- A dedicated VPC with public and private subnets
- Separate security groups for ALB, tasks, and databases โ pre-wired correctly
- NAT Gateway per environment
- VPC endpoints for ECR and Secrets Manager if you're on the compliance-ready tier
The result: your containers run in private subnets with correct security group rules from the first deploy. You don't debug a PROVISIONING timeout on day one.
Request a free demo at noahops.com to see the network architecture we provision for your stack.
FAQ
Can I put my ECS tasks in a public subnet?
Yes, with assign_public_ip = true. It works and costs less (no NAT Gateway). The risk is that your task's public IP is directly reachable โ only the security group prevents access. For production, use private subnets.
Do I need a separate security group per ECS service?
Not strictly required, but recommended. If you share one security group across all services, you can't restrict database access per service. Use one security group per service type (API, worker, etc.) and reference them in database rules.
Why does my task work in development but fail in production?
Almost always a subnet or security group mismatch. Development often uses public subnets with a permissive security group. Production uses private subnets where the NAT Gateway (or VPC endpoint) configuration matters. Check that your production tasks can reach ECR and Secrets Manager.
What's the difference between the execution role and the task role for networking?
The execution role lets ECS pull your image from ECR and read secrets. The task role is what your application code uses to call AWS services. Both need to exist; forgetting the execution role causes image pull failures that look like networking errors.
Do I need a NAT Gateway in every Availability Zone?
For production: yes. A single NAT Gateway in one AZ creates a single point of failure โ if that AZ has issues, tasks in other AZs lose internet access. For development, one NAT Gateway is fine.