Skip to content

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:

docker --version
docker run hello-world

Step 1 — Pin your dependencies

In your project folder:

pip freeze > requirements.txt

Inspect the file. Should look like:

fastapi==0.115.5
uvicorn==0.32.0
pydantic==2.9.2
scikit-learn==1.5.2
pandas==2.2.3

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 (not 127.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:

__pycache__
*.pyc
.venv
.git
.gitignore
*.log
.env
.pytest_cache
.DS_Store
notebooks/
data/raw/

Step 4 — Build the image

docker build -t my-fastapi-app .

-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:

docker images

Step 5 — Run the container

docker run -d -p 8000:8000 --name fapi my-fastapi-app
  • -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:

import os
SECRET_KEY = os.environ["SECRET_KEY"]

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:

docker-compose up -d
docker-compose logs -f
docker-compose down            # stop all

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)

  1. Push your repo to GitHub (include the Dockerfile).
  2. Sign up at render.com.
  3. NewWeb Service → connect your repo.
  4. Render auto-detects the Dockerfile.
  5. Set env vars under Environment.
  6. 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)

  1. Spin up an EC2 instance (Ubuntu).
  2. SSH in: ssh -i key.pem ubuntu@<public-ip>
  3. Install Docker:
    sudo apt update && sudo apt install -y docker.io
    sudo usermod -aG docker $USER && newgrp docker
    
  4. Pull or build the image on the server.
  5. Run:
    docker run -d -p 80:8000 --restart=always --name fapi my-fastapi-app
    
  6. Open ports 80/443 in the EC2 security group.
  7. (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 app in the Dockerfile.
  • Multi-worker uvicornuvicorn main:app --workers 4 (or use gunicorn + uvicorn workers).
  • HTTPS only — put nginx, Caddy, or a managed load balancer in front.
  • Health check endpoint/health that 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. loguru or stdlib logging with a JSON formatter.
  • Don't bake secrets into the image — env vars or a secrets manager.
  • CDN / Cache static stuff — Cloudflare, AWS CloudFront.
  • Image scanningdocker scan my-fastapi-app for 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.1 inside Docker — container can't be reached from outside. Always 0.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.txt BEFORE the rest of your code so Docker only re-runs pip install when 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

# Common FastAPI dev port
default_port = 8000
print(default_port)

Use multi-stage Docker builds to keep images small

Expected: True

dockerfile_stages = 1            # bug: single stage = bloated image with dev tools
is_multi_stage = dockerfile_stages > 1
print(not is_multi_stage)

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?

  • /health or /healthz returning 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.

← Back to FastAPI home