DEV Community

Goodluck Ekeoma Adiole
Goodluck Ekeoma Adiole

Posted on

Deploying a FastAPI Application on AWS EC2 with CI/CD Using GitHub Actions and Nginx

Introduction

In this blog post, we will walk through the process of deploying a FastAPI application on an AWS EC2 instance with Nginx as a reverse proxy, while leveraging GitHub Actions for Continuous Integration and Continuous Deployment (CI/CD). This will ensure that every change pushed to the main branch is automatically deployed to the server.

By the end of this tutorial, you will have a fully functional, continuously deployed FastAPI application running on AWS.

Project Overview

We will be working with a FastAPI Book API and following these key steps:

  1. Implement the missing GET /api/v1/books/{book_id} endpoint.
  2. Test the application locally.
  3. Dockerize the application.
  4. Deploy it on an AWS EC2 instance.
  5. Set up Nginx as a reverse proxy.
  6. Configure GitHub Actions to automate deployment.

Step 1: Implement the Missing Endpoint

The application already provides endpoints for creating, updating, and deleting books, but we need to add an endpoint to retrieve a book by its ID.

Modify api/routes/books.py by adding the missing endpoint:

from typing import OrderedDict
from typing import Dict

from fastapi import APIRouter, status, HTTPException
from fastapi.responses import JSONResponse

from api.db.schemas import Book, Genre, InMemoryDB

router = APIRouter()

db = InMemoryDB()
db.books = {
    1: Book(
        id=1,
        title="The Hobbit",
        author="J.R.R. Tolkien",
        publication_year=1937,
        genre=Genre.SCI_FI,
    ),
    2: Book(
        id=2,
        title="The Lord of the Rings",
        author="J.R.R. Tolkien",
        publication_year=1954,
        genre=Genre.FANTASY,
    ),
    3: Book(
        id=3,
        title="The Return of the King",
        author="J.R.R. Tolkien",
        publication_year=1955,
        genre=Genre.FANTASY,
    ),
}

@router.post("/", status_code=status.HTTP_201_CREATED)
async def create_book(book: Book):
    db.add_book(book)
    return JSONResponse(
        status_code=status.HTTP_201_CREATED, content=book.model_dump()
    )

@router.get("/", response_model=OrderedDict[int, Book], status_code=status.HTTP_200_OK)
async def get_books() -> OrderedDict[int, Book]:
    return db.get_books()

@router.put("/{book_id}", response_model=Book, status_code=status.HTTP_200_OK)
async def update_book(book_id: int, book: Book) -> Book:
    return JSONResponse(
        status_code=status.HTTP_200_OK,
        content=db.update_book(book_id, book).model_dump(),
    )

@router.delete("/{book_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_book(book_id: int) -> None:
    db.delete_book(book_id)
    return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)

# Added the missing api endpoint
@router.get("/{book_id}", response_model=Book, status_code=status.HTTP_200_OK)
async def get_book(book_id: int):
    book=db.books.get(book_id)
    if not book:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found")
    return book
Enter fullscreen mode Exit fullscreen mode

This ensures that a book is retrieved by its ID or returns a 404 Not Found error if it doesn't exist.


Step 2: Test the Application Locally

Before deploying, test the API locally using pytest:

pytest
Enter fullscreen mode Exit fullscreen mode

To run the FastAPI application locally:

uvicorn api.main:app --host 0.0.0.0 --port 8084 --reload
Enter fullscreen mode Exit fullscreen mode

Test the new endpoint with:

curl -X 'GET' 'http://127.0.0.1:8084/api/v1/books/1' -H 'accept: application/json'
Enter fullscreen mode Exit fullscreen mode

Step 3: Dockerizing the Application

Create a Dockerfile in the root directory:

FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8084"]
Enter fullscreen mode Exit fullscreen mode

Build and run the Docker container:

docker build -t fastapi-app .
docker run -d -p 8084:8084 --name fastapi-container fastapi-app
Enter fullscreen mode Exit fullscreen mode

Step 4: Deploy to AWS EC2

1. Launch an EC2 Instance

  • Choose Ubuntu 22.04.
  • Configure security groups to allow SSH (22), HTTP (80), and Custom TCP (8084).

2. SSH into the Instance

ssh -i your-key.pem ubuntu@your-ec2-public-ip
Enter fullscreen mode Exit fullscreen mode

3. Install Docker and Git

sudo apt update && sudo apt install -y docker.io git
Enter fullscreen mode Exit fullscreen mode

4. Clone the Repository and Run the Application

git clone https://github.com/yourusername/fastapi-book-project.git
cd fastapi-book-project
docker build -t fastapi-app .
docker run -d -p 8084:8084 --name fastapi-container fastapi-app
Enter fullscreen mode Exit fullscreen mode

Test the application from your browser: http://your-ec2-public-ip:8084/api/v1/books/1


Step 5: Set Up Nginx as a Reverse Proxy

Install Nginx:

sudo apt install -y nginx
Enter fullscreen mode Exit fullscreen mode

Modify the Nginx configuration file /etc/nginx/sites-available/default:

server {
    listen 80;
    server_name your-ec2-public-ip;

    location / {
        proxy_pass http://127.0.0.1:8084/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
Enter fullscreen mode Exit fullscreen mode

Restart Nginx:

sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

Now, your FastAPI application is accessible at http://your-ec2-public-ip/.


Step 6: Automate Deployment with GitHub Actions

Create .github/workflows/deploy.yml:

name: Deploy FastAPI to EC2

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
      - name: Deploy to EC2
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            cd ~/fastapi-book-project
            git pull origin main
            docker build -t fastapi-app .
            docker stop fastapi-container || true
            docker rm fastapi-container || true
            docker run -d -p 8084:8084 --name fastapi-container fastapi-app
            sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

Set Up GitHub Secrets

In your GitHub repository:

  • Navigate to Settings > Secrets > Actions.
  • Add EC2_HOST (EC2 public IP) and EC2_SSH_KEY (your private key).

Trigger Deployment

Run:

git add .
git commit -m "Test auto-deploy"
git push origin main
Enter fullscreen mode Exit fullscreen mode

On a successful push to main, GitHub Actions will automatically deploy the app!


Challenges and Resolutions

1. Nginx Not Proxying Requests

Check Nginx logs:

sudo journalctl -u nginx --no-pager | tail -n 20
Enter fullscreen mode Exit fullscreen mode

2. Docker Container Fails to Start

Verify logs:

docker logs fastapi-container
Enter fullscreen mode Exit fullscreen mode

3. GitHub Actions SSH Key Issues

Ensure EC2_SSH_KEY secret is correctly set in GitHub.


Further Enhancements

  • Use AWS ECS instead of EC2 for better scalability.
  • Set up HTTPS with Let's Encrypt.
  • Store secrets securely using AWS Secrets Manager.

With this setup, you have a robust CI/CD pipeline that ensures seamless deployment of your FastAPI application. 🚀

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay