How to deploy a FastAPI app on AWS EC2 with Docker and Nginx
To deploy a FastAPI app on AWS EC2, package it in a Docker image that runs Uvicorn, start the container with a restart policy bound to localhost, and put Nginx in front as a reverse proxy. Then add HTTPS with Certbot and open only ports 80 and 443 in the security group. Updates are a rebuild and a quick container swap.
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.
By