21.11. FastAPI with Docker
Table of Contents
Why Run FastAPI in Docker?
Running FastAPI inside Docker makes your backend:
- Easy to run on any machine that has Docker
- Easy to deploy to servers and cloud platforms
- Isolated from your system Python and other projects
- Reproducible, because everything is defined in code
You define:
- A Dockerfile: how to build an image that can run your FastAPI app.
- A docker-compose.yml (optional, but very common): how to run your app container, and often other services like PostgreSQL or Redis.
In this chapter we focus only on FastAPI inside Docker, not on databases or larger stacks. Those come later in other chapters.
Minimal FastAPI App to Containerize
To keep examples clear, we will use a very small FastAPI app.
Imagine this file structure:
fastapi-docker-example/
├─ app/
│ ├─ main.py
│ └─ __init__.py
└─ requirements.txt
app/main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from Dockerized FastAPI!"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
requirements.txt:
fastapi==0.111.0
uvicorn[standard]==0.30.0You can adjust versions, but the structure stays the same.
Basic FastAPI Dockerfile
A Dockerfile describes how to build an image that can run your app.
Create a file named Dockerfile in the project root:
# 1. Use an official Python runtime as a parent image
FROM python:3.12-slim
# 2. Set working directory inside the container
WORKDIR /app
# 3. Install system dependencies (optional, but common)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# 4. Copy dependency file first (for better build caching)
COPY requirements.txt .
# 5. Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# 6. Copy the FastAPI application code
COPY app ./app
# 7. Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# 8. Expose the port that Uvicorn will listen on
EXPOSE 8000
# 9. Command to run the application with Uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Key ideas:
FROM python:3.12-slimchooses a small base image with Python 3.12.WORKDIR /appsets the working directory inside the container.COPY requirements.txt .thenRUN pip install ...lets Docker reuse the dependency layer when only code changes.COPY app ./appcopies your code.CMD [...]specifies how the container starts the app.
Important rule: Always run your FastAPI app with --host 0.0.0.0 inside Docker.
If you use the default 127.0.0.1, the app will only listen inside the container and will not be reachable from your host.
Building and Running the Container
From the project root, build the image:
docker build -t fastapi-docker-example .-t fastapi-docker-examplegives your image a name (a “tag”).
List images:
docker images
You should see fastapi-docker-example in the repository column.
Run the container:
docker run -d --name fastapi-docker -p 8000:8000 fastapi-docker-exampleExplanation:
| Option | Meaning |
|---|---|
-d | Run in detached mode, in the background |
--name fastapi-docker | Name the container for easy reference |
-p 8000:8000 | Map host port 8000 to container port 8000 |
fastapi-docker-example | Image to run |
Now open:
- http://localhost:8000
- http://localhost:8000/docs (FastAPI Swagger UI)
- http://localhost:8000/items/123
To see logs:
docker logs -f fastapi-dockerTo stop and remove the container:
docker stop fastapi-docker
docker rm fastapi-dockerHandling Code Changes During Development
Rebuilding the image after every small code change is slow.
For development you usually:
- Mount your source code into the container with a volume.
- Use
uvicornwith--reload.
Update the Dockerfile to use a development style entrypoint:
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy only requirements here. Code will be mounted at runtime in dev.
# (You can still COPY app ./app for production builds.)
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
EXPOSE 8000
# Use reload in development
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
Then run the container and mount your local app directory:
docker run -d \
--name fastapi-dev \
-p 8000:8000 \
-v "$(pwd)/app:/app/app" \
fastapi-docker-example
Now if you change a file in app/ on your host, FastAPI reloads automatically inside the container.
Important rule: Use --reload only in development.
For production, use a stable process, for example Uvicorn workers managed by Gunicorn, without auto-reload.
Environment Variables in Docker
Backend apps often need configuration like:
- Database URLs
- Secrets
- Debug flags
- External service URLs
In Docker you usually pass these as environment variables.
Modify app/main.py to read an environment variable:
import os
from fastapi import FastAPI
app = FastAPI()
GREETING = os.getenv("GREETING", "Hello")
@app.get("/")
def read_root():
return {"message": f"{GREETING} from Dockerized FastAPI!"}Run with custom environment:
docker run -d \
--name fastapi-env \
-p 8000:8000 \
-e GREETING="Hi" \
fastapi-docker-exampleOr load from a file:
Create env.list:
GREETING=HowdyRun:
docker run -d \
--name fastapi-env-file \
--env-file env.list \
-p 8000:8000 \
fastapi-docker-example
In production, do not bake secrets directly into the image. Pass them at runtime with -e, --env-file, Docker Compose, or secret managers.
Multi-Stage Builds for Smaller Images
A common pattern is a multi-stage build.
You build dependencies in one stage, then copy only what you need into a small final image.
Example:
# Stage 1: build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
# Stage 2: runtime image
FROM python:3.12-slim
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /install /usr/local
# Copy application code
COPY app ./app
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Here, the final image:
- Reuses dependencies built in the first stage
- Does not contain build tools like
build-essential
This is useful when your dependencies need compilation.
Running FastAPI with Gunicorn and Uvicorn Workers
For real production you often use Gunicorn with Uvicorn workers.
Install extra packages in requirements.txt:
fastapi==0.111.0
uvicorn[standard]==0.30.0
gunicorn==22.0.0A simple Gunicorn command:
gunicorn -k uvicorn.workers.UvicornWorker app.main:app -b 0.0.0.0:8000 -w 4
Update Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
EXPOSE 8000
# Use Gunicorn with Uvicorn workers
CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000", "-w", "4"]You can control worker count with an environment variable:
ENV WORKERS=4
CMD ["sh", "-c", "gunicorn app.main:app -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 -w ${WORKERS}"]Then run:
docker run -d \
--name fastapi-prod \
-p 8000:8000 \
-e WORKERS=2 \
fastapi-docker-exampleImportant guideline: Keep application configuration (like number of workers) controlled by environment variables, not hardcoded, so you can tune it per environment without rebuilding images.
Using Docker Compose with FastAPI
Although full multi-container setups are handled in other chapters, a simple docker-compose.yml is common even for a single FastAPI service.
Example docker-compose.yml:
version: "3.9"
services:
api:
build: .
container_name: fastapi-compose
ports:
- "8000:8000"
environment:
GREETING: "Hello from Docker Compose"
volumes:
- ./app:/app/app
restart: unless-stoppedThen run:
docker compose up --build- The app is reachable at
http://localhost:8000. - When you change code in
app/, FastAPI reloads (if you used--reloadin your CMD). restart: unless-stoppedrestarts the container if it crashes.
Stop:
docker compose downCommon Gotchas and Tips
1. Container starts then exits immediately
Check the logs:
docker logs fastapi-dockerCommon causes:
- Wrong module path in
uvicornorgunicorncommand, for examplemain:appinstead ofapp.main:app. - Missing dependency because
requirements.txtis not copied correctly.
2. “Connection refused” when opening localhost:8000
Check:
- Did you use
--host 0.0.0.0inside the container? - Did you map the port? For example
-p 8000:8000. - Is the container still running?
docker ps.
3. File changes not reflected
- In development, ensure you mounted the directory:
-v "$(pwd)/app:/app/app". - Ensure
--reloadis enabled in the FastAPI server command. - If using Docker Compose, check the
volumes:section.
4. Timezone differences
Containers often run in UTC. For logs and timestamps, always treat server time as UTC and convert in the frontend or client when needed.
Summary
In this chapter you learned how to:
- Write a
Dockerfilefor a basic FastAPI application. - Build and run a FastAPI Docker image with
docker buildanddocker run. - Use volume mounts and
--reloadfor fast development. - Pass configuration through environment variables.
- Use multi-stage builds to keep images smaller.
- Run FastAPI with Gunicorn and Uvicorn workers in a production style.
- Use a simple Docker Compose configuration to run your FastAPI service.
These skills are the foundation for the upcoming chapters where you add databases, Redis, and other services, and deploy complete backend stacks.
Views: 8
KAHIBARO