Dockerizing Your FastAPI Application

Docker has become the standard for deploying modern applications. Combined with FastAPI’s speed and simplicity, you get a powerful foundation for building production-ready APIs. This guide walks you through containerizing a FastAPI application from scratch.

Prerequisites

  • Docker installed on your machine
  • Basic understanding of FastAPI
  • Python 3.11+ (we’ll use uv for package management)

Project Structure

Let’s start with a simple project structure:

fastapi-docker/
├── app/
│ ├── __init__.py
│ └── main.py
├── pyproject.toml
├── Dockerfile
└── compose.yaml

Creating the FastAPI Application

First, let’s create a simple FastAPI app:

# app/main.py
from fastapi import FastAPI

app = FastAPI(title="My API", version="1.0.0")

@app.get("/")
def root():
return {"message": "Hello from Docker!"}

@app.get("/health")
def health_check():
return {"status": "healthy"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "query": q}

And the project configuration:

# pyproject.toml
[project]
name = "fastapi-docker"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Writing the Dockerfile

Here’s a production-ready Dockerfile using multi-stage builds and uv:

# Dockerfile
FROM python:3.12-slim AS builder

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Set working directory
WORKDIR /app

# Copy dependency files
COPY pyproject.toml uv.lock* ./

# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev

# Copy application code
COPY app ./app

# Sync the project
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev

# Production stage
FROM python:3.12-slim

WORKDIR /app

# Copy virtual environment from builder
COPY --from=builder /app/.venv /app/.venv

# Copy application code
COPY --from=builder /app/app ./app

# Set environment variables
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

# Expose port
EXPOSE 8000

# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Understanding the Dockerfile

Let’s break down the key parts:

Multi-stage Build

We use two stages:

  1. Builder stage: Installs dependencies and creates the virtual environment
  2. Production stage: Contains only the runtime essentials

This reduces the final image size significantly.

Using uv

We copy uv directly from its official image, which is faster than installing via pip. The --mount=type=cache directive caches downloaded packages between builds.

Environment Variables

  • PYTHONUNBUFFERED=1: Ensures Python output is sent straight to the terminal
  • PYTHONDONTWRITEBYTECODE=1: Prevents Python from writing .pyc files

Building and Running

Build the Docker image:

docker build -t fastapi-app .

Run the container:

docker run -d -p 8000:8000 --name my-api fastapi-app

Test it:

curl http://localhost:8000
# {"message":"Hello from Docker!"}

curl http://localhost:8000/health
# {"status":"healthy"}

Using Docker Compose

For more complex setups with databases and other services, use Docker Compose:

# compose.yaml
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:password@db:5432/mydb
depends_on:
db:
condition: service_healthy
restart: unless-stopped

db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5

volumes:
postgres_data:

Start all services:

docker compose up -d

View logs:

docker compose logs -f api

Stop services:

docker compose down

Development with Hot Reload

For development, you want hot reload when code changes. Create a separate compose file:

# compose.dev.yaml
services:
api:
build: .
ports:
- "8000:8000"
volumes:
- ./app:/app/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

Run with:

docker compose -f compose.dev.yaml up

Now changes to your code will automatically reload the server.

Production Best Practices

1. Use a Non-Root User

Add this to your Dockerfile for better security:

# Create non-root user
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

2. Add Health Checks

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1

3. Use .dockerignore

Create a .dockerignore file to exclude unnecessary files:

__pycache__
*.pyc
*.pyo
.git
.gitignore
.env
.venv
*.md
.pytest_cache
.mypy_cache
.ruff_cache

4. Pin Your Base Image

Instead of python:3.12-slim, use a specific digest:

FROM python:3.12-slim@sha256:...

This ensures reproducible builds.

5. Use Gunicorn in Production

For production, use Gunicorn with Uvicorn workers:

CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]

Add gunicorn to your dependencies:

dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"gunicorn>=23.0.0",
]

Complete Production Dockerfile

Here’s the complete production-ready Dockerfile with all best practices:

FROM python:3.12-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

COPY pyproject.toml uv.lock* ./

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev

COPY app ./app

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev

FROM python:3.12-slim

RUN useradd --create-home --shell /bin/bash appuser

WORKDIR /app

COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appuser /app/app ./app

ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1

CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]

Conclusion

Containerizing your FastAPI application with Docker provides consistency across environments, simplified deployments, and easy scaling. The combination of multi-stage builds, uv for fast dependency installation, and proper production configurations gives you a solid foundation for deploying your APIs.

Key takeaways:

  • Use multi-stage builds to minimize image size
  • Use uv for fast, reproducible dependency installation
  • Run as non-root user in production
  • Add health checks for container orchestration
  • Use Gunicorn with Uvicorn workers for production workloads

   Reprint policy


《Dockerizing Your FastAPI Application》 by Isaac Zhou is licensed under a Creative Commons Attribution 4.0 International License
  TOC