Docker & Cloud Deployment¶
A FastAPI app on your laptop is fun. A FastAPI app anyone in the world can hit is useful. This chapter packages your app in Docker and deploys it.
Why Docker?¶
The problem: "It works on my machine." The fix: Ship the entire environment (Python version, packages, OS bits, your code) as one image. Run it anywhere — laptop, AWS, GCP, Azure, your friend's server.
Docker container = a tiny, isolated Linux machine with just your app + its deps.
Install Docker¶
Download Docker Desktop for Mac / Windows. Linux: sudo apt install docker.io.
Verify:
Step 1 — Pin your dependencies¶
In your project folder:
Inspect the file. Should look like:
Pin everything. Floating versions = "works today, broken tomorrow."
Step 2 — Write a Dockerfile¶
In your project folder, create a file named exactly Dockerfile (no extension):
# Use an official slim Python base
FROM python:3.12-slim
# Set the working dir inside the container
WORKDIR /app
# Install deps first (cached layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy app code
COPY . .
# uvicorn listens on port 8000 inside the container
EXPOSE 8000
# Run the server
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Note:
--host 0.0.0.0(not127.0.0.1) — otherwise the container only accepts connections from itself.
Step 3 — Add .dockerignore¶
Speeds up the build and keeps secrets out of the image:
Step 4 — Build the image¶
-t = tag (name) the image. . = build context = current directory.
You'll see steps:
[+] Building 25.0s
=> CACHED [1/5] FROM python:3.12-slim
=> [2/5] WORKDIR /app
=> [3/5] COPY requirements.txt .
=> [4/5] RUN pip install --no-cache-dir -r requirements.txt
=> [5/5] COPY . .
=> exporting to image
Confirm:
Step 5 — Run the container¶
-d— detached (background).-p 8000:8000— map host port 8000 → container port 8000.--name fapi— name the container.
Visit http://127.0.0.1:8000 — your app served from the container.
docker ps # list running containers
docker logs -f fapi # live logs
docker stop fapi # stop
docker rm fapi # remove
docker rmi my-fastapi-app # delete the image
Step 6 — Environment variables / secrets¶
Don't hard-code secrets in the Dockerfile. Pass them at runtime:
docker run -d -p 8000:8000 \
-e SECRET_KEY="long-random-string" \
-e DATABASE_URL="postgresql://user:pwd@host/db" \
--name fapi my-fastapi-app
Inside Python:
Or via a .env file + python-dotenv. Don't commit .env to git.
Step 7 — Use docker-compose for multi-service apps¶
Real apps have a database, a cache, maybe Redis. docker-compose.yml describes them all:
# docker-compose.yml
version: "3.9"
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Run everything:
api can reach the DB at hostname db because docker-compose creates a virtual network.
Step 8 — Deploy to the cloud¶
Option A — Render (easiest, free tier)¶
- Push your repo to GitHub (include the Dockerfile).
- Sign up at render.com.
- New → Web Service → connect your repo.
- Render auto-detects the Dockerfile.
- Set env vars under Environment.
- Click Create Web Service.
You get a public HTTPS URL like https://my-fastapi-app.onrender.com in a few minutes.
Option B — AWS (EC2 + Docker)¶
- Spin up an EC2 instance (Ubuntu).
- SSH in:
ssh -i key.pem ubuntu@<public-ip> - Install Docker:
- Pull or build the image on the server.
- Run:
- Open ports 80/443 in the EC2 security group.
- (Recommended) Put nginx in front for HTTPS via Let's Encrypt.
Option C — Google Cloud Run¶
# Tag for GCR
docker tag my-fastapi-app gcr.io/PROJECT-ID/my-fastapi-app
# Push
docker push gcr.io/PROJECT-ID/my-fastapi-app
# Deploy
gcloud run deploy my-fastapi-app \
--image gcr.io/PROJECT-ID/my-fastapi-app \
--region us-central1 \
--allow-unauthenticated
Cloud Run auto-scales to zero when idle. You pay only when traffic hits.
Production hardening checklist¶
- Don't run as root — add
RUN useradd -m app && USER appin the Dockerfile. - Multi-worker uvicorn —
uvicorn main:app --workers 4(or use gunicorn + uvicorn workers). - HTTPS only — put nginx, Caddy, or a managed load balancer in front.
- Health check endpoint —
/healththat returns 200 only if dependencies are reachable. Docker can use it:HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1. - Structured logging — JSON logs to stdout.
loguruor stdlibloggingwith a JSON formatter. - Don't bake secrets into the image — env vars or a secrets manager.
- CDN / Cache static stuff — Cloudflare, AWS CloudFront.
- Image scanning —
docker scan my-fastapi-appfor CVEs. - Pin everything — Python version, package versions.
- CI/CD — auto-build & deploy on every push to
main.
Multi-stage Dockerfile (smaller images)¶
# Stage 1 — build deps
FROM python:3.12 AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2 — runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
The final image only has the runtime — no build tools, smaller surface, faster pulls.
Common pitfalls¶
- ❗
--host 127.0.0.1inside Docker — container can't be reached from outside. Always0.0.0.0. - ❗ Forgetting
-p 8000:8000— container runs but no port is exposed. - ❗ Huge images — comes from using
python:3.12(full) instead of:3.12-slim. Switch. - ❗ Pip install layer not cached — copy
requirements.txtBEFORE the rest of your code so Docker only re-runspip installwhen deps change. - ❗ Logs going to a file inside the container — they disappear on restart. Log to stdout.
- ❗ No graceful shutdown — uvicorn handles SIGTERM, but if you proxy you need to forward the signal.
- ❗ Burning a public IP without HTTPS — bad. Use a managed proxy or Let's Encrypt.
What you've learned¶
You now have a complete FastAPI tutorial from "Hello, World" to production deployment:
- API basics + the request lifecycle.
- Path / query / body parameters + Pydantic validation.
- Status codes, response models, error handling.
- A complete CRUD app with file-backed storage.
- Pydantic deep dive — validators, computed fields, serializers.
- Async / await, when to use it.
- Dependency injection — clean reusable logic.
- Middleware + CORS.
- Authentication with OAuth2 + JWT.
- Deploying an ML model as an API.
- Docker + cloud deployment.
You're ready to ship. Start small, iterate, monitor in production, and grow.
Practice¶
What does this print?
Expected: 8000
Use multi-stage Docker builds to keep images small
Expected: True
Quiz — Quick check¶
What you remember
Q1. Why use multi-stage Docker builds?
- Separates build deps (compilers, dev tools) from the runtime image — smaller final image, smaller attack surface
- Faster builds
- Required by Docker
- For caching only
Why: Stage 1: install deps, build wheels. Stage 2: copy only the runtime artifacts from stage 1. Final image is just what you need to run — often 10× smaller than single-stage.
Q2. How do you run FastAPI with multiple worker processes in production?
-
uvicorn main:app --workers 4(or gunicorn + uvicorn workers) -
python main.py -
--reload - In a single process
Why: Workers = parallel Python processes. Each handles its own requests independently. Rule of thumb: workers = CPU cores. For better process management, use gunicorn with uvicorn worker class.
Q3. What's the standard health check endpoint pattern?
-
/healthor/healthzreturning 200 with minimal logic — used by load balancers and orchestrators -
/ -
/api - Optional
Why: Health checks let infrastructure (Kubernetes, ELB, Cloud Run) know if your container is alive. Should be fast (<100ms), minimal (no DB calls usually), and return 200 when healthy. Returning 5xx triggers automatic restarts.
Common doubts¶
Where should I deploy a FastAPI app?
Options ranked by simplicity: Render / Railway / Fly.io (one-click). Vercel/Netlify (works for serverless FastAPI via Mangum). Google Cloud Run (containers, scale to zero). AWS ECS/Fargate (more control). Kubernetes (most control, most complexity). Start simple, scale up as needed.
Should I use uvicorn directly or behind nginx?
For most apps, uvicorn alone is fine. Add nginx if you need TLS termination, static files, rate limiting, or complex routing. On managed platforms (Cloud Run, Render), the platform handles TLS and routing — just expose uvicorn.
How do I handle environment variables?
Use pydantic-settings (Pydantic v2) — define a BaseSettings class with type-validated fields that read from environment vars. Falls back to a .env file in development. Validates at startup, so misconfigured envs fail fast instead of mid-request.