How do you deploy a FastAPI app on AWS EC2?

You deploy a FastAPI app on AWS EC2 by running it in a Docker container with Uvicorn, then putting Nginx in front of it as a reverse proxy. Certbot adds a free HTTPS certificate, and the EC2 security group only allows web traffic on ports 80 and 443. The whole setup fits on one small Ubuntu instance.

A reverse proxy is a server that receives public requests and forwards them to your app running on a private port. Uvicorn is an ASGI server, the program that actually runs your FastAPI code. Docker packages the app and its Python dependencies into an image that runs the same way everywhere.

What do you need before you start?

You need an EC2 instance running a current Ubuntu LTS release, a domain name, and SSH access. Point an A record for your domain (for example api.example.com) at the instance's public IP. An Elastic IP keeps that address fixed if the instance restarts.

Install Docker, the Compose plugin and Nginx on the server. The steps below use the docker.io and docker-compose-v2 packages from Ubuntu's own repositories. Docker's official apt repository also works if you prefer its newer releases.

sudo apt update
sudo apt install -y docker.io docker-compose-v2 nginx
sudo systemctl enable --now docker
sudo usermod -aG docker $USER   # log out and back in after this

How do you write the Dockerfile for FastAPI?

The Dockerfile below builds a small image from python:3.12-slim and starts Uvicorn on port 8000. It copies requirements.txt first so Docker can cache the dependency layer between builds.

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

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

COPY . .

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips", "*"]

Make sure requirements.txt includes fastapi and uvicorn. The --proxy-headers flag tells Uvicorn to trust the X-Forwarded-* headers from Nginx, so your app sees the real client IP and https scheme. Allowing all forwarded IPs is safe here only because the container port is not reachable from the internet. Add a .dockerignore file listing .env, .git and __pycache__ so secrets and clutter stay out of the image.

How do you run the container so it survives crashes and reboots?

Docker Compose describes the container in one file and applies a restart policy. Save this as compose.yaml next to the Dockerfile.

services:
  api:
    build: .
    env_file: .env
    ports:
      - "127.0.0.1:8000:8000"
    restart: unless-stopped

Start it with docker compose up -d --build. The 127.0.0.1: prefix binds the port to localhost only, so the app is reachable by Nginx but not directly from the internet. restart: unless-stopped brings the container back after a crash or a server reboot.

The same result without Compose is one command:

docker build -t fastapi-app .
docker run -d --name api --restart unless-stopped \
  --env-file .env -p 127.0.0.1:8000:8000 fastapi-app

How do you configure Nginx as a reverse proxy?

Nginx receives requests on port 80 (and later 443) and forwards them to the container on port 8000. Create /etc/nginx/sites-available/api with this server block.

server {
    listen 80;
    server_name api.example.com;

    client_max_body_size 10m;

    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;

        # Optional: only needed if your app uses WebSockets
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_read_timeout 120s;
    }
}

Enable the site and reload Nginx:

sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

nginx -t checks the config for errors before you reload. The longer proxy_read_timeout helps with slow endpoints, such as ones that wait on an LLM. If you use WebSockets alongside normal requests, the Nginx docs describe a map block that sets Connection only when an upgrade is requested.

How do you add HTTPS with Certbot?

Certbot is a free tool from the Electronic Frontier Foundation (EFF) that gets TLS certificates from Let's Encrypt. The --nginx plugin edits your server block for you and adds an HTTP-to-HTTPS redirect.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com
sudo certbot renew --dry-run

Certbot installs a systemd timer that renews certificates automatically before they expire. The --dry-run command confirms renewal will work.

Which ports should the EC2 security group open?

The EC2 security group should allow only the traffic your server needs. Port 8000 must never be open to the world, because Nginx is the only public entry point.

Port Protocol Source Purpose
22 TCP Your IP only SSH access
80 TCP 0.0.0.0/0 and ::/0 HTTP, redirects to HTTPS and Certbot checks
443 TCP 0.0.0.0/0 and ::/0 HTTPS traffic
8000 TCP Not open FastAPI container, reachable only through Nginx

How should you handle secrets, logs and updates?

Secrets. Keep API keys, database URLs and tokens in a .env file on the server with chmod 600 .env. Compose loads it through env_file. Never commit it to Git or copy it into the image. For larger setups, AWS Systems Manager Parameter Store or AWS Secrets Manager can hold secrets centrally.

Logs. Docker captures everything your app prints to stdout and stderr. Read it with docker compose logs -f api. Nginx writes to /var/log/nginx/access.log and /var/log/nginx/error.log. A 502 Bad Gateway in the browser almost always means the container is down or listening on the wrong port, so check the container logs first.

Updates. Pull the new code and rebuild:

git pull
docker compose up -d --build

Compose builds the new image while the old container keeps serving, then swaps containers. Downtime is usually a few seconds while the new container starts. For true zero downtime, run two containers on different ports behind an Nginx upstream block and restart them one at a time.

How does this compare to PM2 for Node.js apps?

PM2 is the equivalent process manager for Node.js apps. It restarts crashed processes, starts them on boot with pm2 startup, and collects logs, which is the same job Docker's restart policy does here. Nginx and Certbot work exactly the same in front of either. I use Docker, PM2 and Nginx across projects, including the no-code AI bot framework.

Summary

A reliable FastAPI deployment on EC2 is Uvicorn in a Docker container bound to localhost, Nginx as the reverse proxy, Certbot for HTTPS, and a security group that opens only ports 80 and 443. Keep secrets in an .env file and read logs with docker compose logs. If you are still choosing a framework, see FastAPI vs Express, and for help with your own backend, see Python backend systems.

Need this built? See Python & backend systems or get in touch.

FAQ

Questions about this topic

Do I need Nginx if Uvicorn can serve HTTP directly?

Uvicorn can serve traffic directly, but Nginx handles HTTPS, request buffering, timeouts and multiple sites on one server more cleanly. It is the standard front door for Python apps on a VPS.

Which Ubuntu version should I use on EC2?

Use a current Ubuntu LTS image from the EC2 launch wizard. LTS releases get security updates for years, and the commands in this guide work on them.

How do I run Docker commands without sudo?

Add your user to the docker group with sudo usermod -aG docker $USER, then log out and back in. Be aware that the docker group has root-level access to the machine.

What is the Node.js equivalent of this setup?

For Node.js apps, PM2 is the common process manager. It restarts the app on crashes, starts it on boot and collects logs, and Nginx sits in front the same way.

Keep reading

More on Python & backend systems

Have a bot, a backend or a strategy in mind?

Tell me what you want to build and where you are with it. Send a few lines about the project and I’ll reply with questions and next steps.

Rajshahi, Bangladesh