FastAPI Complete Course Module 16: Deployment

FastAPI Complete Course Module 16: Deployment

AI Reading

Quick summary of this article

This module teaches you how to deploy a FastAPI application to production. It covers setting up a VPS with Ubuntu, using Gunicorn with Uvicorn workers for reliable process management, configuring Nginx as a reverse proxy, securing the site with free HTTPS certificates from Let's Encrypt, and containerizing the app with Docker. The goal is to move from local development to a secure, scalable, and always-available production server.

  • Production Stack: The recommended setup is a VPS running Ubuntu, with Nginx as a reverse proxy, Gunicorn managing Uvicorn workers, and Let's Encrypt for HTTPS. This combination provides reliability, performance, and security.
  • Gunicorn + Uvicorn: For production, use Gunicorn with Uvicorn workers instead of Uvicorn alone. Gunicorn handles process management (restarts, signals) while Uvicorn provides the ASGI server.
  • Systemd Service: Create a systemd service to keep your app running after you log out, automatically restart it on failure, and manage it as a background process.
  • Nginx Reverse Proxy: Configure Nginx to listen on public ports (80/443) and forward requests to your FastAPI app running on an internal port (e.g., 127.0.0.1:8000). It handles SSL termination and can serve static files directly.
  • Docker Option: Containerize your app with a Dockerfile for consistent environments. Use Docker Compose to manage multi-service setups, like connecting your app to a PostgreSQL database.

Introduction

Welcome to Module 16 of the FastAPI Complete Course. You’ve built a robust API with FastAPI, connected it to databases, handled authentication, and written tests. Now it’s time to share your work with the world. This module covers FastAPI Deployment, taking your application from your local development machine to a production server that users can access 24/7.

Deploying a FastAPI application involves more than just running uvicorn main:app. In production, you need a reliable, secure, and scalable setup. We will cover the most common and professional deployment stack: a Virtual Private Server (VPS) running Ubuntu, with Nginx as a reverse proxy, Gunicorn as the process manager running Uvicorn workers, and a Dockerized deployment option. We’ll also secure everything with HTTPS using Let’s Encrypt.

By the end of this module, you will be able to deploy a FastAPI application to a real server with confidence. Let’s get started.

Deploy on a VPS (Virtual Private Server)

A VPS gives you full control over your server environment. For a production FastAPI application, this is the standard choice. We’ll use Ubuntu Server 22.04 LTS as our operating system.

Choosing a VPS Provider

Popular providers include DigitalOcean, Linode, Vultr, and AWS EC2. For this tutorial, we assume you have a fresh Ubuntu 22.04 server with root or sudo access. You should be able to SSH into it.

Initial Server Setup

After SSH-ing into your server, perform these initial steps:

  1. Update the package list and upgrade existing packages:
sudo apt update && sudo apt upgrade -y
  1. Create a non-root user (replace ‘fastapi_user’ with your preferred username):
sudo adduser fastapi_user
sudo usermod -aG sudo fastapi_user
su - fastapi_user
  1. Install essential tools:
sudo apt install python3-pip python3-venv nginx git curl -y

Setting Up the Application Directory

We’ll clone our FastAPI project into /home/fastapi_user/app.

mkdir /home/fastapi_user/app
cd /home/fastapi_user/app
git clone [your-repository-url] .
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Make sure your requirements.txt includes fastapi, uvicorn, gunicorn, and any other dependencies.

Gunicorn/Uvicorn Setup

While Uvicorn is great for development, in production you need a process manager that can handle multiple worker processes, restart on failure, and manage signals. Gunicorn with Uvicorn workers is the recommended combination.

Why Gunicorn + Uvicorn?

  • Gunicorn manages worker processes, handles signals (like HUP for reload), and provides robust logging.
  • Uvicorn provides the ASGI server that speaks the ASGI protocol FastAPI uses.
  • Together, they give you the reliability of Gunicorn with the performance of Uvicorn.

Testing the Setup Manually

First, test that your application runs correctly with Gunicorn:

cd /home/fastapi_user/app
source venv/bin/activate
gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:8000

Explanation:

  • -w 4: Number of worker processes (adjust based on your server’s CPU cores).
  • -k uvicorn.workers.UvicornWorker: Tells Gunicorn to use Uvicorn’s worker class.
  • main:app: Replace main with your FastAPI file name and app with your FastAPI instance.
  • --bind 0.0.0.0:8000: Listens on all network interfaces on port 8000.

If your application starts without errors, press Ctrl+C to stop it.

Creating a Systemd Service

To keep your application running even after you log out, and to automatically restart it on failure, we’ll create a systemd service.

Create the service file:

sudo nano /etc/systemd/system/fastapi.service

Add the following content:

[Unit]
Description=FastAPI application with Gunicorn
After=network.target

[Service]
User=fastapi_user
Group=www-data
WorkingDirectory=/home/fastapi_user/app
Environment="PATH=/home/fastapi_user/app/venv/bin"
ExecStart=/home/fastapi_user/app/venv/bin/gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 127.0.0.1:8000
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Explanation of key directives:

  • User and Group: Runs the service as your non-root user for security.
  • WorkingDirectory: Sets the working directory to your app folder.
  • Environment: Points to the virtual environment’s bin directory so Python and Gunicorn are found.
  • ExecStart: The exact command to start your app. Note we bind to 127.0.0.1:8000 (localhost) because Nginx will proxy requests to this internal port.
  • Restart=always and RestartSec=5: Automatically restart if the process crashes, with a 5-second delay.

Now enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable fastapi
sudo systemctl start fastapi
sudo systemctl status fastapi

If the status shows active (running), your FastAPI app is now running as a service.

Nginx Configuration

Nginx acts as a reverse proxy. It sits in front of your Gunicorn/Uvicorn server and handles:

  • SSL/TLS termination (HTTPS)
  • Serving static files directly (if any)
  • Load balancing
  • Security (hiding internal server details)

Basic Nginx Reverse Proxy Config

Create a new Nginx configuration file:

sudo nano /etc/nginx/sites-available/fastapi

Add the following:

server {
    listen 80;
    server_name your_domain.com www.your_domain.com;

    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;
    }

    location /static/ {
        alias /home/fastapi_user/app/static/;
    }
}

Explanation:

  • listen 80: Listens on port 80 (HTTP).
  • server_name: Replace with your actual domain name.
  • proxy_pass: Forwards all requests to your FastAPI app running on port 8000.
  • proxy_set_header: These headers pass important information like the original client IP and protocol to your FastAPI app. Your app can then use request.client.host and request.url.scheme correctly.
  • location /static/: If you have static files (e.g., for a documentation site), serve them directly from Nginx for better performance.

Enable the site and test:

sudo ln -s /etc/nginx/sites-available/fastapi /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

If nginx -t returns syntax is ok, your configuration is valid.

HTTPS SSL Configuration

HTTPS is non-negotiable for production. We’ll use Certbot from Let’s Encrypt to obtain a free SSL certificate.

Installing Certbot

sudo apt install certbot python3-certbot-nginx -y

Obtaining and Installing the Certificate

Run Certbot with the Nginx plugin. It will automatically modify your Nginx configuration to enable HTTPS.

sudo certbot --nginx -d your_domain.com -d www.your_domain.com

Follow the interactive prompts:

  1. Enter your email address (for renewal notices).
  2. Agree to the terms of service.
  3. Choose whether to redirect HTTP to HTTPS (recommended: choose option 2).

After completion, Certbot will modify your Nginx config to include SSL settings. Your /etc/nginx/sites-available/fastapi file will now look similar to this:

server {
    listen 443 ssl;
    server_name your_domain.com www.your_domain.com;

    ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    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;
    }

    location /static/ {
        alias /home/fastapi_user/app/static/;
    }
}

server {
    listen 80;
    server_name your_domain.com www.your_domain.com;
    return 301 https://$server_name$request_uri;
}

The second server block redirects all HTTP traffic to HTTPS. Certbot also sets up automatic renewal via a systemd timer. You can test renewal with:

sudo certbot renew --dry-run

Docker Deployment

Docker provides a consistent environment across development, testing, and production. It eliminates the “it works on my machine” problem.

Creating a Dockerfile

In the root of your FastAPI project, create a file named Dockerfile (no extension):

# Use an official Python runtime as a parent image
FROM python:3.11-slim

# Set the working directory in the container
WORKDIR /app

# Copy the requirements file first for better caching
COPY requirements.txt .

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY . .

# Make port 8000 available to the world outside this container
EXPOSE 8000

# Run the application using Gunicorn with Uvicorn workers
CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "main:app", "--bind", "0.0.0.0:8000"]

Line-by-line explanation:

  • FROM python:3.11-slim: Starts from a lightweight Python 3.11 image.
  • WORKDIR /app: Sets the working directory inside the container.
  • COPY requirements.txt .: Copies only the requirements file first. This leverages Docker’s layer caching—if requirements haven’t changed, this layer is reused.
  • RUN pip install ...: Installs dependencies. --no-cache-dir keeps the image smaller.
  • COPY . .: Copies the rest of your application.
  • EXPOSE 8000: Documents that the container listens on port 8000 (does not publish the port).
  • CMD: The command to run when the container starts.

Building and Running the Docker Image

# Build the image
docker build -t fastapi-app .

# Run the container
docker run -d --name fastapi-container -p 8000:8000 fastapi-app
  • -d: Runs in detached mode (background).
  • --name: Gives the container a name.
  • -p 8000:8000: Maps host port 8000 to container port 8000.

Your FastAPI app is now running in a Docker container. You can test it at http://your_server_ip:8000.

Docker Compose for Multi-Service Apps

If your app uses a database (like PostgreSQL) or Redis, use Docker Compose. Create a docker-compose.yml file:

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/fastapi_db
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: fastapi_db
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Run with docker-compose up -d. This sets up both your FastAPI app and a PostgreSQL database, with the app connecting to the db service via the internal Docker network.

Common Mistakes

Here are pitfalls beginners often face during deployment:

  1. Binding to 0.0.0.0 directly in production: When using Gunicorn behind Nginx, bind to 127.0.0.1 (localhost) to prevent direct external access to your app server. Only Nginx should be publicly accessible.
  2. Forgetting to set environment variables: Use a .env file or set them in the systemd service file using Environment= directives.
  3. Not configuring CORS: If your frontend is on a different domain, you must configure CORS middleware in FastAPI. Otherwise, browsers will block requests.
  4. Using the default Uvicorn in production: Always use Gunicorn with Uvicorn workers for production. Uvicorn alone lacks process management features.
  5. Ignoring firewall rules: Configure ufw (Uncomplicated Firewall) to allow only ports 80 (HTTP) and 443 (HTTPS). Deny port 8000.
sudo ufw allow 80
sudo ufw allow 443
sudo ufw deny 8000
sudo ufw enable

Practice Task

To solidify your learning, complete the following task:

  1. Deploy a sample FastAPI application (you can use the one from Module 1) to a VPS (or a local VM if you don’t have a VPS).
  2. Set up Gunicorn with Uvicorn workers behind Nginx.
  3. Configure HTTPS using Certbot.
  4. Create a Dockerfile for the same application and verify it runs in a container.
  5. Write a systemd service file for the non-Docker deployment.
  6. Test that your API is accessible via HTTPS and that the root endpoint returns a JSON response.

If you get stuck, revisit the code examples above. The goal is to have a fully functional, secure, and production-ready FastAPI application.

Summary

In this module, you learned how to take a FastAPI application from development to production. We covered:

  • VPS Setup: Preparing an Ubuntu server with Python and Nginx.
  • Gunicorn/Uvicorn: The recommended production server combination with a systemd service for reliability.
  • Nginx Configuration: Reverse proxy setup to forward requests to your app and serve static files.
  • HTTPS with Let’s Encrypt: Securing your site with free SSL certificates using Certbot.
  • Docker Deployment: Containerizing your application with a Dockerfile and Docker Compose for multi-service setups.

You now have the skills to deploy any FastAPI application professionally. This is a critical skill for any job-oriented developer.

FAQs

1. Can I use Uvicorn alone in production?

Technically yes, but it’s not recommended. Uvicorn lacks process management features like automatic restarts, graceful shutdowns, and handling multiple concurrent requests efficiently. Gunicorn with Uvicorn workers is the standard production setup.

2. Do I need a domain name to deploy?

For HTTPS, yes—Let’s Encrypt requires a domain to issue certificates. However, you can deploy without a domain using just the IP address, but you’ll get browser security warnings. For testing, you can use self-signed certificates.

3. How do I update my deployed application?

For non-Docker: SSH into the server, pull the latest code from git, and restart the service (sudo systemctl restart fastapi). For Docker: rebuild the image (docker build -t fastapi-app .), stop the old container, and run a new one.

4. What is the recommended number of Gunicorn workers?

A common formula is 2 * number_of_CPU_cores + 1. For a 2-core server, that’s 5 workers. Start with 4 and monitor performance.

5. My Nginx returns 502 Bad Gateway. What’s wrong?

This usually means Nginx cannot reach your FastAPI app. Check that your Gunicorn service is running (sudo systemctl status fastapi). Verify the port in your Nginx config matches the port in your Gunicorn command (both should be 127.0.0.1:8000). Also check firewall rules.


You’ve completed Module 16: Deployment. Your FastAPI application is now live, secure, and scalable. In Module 17: Advanced Topics and Performance Tuning, we’ll dive into database connection pooling, caching with Redis, asynchronous background tasks, and profiling your application for maximum performance. See you there!

Additional Practical Example

Let’s build a complete deployment-ready FastAPI application with environment-specific configurations, database migrations, and a production-grade Docker setup. This example will demonstrate how to structure a real-world application for deployment.

First, create the project structure:

# project_structure.txt
my_fastapi_app/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── database.py
│   ├── models.py
│   ├── schemas.py
│   ├── routers/
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── utils/
│       ├── __init__.py
│       └── security.py
├── alembic/
│   └── versions/
├── alembic.ini
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env
├── .env.example
└── tests/
    ├── __init__.py
    └── test_main.py

Now, let’s create the configuration module that handles different environments:

# app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
from typing import Optional

class Settings(BaseSettings):
    # Application settings
    APP_NAME: str = "My FastAPI App"
    APP_VERSION: str = "1.0.0"
    DEBUG: bool = False
    
    # Database settings
    DATABASE_URL: str = "sqlite:///./test.db"
    DATABASE_POOL_SIZE: int = 10
    DATABASE_MAX_OVERFLOW: int = 20
    
    # Security settings
    SECRET_KEY: str = "your-secret-key-here"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    ALGORITHM: str = "HS256"
    
    # Server settings
    HOST: str = "0.0.0.0"
    PORT: int = 8000
    WORKERS: int = 4
    
    # CORS settings
    CORS_ORIGINS: list = ["*"]
    
    # Redis settings (optional)
    REDIS_URL: Optional[str] = None
    
    # Logging settings
    LOG_LEVEL: str = "INFO"
    LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    
    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

@lru_cache()
def get_settings():
    return Settings()

Next, create the database setup with connection pooling:

# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.config import get_settings

settings = get_settings()

# Create engine with connection pooling for production
engine = create_engine(
    settings.DATABASE_URL,
    pool_size=settings.DATABASE_POOL_SIZE,
    max_overflow=settings.DATABASE_MAX_OVERFLOW,
    pool_pre_ping=True,  # Verify connections before using
    pool_recycle=3600,   # Recycle connections after 1 hour
    echo=settings.DEBUG  # SQL logging in debug mode
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def get_db():
    """Dependency that provides database sessions."""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def init_db():
    """Create all tables (use migrations in production)."""
    Base.metadata.create_all(bind=engine)

Now, let’s create the main application with lifecycle events:

# app/main.py
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from contextlib import asynccontextmanager
import logging
import time
from app.config import get_settings
from app.database import init_db, engine
from app.routers import items, users

settings = get_settings()

# Configure logging
logging.basicConfig(
    level=getattr(logging, settings.LOG_LEVEL),
    format=settings.LOG_FORMAT
)
logger = logging.getLogger(__name__)

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Handle application startup and shutdown events."""
    # Startup
    logger.info("Starting up application...")
    init_db()
    logger.info("Database initialized successfully")
    yield
    # Shutdown
    logger.info("Shutting down application...")
    engine.dispose()
    logger.info("Database connections closed")

app = FastAPI(
    title=settings.APP_NAME,
    version=settings.APP_VERSION,
    debug=settings.DEBUG,
    lifespan=lifespan,
    docs_url="/docs" if settings.DEBUG else None,
    redoc_url="/redoc" if settings.DEBUG else None
)

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Add request timing middleware
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    logger.info(f"Request to {request.url.path} took {process_time:.3f}s")
    return response

# Include routers
app.include_router(items.router, prefix="/api/v1/items", tags=["items"])
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])

@app.get("/")
async def root():
    return {
        "message": "Welcome to My FastAPI App",
        "version": settings.APP_VERSION,
        "docs": "/docs" if settings.DEBUG else "Disabled in production"
    }

@app.get("/health")
async def health_check():
    return {"status": "healthy", "timestamp": time.time()}

Create a sample router with proper error handling:

# app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from app.database import get_db
from app.models import Item
from app.schemas import ItemCreate, ItemResponse

router = APIRouter()

@router.get("/", response_model=List[ItemResponse])
async def get_items(
    skip: int = 0,
    limit: int = 100,
    db: Session = Depends(get_db)
):
    """Get all items with pagination."""
    items = db.query(Item).offset(skip).limit(limit).all()
    return items

@router.post("/", response_model=ItemResponse, status_code=status.HTTP_201_CREATED)
async def create_item(
    item: ItemCreate,
    db: Session = Depends(get_db)
):
    """Create a new item."""
    db_item = Item(**item.model_dump())
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

@router.get("/{item_id}", response_model=ItemResponse)
async def get_item(
    item_id: int,
    db: Session = Depends(get_db)
):
    """Get a specific item by ID."""
    item = db.query(Item).filter(Item.id == item_id).first()
    if not item:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Item with id {item_id} not found"
        )
    return item

Now, create the production Docker setup:

# Dockerfile
FROM python:3.11-slim AS builder

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends 
    gcc 
    && rm -rf /var/lib/apt/lists/*

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

# Production stage
FROM python:3.11-slim AS production

WORKDIR /app

# Create non-root user
RUN useradd -m -u 1000 appuser && 
    chown -R appuser:appuser /app

# Copy dependencies from builder
COPY --from=builder /root/.local /home/appuser/.local
ENV PATH=/home/appuser/.local/bin:$PATH

# Copy application code
COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

# Expose port
EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 
    CMD curl -f http://localhost:8000/health || exit 1

# Run with uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--proxy-headers", "--forwarded-allow-ips", "*"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
      - SECRET_KEY=${SECRET_KEY}
      - DEBUG=false
      - LOG_LEVEL=INFO
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - static_volume:/app/static
    networks:
      - app_network
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    networks:
      - app_network
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - static_volume:/static:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    networks:
      - app_network
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  static_volume:

networks:
  app_network:
    driver: bridge
# nginx.conf
upstream fastapi_app {
    server app:8000;
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://fastapi_app;
        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;
        proxy_read_timeout 60s;
        proxy_connect_timeout 10s;
    }

    location /static/ {
        alias /static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location /health {
        proxy_pass http://fastapi_app/health;
        access_log off;
    }
}

Class-Based Implementation Example

While FastAPI works well with function-based views, you can also use class-based implementations for better organization and reusability. Here’s how to implement a deployment-ready application using classes:

# app/classes.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
from pydantic import BaseModel
from app.database import get_db
from app.models import Item
from app.schemas import ItemCreate, ItemResponse

class ItemService:
    """Service layer for item operations."""
    
    def __init__(self, db: Session):
        self.db = db
    
    def get_all(self, skip: int = 0, limit: int = 100) -> List[Item]:
        return self.db.query(Item).offset(skip).limit(limit).all()
    
    def get_by_id(self, item_id: int) -> Optional[Item]:
        return self.db.query(Item).filter(Item.id == item_id).first()
    
    def create(self, item_data: ItemCreate) -> Item:
        db_item = Item(**item_data.model_dump())
        self.db.add(db_item)
        self.db.commit()
        self.db.refresh(db_item)
        return db_item
    
    def update(self, item_id: int, item_data: ItemCreate) -> Item:
        db_item = self.get_by_id(item_id)
        if not db_item:
            raise HTTPException(status_code=404, detail="Item not found")
        for key, value in item_data.model_dump().items():
            setattr(db_item, key, value)
        self.db.commit()
        self.db.refresh(db_item)
        return db_item
    
    def delete(self, item_id: int) -> bool:
        db_item = self.get_by_id(item_id)
        if not db_item:
            raise HTTPException(status_code=404, detail="Item not found")
        self.db.delete(db_item)
        self.db.commit()
        return True

class ItemController:
    """Controller class for item endpoints."""
    
    def __init__(self):
        self.router = APIRouter(prefix="/api/v2/items", tags=["items-v2"])
        self.router.add_api_route("/", self.get_items, methods=["GET"])
        self.router.add_api_route("/", self.create_item, methods=["POST"], status_code=201)
        self.router.add_api_route("/{item_id}", self.get_item, methods=["GET"])
        self.router.add_api_route("/{item_id}", self.update_item, methods=["PUT"])
        self.router.add_api_route("/{item_id}", self.delete_item, methods=["DELETE"])
    
    async def get_items(
        self,
        skip: int = 0,
        limit: int = 100,
        db: Session = Depends(get_db)
    ) -> List[ItemResponse]:
        service = ItemService(db)
        return service.get_all(skip, limit)
    
    async def get_item(
        self,
        item_id: int,
        db: Session = Depends(get_db)
    ) -> ItemResponse:
        service = ItemService(db)
        item = service.get_by_id(item_id)
        if not item:
            raise HTTPException(status_code=404, detail="Item not found")
        return item
    
    async def create_item(
        self,
        item: ItemCreate,
        db: Session = Depends(get_db)
    ) -> ItemResponse:
        service = ItemService(db)
        return service.create(item)
    
    async def update_item(
        self,
        item_id: int,
        item: ItemCreate,
        db: Session = Depends(get_db)
    ) -> ItemResponse:
        service = ItemService(db)
        return service.update(item_id, item)
    
    async def delete_item(
        self,
        item_id: int,
        db: Session = Depends(get_db)
    ) -> dict:
        service = ItemService(db)
        service.delete(item_id)
        return {"message": "Item deleted successfully"}

# In main.py, add:
# from app.classes import ItemController
# item_controller = ItemController()
# app.include_router(item_controller.router)

This class-based approach separates concerns into service and controller layers, making your code more maintainable and testable. The service layer handles business logic, while the controller handles HTTP-specific concerns.

Hands-On Practice Task

Now it’s your turn to practice deploying a FastAPI application. Complete the following task:

Task: Deploy a Multi-Environment FastAPI Application

Create a complete deployment setup for a FastAPI application that includes:

  1. Configuration Management: Create environment-specific configuration files (.env.development, .env.staging, .env.production) with appropriate settings for each environment.
  2. Docker Setup: Create a multi-stage Dockerfile that builds and runs your application efficiently.
  3. Database Migrations: Set up Alembic for database migrations and create an initial migration.
  4. CI/CD Pipeline: Create a GitHub Actions workflow that:
    • Runs tests on pull requests
    • Builds Docker image
    • Pushes to Docker Hub or GitHub Container Registry
    • Deploys to a staging environment
  5. Monitoring: Add health check endpoints and logging configuration.
  6. Load Testing: Use Locust or similar tool to test your application under load.

Starter Code for GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy FastAPI Application

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: test_db
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
    - uses: actions/checkout@v3
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pytest pytest-cov
    - name: Run tests
      env:
        DATABASE_URL: postgresql://test:test@localhost:5432/test_db
      run: |
        pytest --cov=app --cov-report=xml
    - name: Upload coverage
      uses: codecov/codecov-action@v3

  build-and-deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    - name: Build Docker image
      run: docker build -t myapp:${{ github.sha }} .
    - name: Push to registry
      run: |
        echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
        docker tag myapp:${{ github.sha }} myapp:latest
        docker push myapp:${{ github.sha }}
        docker push myapp:latest
    - name: Deploy to staging
      run: |
        # Add your deployment commands here
        echo "Deploying to staging environment..."

Expected Outcomes:

  • Your application should run in three different environments with appropriate configurations
  • Docker builds should complete in under 5 minutes
  • Tests should pass with at least 80% code coverage
  • Health checks should respond within 200ms
  • The application should handle at least 1000 concurrent users

Common Interview Questions

Here are common deployment-related interview questions with detailed answers:

Q1: How do you handle environment-specific configurations in FastAPI?

A: Use Pydantic’s BaseSettings with .env files. Create a Settings class that reads from environment variables with sensible defaults. Use different .env files for each environment (development, staging, production) and load them using the env_file parameter. Use dependency injection to provide settings throughout the application.

Q2: What’s the difference between uvicorn and gunicorn, and when would you use each?

A: Uvicorn is an ASGI server that runs Python async applications directly. Gunicorn is a WSGI server that can manage multiple worker processes. For FastAPI, you typically use uvicorn with gunicorn as a process manager: gunicorn -k uvicorn.workers.UvicornWorker. Use uvicorn alone for development, and gunicorn with uvicorn workers for production to get better process management and graceful shutdowns.

Q3: How do you handle database migrations in production?

A: Use Alembic for database migrations. Create migration scripts that are version-controlled. Run migrations as part of your deployment process, typically before starting the new application version. Use automated tools like Flyway or custom scripts in your CI/CD pipeline. Always test migrations on a staging environment first.

Q4: What strategies do you use for zero-downtime deployments?

A: Common strategies include: (1) Blue-green deployment – maintain two identical environments and switch traffic between them. (2) Rolling updates – gradually replace old instances with new ones. (3) Canary releases – deploy to a small subset of users first. (4) Use load balancers to manage traffic routing. Implement health checks to ensure new instances are ready before routing traffic.

Q5: How do you monitor a FastAPI application in production?

A: Implement multiple monitoring layers: (1) Application-level – add logging with structured logging (JSON format), health check endpoints, and metrics endpoints. (2) Infrastructure monitoring – use tools like Prometheus for metrics collection and Grafana for visualization. (3) APM tools – use Datadog, New Relic, or Elastic APM for detailed performance monitoring. (4) Error tracking – integrate Sentry or similar tools for error reporting. (5) Uptime monitoring – use services like Pingdom or UptimeRobot.

Q6: Explain the concept of CORS and how to handle it in FastAPI.

A: CORS (Cross-Origin Resource Sharing) is a security mechanism that controls which domains can access your API. In FastAPI, use the CORSMiddleware to configure allowed origins, methods, and headers. For production, specify exact origins instead of using “*”. Consider using environment variables to manage CORS settings per environment.

Q7: How do you secure a FastAPI application in production?

A: Implement multiple security layers: (1) HTTPS with valid SSL certificates. (2) Authentication and authorization using JWT tokens. (3) Rate limiting to prevent abuse. (4) Input validation and sanitization. (5) Security headers (CSP, X-Frame-Options, etc.). (6) Use environment variables for secrets. (7) Regular dependency updates. (8) Implement proper logging for security events. (9) Use a Web Application Firewall (WAF) for additional protection.

Q8: What are the best practices for Dockerizing a FastAPI application?

A: Use multi-stage builds to keep the final image small. Use a non-root user for security. Implement health checks. Use environment variables for configuration. Pin dependency versions. Use .dockerignore to exclude unnecessary files. Use Alpine-based images when possible. Implement proper logging that goes to stdout/stderr. Use Docker Compose for local development with all services.

Q9: How do you handle background tasks in a deployed FastAPI application?

A: For simple tasks, use FastAPI’s BackgroundTasks. For complex tasks, use a task queue like Celery with Redis or RabbitMQ. Deploy separate worker processes for background tasks. Monitor task queues for failures and retries. Use task prioritization for critical operations. Implement proper error handling and logging for background tasks.

Q10: What’s your approach to testing a deployed FastAPI application?

A: Implement multiple testing layers: (1) Unit tests for individual components. (2) Integration tests for API endpoints. (3) End-to-end tests for critical workflows. (4) Load testing with tools like Locust or k6. (5) Security testing with tools like OWASP ZAP. (6) Contract testing for API consumers. (7) Canary testing in production. Use separate test databases and mock external services. Automate tests in CI/CD pipeline.

Remember, deployment is an ongoing process that requires continuous monitoring, optimization, and updates. Always have rollback plans and practice disaster recovery procedures regularly.

Leave a Reply

Your email address will not be published. Required fields are marked *