This is Part 1 of a 3-part series on deploying FastAPI applications to AWS. We'll start with EC2 to understand the fundamentals.
I just finished building my first real FastAPI application - a journal API with JWT authentication, PostgreSQL database, and all the features you'd expect. The app worked perfectly on my laptop, but then I hit the wall that every developer faces: how do I deploy this thing?
After trying different approaches and making plenty of mistakes, I decided to document what I learned. This series will cover three main ways to deploy FastAPI to AWS, starting with the most educational approach: EC2.
Why Start with EC2?
I know what you're thinking - "Isn't EC2 old school? Shouldn't I use containers or serverless?"
Here's the thing: starting with EC2 taught me networking, security, and infrastructure concepts that I use every day now. It's like learning to drive with a manual transmission - once you understand the fundamentals, everything else makes more sense.
What We're Building
Our FastAPI application has:
- User authentication with JWT tokens
- PostgreSQL database
- Email functionality for password resets
- RESTful endpoints for journal entries
- Environment-based configuration
The goal is to deploy this securely on AWS with proper networking and database separation.
Architecture Overview
We'll create:
Internet Gateway → Public Subnet (FastAPI VM) → Private Subnet (PostgreSQL VM)
This setup teaches you:
- VPC networking concepts
- Security group configuration
- Database security best practices
- Server management fundamentals
Step 1: Setting Up the Network Infrastructure
Create the VPC
First, let's create our Virtual Private Cloud:
# Create VPC
aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=journal-api-vpc}]'
Create Subnets
We need a public subnet for our API server and a private subnet for the database:
# Public subnet for API server
aws ec2 create-subnet \
--vpc-id vpc-xxxxxxxxx \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-subnet}]'
# Private subnet for database
aws ec2 create-subnet \
--vpc-id vpc-xxxxxxxxx \
--cidr-block 10.0.2.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-subnet}]'
Internet Gateway and Routing
# Create and attach an internet gateway
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=journal-igw}]'
aws ec2 attach-internet-gateway \
--vpc-id vpc-xxxxxxxxx \
--internet-gateway-id igw-xxxxxxxxx
Step 2: Security Groups (This Is Important!)
Security groups are like firewalls for your instances. Getting this right is crucial for security.
API Server Security Group
# Create a security group for the API server
aws ec2 create-security-group \
--group-name journal-api-sg \
--description "Security group for FastAPI server" \
--vpc-id vpc-xxxxxxxxx
# Allow HTTP traffic on port 8000
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxxx \
--protocol tcp \
--port 8000 \
--cidr 0.0.0.0/0
# Allow SSH for management
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxxx \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0
Database Security Group
# Create a security group for the database
aws ec2 create-security-group \
--group-name journal-db-sg \
--description "Security group for PostgreSQL" \
--vpc-id vpc-xxxxxxxxx
# Allow PostgreSQL access ONLY from the API server
aws ec2 authorize-security-group-ingress \
--group-id sg-yyyyyyyyy \
--protocol tcp \
--port 5432 \
--source-group sg-xxxxxxxxx
Key Point: Notice how the database only accepts connections from the API server security group, not from the entire internet. This is a fundamental security practice.
Step 3: Launch the Instances
API Server Instance
aws ec2 run-instances \
--image-id ami-0c02fb55956c7d316 \
--instance-type t3.micro \
--key-name your-key-pair \
--security-group-ids sg-xxxxxxxxx \
--subnet-id subnet-xxxxxxxxx \
--associate-public-ip-address \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=journal-api-server}]'
Database Instance
aws ec2 run-instances \
--image-id ami-0c02fb55956c7d316 \
--instance-type t3.micro \
--key-name your-key-pair \
--security-group-ids sg-yyyyyyyyy \
--subnet-id subnet-yyyyyyyyy \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=journal-db-server}]'
Step 4: Setting Up the API Server
SSH into your API server and let's get FastAPI running:
# Update the system
sudo apt update && sudo apt upgrade -y
# Install Python 3.11
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt update
sudo apt install python3.11 python3.11-venv python3.11-dev python3-pip git -y
# Clone your repository (replace with your repo)
git clone https://github.com/your-username/journal-api.git
cd journal-api
# Create virtual environment
python3.11 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
Environment Configuration
Create your .env
file with the database connection:
cat > .env << EOF
POSTGRES_HOST=10.0.2.xxx # Private IP of your database server
POSTGRES_PORT=5432
POSTGRES_USER=journal_user
POSTGRES_PASSWORD=your_secure_password
POSTGRES_DB=journal_db
SECRET_KEY=your_jwt_secret_key
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
EMAIL_SENDER=your_email@example.com
EMAIL_PASSWORD=your_app_password
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EOF
Step 5: Database Setup
Now let's set up PostgreSQL on the database server. You'll need to access it through a bastion host or AWS Systems Manager Session Manager since it's in a private subnet.
# Install PostgreSQL
sudo apt update
sudo apt install postgresql postgresql-contrib -y
# Start and enable PostgreSQL
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Create a database and a user
sudo -u postgres psql
In the PostgreSQL prompt:
CREATE DATABASE journal_db;
CREATE USER journal_user WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE journal_db TO journal_user;
\q
Configure PostgreSQL for Remote Access
Edit the configuration files:
# Edit postgresql.conf
sudo nano /etc/postgresql/15/main/postgresql.conf
# Change: listen_addresses = '*'
# Edit pg_hba.conf
sudo nano /etc/postgresql/15/main/pg_hba.conf
# Add: host all all 10.0.1.0/24 md5
# Restart PostgreSQL
sudo systemctl restart postgresql
Step 6: Deploy and Test
Back on your API server, run the database migrations:
cd journal-api
source venv/bin/activate
alembic upgrade head
Start your FastAPI application:
uvicorn app.main:app --host 0.0.0.0 --port 8000
Test it:
curl http://your-public-ip:8000/docs
Making It Production Ready
For a production setup, create a systemd service:
sudo tee /etc/systemd/system/journal-api.service > /dev/null <<EOF
[Unit]
Description=Journal API FastAPI application
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/journal-api
Environment=PATH=/home/ubuntu/journal-api/venv/bin
ExecStart=/home/ubuntu/journal-api/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable journal-api
sudo systemctl start journal-api
What I Learned
The Good:
- Complete control over the environment
- Great for understanding networking and security
- Predictable costs
- Easy to debug with direct SSH access
The Challenges:
- Manual scaling (you handle load balancing yourself)
- Ongoing maintenance (OS updates, security patches)
- Single points of failure
- More operational overhead
Cost: Around $25-50/month for a small application
Common Mistakes to Avoid
- Opening security groups too wide - Only allow the minimum required access
- Not setting up monitoring - At least enable CloudWatch basic monitoring
- Forgetting backups - Set up automated database backups
- Not using Elastic IP - Your public IP will change when you restart the instance
What's Next?
In Part 2, we'll look at containerizing this same application and deploying it with ECS Fargate. You'll see how containers solve many of the operational challenges we faced with EC2.
In Part 3, we'll explore the serverless approach with Lambda and see when it makes sense to go completely serverless.
Have you tried deploying FastAPI to EC2? What challenges did you face? Drop your questions in the comments - I'll answer them and might include solutions in the next parts of this series.
Found this helpful? Follow me for Parts 2 and 3 of this series!
Next up: Part 2 - Containerizing with ECS Fargate
Top comments (0)