Deploy FastAPI with Docker and Nginx
Learn how to containerize FastAPI with Docker and set up Nginx as a reverse proxy for production. This lesson covers Dockerfile setup, docker-compose for Nginx and API services, and practical troubleshooting for smooth deployment.
Focus: deploying fastapi with docker and nginx
You've built a beautiful FastAPI app. Tests pass, the docs render, and everything works on your laptop. But when you try to uvicorn app.main:app --host 0.0.0.0 --port 8000 on a server, your API is fragile, hard to scale, and vulnerable to slowloris attacks. Here's the pain: your app is a monolith living inside a single Python process, with no process manager, no container boundaries, and no reverse proxy to handle TLS, compression, and load balancing. That's where deploying FastAPI with Docker and Nginx comes in — a battle-tested stack that turns your toy app into a production-grade service. By the end of this lesson, you'll containerize your API with Docker and put Nginx in front of it as a reverse proxy, ready for real traffic.
The problem this lesson solves
Before you learn the how, let's nail the why. A bare uvicorn process is fine for development but collapses under production pressure:
- No process isolation — a crash takes down the whole API, and you can't easily roll back to a known-good version.
- FastAPI runs as a single worker by default — one CPU core, one process, so your multi-core server is underutilized.
- No automatic restart —
uvicorncrashes and your API stays dead until a human intervenes. - No native TLS — you'd have to wire up
--ssl-keyfileand--ssl-certfilemanually, and managing certificates is a nightmare. - No static asset handling — Nginx is far better at serving static files or gzipping responses than Python ever will be.
The result? An API that's slow, fragile, and a security hazard. Docker solves the isolation and reproducibility problem by bundling your app with its exact runtime, and Nginx solves the production-hardening problem by standing in front of your API as a reverse proxy. Together, they give you a deployable unit that runs identically on your laptop, a test server, and AWS EC2.
Core concept / mental model
Think of your deployment as a three-layer cake:
- The App Layer — your FastAPI code, Pydantic models, and routes. This lives inside a Docker container.
- The Runtime Layer — the Python interpreter plus your installed dependencies, captured in a
Dockerfile. - The Edge Layer — Nginx, which listens on port 80 (or 443 for HTTPS) and forwards / proxies requests to your FastAPI container's internal port (e.g., 8000).
Here's the flow: a user's browser sends GET /api/items to your server's IP. Nginx (running on the host) receives it, inspects the Host header, and forwards the request to the FastAPI container's internal network. FastAPI processes it, returns a JSON response, and Nginx sends that back to the client — while optionally adding security headers, compression, and caching.
Pro tip: In this model, Nginx is your public face; your FastAPI container is never directly exposed to the internet. This gives you a single choke point for security, TLS termination, and load balancing — a mental model every senior engineer spins up instinctively.
How it works step by step
Let's break down the two moves you'll make: containerizing FastAPI and configuring Nginx as a reverse proxy.
Step 1: Create a Dockerfile
The Dockerfile is a recipe that tells Docker how to build an image. For FastAPI, you typically start from the official Python image, install dependencies, and copy your code. Use a pinned Python version for reproducibility (e.g., python:3.12-slim), and always use a non-root user for security.
Step 2: Write a docker-compose.yml
Docker Compose lets you define and run multi-container apps with one command. For this lesson, you'll define two services: api (your FastAPI container) and nginx (the reverse proxy). The nginx service depends on api, and both share a private Docker network that Nginx uses to reach the API.
Step 3: Configure Nginx
Nginx needs a config file that tells it to listen on port 80 and proxy requests to your api service. Inside the Docker network, you can reference the service by its Compose name (e.g., http://api:8000), because Docker provides built-in DNS resolution between containers.
Step 4: Build and run
Run docker compose up --build -d. Docker builds the images, creates the network, starts the containers, and you're live. The -d flag runs services in the background.
The cause-and-effect chain: Docker gives you a consistent runtime → Nginx gives you a production-grade entry point → your API scales cleanly and survives crashes.
Hands-on walkthrough
Now let's get our hands dirty. We'll build a minimal FastAPI app, containerize it, and wrap it in Nginx.
1. Your FastAPI project
Create a project folder and a simple app:
# app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from FastAPI"}
@app.get("/health")
def health_check():
return {"status": "ok"}
Save dependencies in requirements.txt:
fastapi==0.115.0
uvicorn[standard]==0.30.0
2. Dockerfile
Create a Dockerfile in the project root:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
RUN useradd -m appuser && chown -R appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Key points:
- We copy requirements.txt before the app code to leverage Docker layer caching — when you change code but not requirements, the image build is instant.
- The --host 0.0.0.0 flag makes Uvicorn listen on all interfaces inside the container, so Nginx can reach it.
- Running as a non-root user reduces security risk.
3. Nginx configuration
Create nginx/nginx.conf:
server {
listen 80;
server_name _; # Accept any hostname in dev
location / {
proxy_pass http://api: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;
}
}
The proxy_pass line is the magic: it forwards all requests to the api container on port 8000. The proxy_set_header lines preserve the original client IP and protocol, which your FastAPI app needs for logging and debugging.
4. Docker Compose
Create docker-compose.yml:
version: "3.8"
services:
api:
build: .
container_name: fastapi-app
expose:
- "8000"
nginx:
image: nginx:1.27-alpine
container_name: nginx-proxy
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
Note that we don't publish the API's port to the host (expose is internal only). The only public entry point is Nginx on port 80. This is the correct design — your FastAPI container is isolated from the outside world.
5. Build and run
$ docker compose up --build -d
$ curl http://localhost/
{"message":"Hello from FastAPI"}
$ curl http://localhost/health
{"status":"ok"}
Your API is now served through Nginx! Try hitting http://localhost/docs — Swagger UI works too because Nginx passes all paths.
Compare options / when to choose what
You now have multiple ways to deploy FastAPI. Here's a quick comparison:
| Approach | Pros | Cons | Best for |
|---|---|---|---|
Bare uvicorn |
Simple, no extra deps | No TLS, no restart, single process | Dev and local testing |
uvicorn + systemd |
Auto-restart, logs, easy to manage | Still no TLS, no isolation | Small internal tools on a single VM |
| Docker + Nginx (this lesson) | Isolation, reverse proxy, TLS-ready, scalable | More moving parts to configure | Production APIs, multi-container apps |
| Docker + Traefik | Auto HTTPS via Let's Encrypt, dynamic config | Different config language, learning curve | Microservices with many services |
| Kubernetes + Ingress | Ultimate scaling and self-healing | Complex to operate | Enterprise, high-traffic platforms |
When to choose Docker + Nginx: This is the sweet spot for most FastAPI backends. It gives you reproducibility, a clean public entry point, and a path to scale horizontally later (just run more api containers behind Nginx's upstream). If you need automatic HTTPS and have only one service, Traefik might save you time. If your team already lives in K8s, skip Nginx and use an Ingress controller.
Troubleshooting & edge cases
Let's solve the most common pitfalls you'll hit when deploying FastAPI with Docker and Nginx.
Error: 502 Bad Gateway from Nginx
This means Nginx can't reach your API container. Common causes:
- Wrong service name in
proxy_pass— Nginx uses the Compose service name as hostname. Double-checkhttp://api:8000matches your service key. - API container crashed — run
docker compose logs apiand inspect for startup errors like missingappmodule. - Port mismatch — your FastAPI container must listen on the port you proxy to (8000). Check
EXPOSEandCMD.
Error: ERR_CONNECTION_REFUSED when curling localhost
First, check if Nginx is actually listening:
$ docker compose ps
If Nginx is not running, look at logs with docker compose logs nginx. Often the config file has a syntax error or the volume mount path is wrong.
Problem: curl works inside Docker network but not from host
Make sure you only expose port 80 on Nginx, not 8000 on the API. If you've accidentally used ports instead of expose on the API, you might conflict with another service.
Gotcha: Uvicorn single worker is a bottleneck
In production, one Uvicorn worker is rarely enough. Run multiple workers by adjusting the CMD:
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Better yet, use Gunicorn as a process manager with Uvicorn workers (see variations below).
Edge case: Static files served by FastAPI are slow
If your app serves images or CSS, let Nginx handle them. Add a location /static { alias /static; } block and mount a shared volume. This offloads Python and speeds up delivery dramatically.
What you learned & what's next
You've conquered the core of deploying FastAPI with Docker and Nginx. You can now:
- Explain why a bare
uvicornprocess fails in production and how Docker + Nginx solves that. - Write a
Dockerfilethat containerizes your FastAPI app reproducibly. - Configure Nginx as a reverse proxy with correct headers and service discovery.
- Use
docker composeto orchestrate both services with one command. - Troubleshoot the classic 502 Bad Gateway and connection-refused errors.
What's next in the track? Now that your API is deployed, the logical next lesson is Implementing CI/CD for FastAPI with GitHub Actions. You'll learn to automate building, testing, and deploying your Docker image every time you push code — closing the loop from commit to production.
Final pro tip: Always pin your base image tags (e.g.,
python:3.12-slim) and never uselatestfor the Nginx image in production — you want deterministic builds, not surprise upgrades.
Go ahead, containerize something real. Your future self deploying at 2 AM will thank you.
Practice recap
Now try it yourself: create a minimal FastAPI app (like the one above), containerize it, and set up Nginx as a reverse proxy. Deploy it to a cloud VM or even your own machine, then run curl http://localhost/ and curl http://localhost/health to verify. To level up, add a restart: always policy and run two api replicas to see Nginx load-balance automatically.
Common mistakes
- Exposing the FastAPI port (8000) to the host while Nginx also listens — creates an open door for attackers; use
exposeinside compose, notports. - Forgetting the
proxy_set_header Host $host;in Nginx — leads to your API seeinglocalhost:8000as the host, breaking any host-based routing or CORS. - Running Uvicorn with a single worker or without a process manager in production — the API dies silently after a crash; use Gunicorn or multiple workers.
- Not using a non-root user in the Dockerfile — containers run as root by default, which is a security vulnerability if an attacker finds a flaw in your app.
Variations
- Use Gunicorn as the process manager with Uvicorn workers:
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorkerfor better reliability. - Add a
docker-composehealthcheck andrestart: alwayspolicy to auto-recover crashed containers. - Replace Nginx with Traefik to get automatic Let's Encrypt HTTPS and a modern dynamic config, reducing setup time for a single-service stack.
Real-world use cases
- Microservice APIs that need isolation and reproducible builds across dev, staging, and prod environments.
- A FastAPI backend serving a single-page app where Nginx terminates TLS, does gzip compression, and proxies API requests.
- Horizontal scaling: run multiple FastAPI containers behind Nginx's
upstreamblock to handle high traffic with no code change.
Key takeaways
- Bare
uvicornis fine for dev, but production needs process isolation, TLS, and multi-core support — solve it with Docker + Nginx. - Docker provides a reproducible runtime; Nginx acts as your public edge, forwarding requests to an internal FastAPI container.
- The
proxy_pass http://api:8000line is the core of Nginx reverse proxy config, using Docker's built-in DNS resolution. - Keep your API container private by using
exposein Compose, and only publish the Nginx port to the host. - Always pin image versions (e.g.,
python:3.12-slim) and run as a non-root user for security. - For production, scale with multiple workers behind Nginx for the best balance of simplicity and performance.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.