Deploy FastAPI to AWS - EC2, ECS, and Lambda

FastAPI is excellent for building APIs, but getting it into production on AWS requires understanding your deployment options. This guide covers three approaches: EC2 (traditional servers), ECS with Fargate (containers), and Lambda (serverless).

Prerequisites

Before we begin, ensure you have:

  • AWS CLI configured (aws configure)
  • Docker installed
  • A FastAPI application ready to deploy

Sample FastAPI App

We’ll use this simple app throughout:

# app/main.py
from fastapi import FastAPI
from mangum import Mangum # For Lambda deployment

app = FastAPI(title="My API", version="1.0.0")

@app.get("/")
def root():
return {"message": "Hello from FastAPI on AWS!"}

@app.get("/health")
def health():
return {"status": "healthy"}

@app.get("/items/{item_id}")
def get_item(item_id: int, q: str = None):
return {"item_id": item_id, "query": q}

# Lambda handler (only used for Lambda deployment)
handler = Mangum(app)
# requirements.txt
fastapi>=0.109.0
uvicorn>=0.27.0
mangum>=0.17.0 # For Lambda
gunicorn>=21.0.0 # For EC2/ECS

Option 1: Deploy to EC2

EC2 gives you full control over the server. Best for applications needing persistent connections, background tasks, or specific system configurations.

Step 1: Launch EC2 Instance

# Create a key pair
aws ec2 create-key-pair --key-name fastapi-key --query 'KeyMaterial' --output text > fastapi-key.pem
chmod 400 fastapi-key.pem

# Create security group
aws ec2 create-security-group \
--group-name fastapi-sg \
--description "FastAPI security group"

# Allow SSH and HTTP
aws ec2 authorize-security-group-ingress --group-name fastapi-sg --protocol tcp --port 22 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-name fastapi-sg --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-name fastapi-sg --protocol tcp --port 443 --cidr 0.0.0.0/0

# Launch instance (Amazon Linux 2023)
aws ec2 run-instances \
--image-id ami-0c55b159cbfafe1f0 \
--instance-type t3.micro \
--key-name fastapi-key \
--security-groups fastapi-sg \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=fastapi-server}]'

Step 2: Connect and Setup Server

# SSH into instance
ssh -i fastapi-key.pem ec2-user@<PUBLIC_IP>

# Update system
sudo yum update -y

# Install Python and dependencies
sudo yum install -y python3.11 python3.11-pip nginx git

# Create app directory
sudo mkdir -p /var/www/fastapi
sudo chown ec2-user:ec2-user /var/www/fastapi
cd /var/www/fastapi

# Create virtual environment
python3.11 -m venv venv
source venv/bin/activate

# Install dependencies
pip install fastapi uvicorn gunicorn

Step 3: Deploy Application

# Create the app
cat > /var/www/fastapi/main.py << 'EOF'
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
return {"message": "Hello from FastAPI on EC2!"}

@app.get("/health")
def health():
return {"status": "healthy"}
EOF

Step 4: Setup Systemd Service

# Create systemd service
sudo cat > /etc/systemd/system/fastapi.service << 'EOF'
[Unit]
Description=FastAPI application
After=network.target

[Service]
User=ec2-user
Group=ec2-user
WorkingDirectory=/var/www/fastapi
Environment="PATH=/var/www/fastapi/venv/bin"
ExecStart=/var/www/fastapi/venv/bin/gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 127.0.0.1:8000
Restart=always

[Install]
WantedBy=multi-user.target
EOF

# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable fastapi
sudo systemctl start fastapi

Step 5: Configure Nginx

sudo cat > /etc/nginx/conf.d/fastapi.conf << 'EOF'
server {
listen 80;
server_name _;

location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF

sudo nginx -t
sudo systemctl restart nginx

Step 6: Add SSL with Certbot (Optional)

# Install certbot
sudo yum install -y certbot python3-certbot-nginx

# Get certificate (replace with your domain)
sudo certbot --nginx -d yourdomain.com

# Auto-renewal
sudo systemctl enable certbot-renew.timer

Option 2: Deploy to ECS with Fargate

ECS Fargate is serverless containers - you don’t manage servers but get container flexibility. Best for microservices and scalable applications.

Step 1: Create Dockerfile

# Dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY app/ ./app/

# Run with gunicorn
CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]

Step 2: Build and Push to ECR

# Create ECR repository
aws ecr create-repository --repository-name fastapi-app

# Get login token
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com

# Build and tag
docker build -t fastapi-app .
docker tag fastapi-app:latest <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/fastapi-app:latest

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

Step 3: Create ECS Cluster

# Create cluster
aws ecs create-cluster --cluster-name fastapi-cluster

# Create CloudWatch log group
aws logs create-log-group --log-group-name /ecs/fastapi-app

Step 4: Create Task Definition

// task-definition.json
{
"family": "fastapi-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "fastapi-container",
"image": "<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/fastapi-app:latest",
"portMappings": [
{
"containerPort": 8000,
"protocol": "tcp"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/fastapi-app",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3
}
}
]
}
# Register task definition
aws ecs register-task-definition --cli-input-json file://task-definition.json

Step 5: Create Application Load Balancer

# Create ALB
aws elbv2 create-load-balancer \
--name fastapi-alb \
--subnets subnet-xxx subnet-yyy \
--security-groups sg-xxx

# Create target group
aws elbv2 create-target-group \
--name fastapi-tg \
--protocol HTTP \
--port 8000 \
--vpc-id vpc-xxx \
--target-type ip \
--health-check-path /health

# Create listener
aws elbv2 create-listener \
--load-balancer-arn <ALB_ARN> \
--protocol HTTP \
--port 80 \
--default-actions Type=forward,TargetGroupArn=<TARGET_GROUP_ARN>

Step 6: Create ECS Service

aws ecs create-service \
--cluster fastapi-cluster \
--service-name fastapi-service \
--task-definition fastapi-task \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[sg-xxx],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=<TARGET_GROUP_ARN>,containerName=fastapi-container,containerPort=8000"

Step 7: Auto Scaling (Optional)

# Register scalable target
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/fastapi-cluster/fastapi-service \
--min-capacity 1 \
--max-capacity 10

# Create scaling policy
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/fastapi-cluster/fastapi-service \
--policy-name cpu-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleOutCooldown": 60,
"ScaleInCooldown": 60
}'

Option 3: Deploy to Lambda with API Gateway

Lambda is fully serverless - pay only for requests. Best for APIs with variable traffic, especially those with idle periods.

Step 1: Prepare Lambda Package

# Create deployment package
mkdir lambda_package
cd lambda_package

# Install dependencies
pip install fastapi mangum -t .

# Copy app
cp -r ../app .

# Create ZIP
zip -r ../lambda_function.zip .

Step 2: Create Lambda Function

# Create IAM role for Lambda
aws iam create-role \
--role-name fastapi-lambda-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'

# Attach basic execution policy
aws iam attach-role-policy \
--role-name fastapi-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# Create function
aws lambda create-function \
--function-name fastapi-app \
--runtime python3.12 \
--handler app.main.handler \
--role arn:aws:iam::<ACCOUNT_ID>:role/fastapi-lambda-role \
--zip-file fileb://lambda_function.zip \
--timeout 30 \
--memory-size 256

Step 3: Create API Gateway

# Create HTTP API (simpler than REST API)
aws apigatewayv2 create-api \
--name fastapi-api \
--protocol-type HTTP

# Create integration
aws apigatewayv2 create-integration \
--api-id <API_ID> \
--integration-type AWS_PROXY \
--integration-uri arn:aws:lambda:us-east-1:<ACCOUNT_ID>:function:fastapi-app \
--payload-format-version 2.0

# Create route (catch-all)
aws apigatewayv2 create-route \
--api-id <API_ID> \
--route-key '$default' \
--target integrations/<INTEGRATION_ID>

# Create stage
aws apigatewayv2 create-stage \
--api-id <API_ID> \
--stage-name prod \
--auto-deploy

# Add Lambda permission
aws lambda add-permission \
--function-name fastapi-app \
--statement-id apigateway-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-east-1:<ACCOUNT_ID>:<API_ID>/*"

Step 4: Using SAM for Easier Deployment

AWS SAM simplifies Lambda deployments:

# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
Function:
Timeout: 30
MemorySize: 256

Resources:
FastAPIFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: app.main.handler
Runtime: python3.12
Events:
ApiEvent:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: ANY
RootEvent:
Type: HttpApi
Properties:
Path: /
Method: ANY

Outputs:
ApiUrl:
Description: API Gateway URL
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
# Build and deploy
sam build
sam deploy --guided

Comparison

Aspect EC2 ECS Fargate Lambda
Pricing Per hour Per second (vCPU/memory) Per request + duration
Scaling Manual/Auto Scaling Auto (service) Automatic
Cold starts No Minimal Yes (can be significant)
Max timeout Unlimited Unlimited 15 minutes
WebSockets Yes Yes Limited (API Gateway)
Background tasks Yes Yes No (use SQS/Step Functions)
Maintenance High Medium Low
Best for Full control, persistent connections Microservices, steady traffic Variable traffic, cost optimization

Cost Estimation

EC2 (t3.micro)

  • ~$8.50/month (on-demand)
  • ~$3.50/month (reserved 1-year)

ECS Fargate (0.25 vCPU, 0.5GB)

  • ~$9/month per task running 24/7
  • Scales to zero = $0 when idle

Lambda

  • First 1M requests/month: Free
  • $0.20 per 1M requests after
  • $0.0000166667 per GB-second
  • Example: 1M requests @ 200ms, 256MB = ~$0.85/month

Production Checklist

Security

  • Use HTTPS (ACM certificates)
  • Configure security groups/IAM roles properly
  • Store secrets in Secrets Manager or Parameter Store
  • Enable WAF for API Gateway/ALB

Monitoring

  • CloudWatch Logs enabled
  • CloudWatch Alarms for errors/latency
  • X-Ray tracing for debugging

Performance

  • Right-size instances/containers
  • Enable response compression
  • Use CloudFront for caching (if applicable)

CI/CD

# .github/workflows/deploy.yml (for ECS)
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: us-east-1

- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2

- name: Build and push
run: |
docker build -t fastapi-app .
docker tag fastapi-app:latest ${{ secrets.ECR_REGISTRY }}/fastapi-app:latest
docker push ${{ secrets.ECR_REGISTRY }}/fastapi-app:latest

- name: Update ECS service
run: |
aws ecs update-service --cluster fastapi-cluster --service fastapi-service --force-new-deployment

Conclusion

Choose your deployment strategy based on your needs:

  • EC2: Full control, persistent connections, predictable costs
  • ECS Fargate: Container flexibility, auto-scaling, no server management
  • Lambda: Lowest cost for variable traffic, zero maintenance

For most FastAPI applications, ECS Fargate offers the best balance of flexibility and ease of management. Start with Lambda if you have unpredictable traffic and can work within its constraints. Use EC2 when you need maximum control or have specific infrastructure requirements.

References


   Reprint policy


《Deploy FastAPI to AWS - EC2, ECS, and Lambda》 by Isaac Zhou is licensed under a Creative Commons Attribution 4.0 International License
 Previous
Jinja Templates with FastAPI and Tailwind CSS Jinja Templates with FastAPI and Tailwind CSS
Build beautiful server-rendered pages in FastAPI using Jinja2 templates styled with Tailwind CSS CDN for rapid prototyping.
2026-01-09
Next 
Richardson Maturity Model and HATEOAS - Building Truly RESTful APIs with FastAPI Richardson Maturity Model and HATEOAS - Building Truly RESTful APIs with FastAPI
Understanding the four levels of REST API maturity and implementing HATEOAS in Python with FastAPI for self-documenting, discoverable APIs.
2026-01-07
  TOC