KAHIBARO
Discord Login Register

21.11. FastAPI with Docker

Why Run FastAPI in Docker?

Running FastAPI inside Docker makes your backend:

You define:

  1. A Dockerfile: how to build an image that can run your FastAPI app.
  2. 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:

text
fastapi-docker-example/
├─ app/
│  ├─ main.py
│  └─ __init__.py
└─ requirements.txt

app/main.py:

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

text
fastapi==0.111.0
uvicorn[standard]==0.30.0

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

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

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:

bash
docker build -t fastapi-docker-example .

List images:

bash
docker images

You should see fastapi-docker-example in the repository column.

Run the container:

bash
docker run -d --name fastapi-docker -p 8000:8000 fastapi-docker-example

Explanation:

OptionMeaning
-dRun in detached mode, in the background
--name fastapi-dockerName the container for easy reference
-p 8000:8000Map host port 8000 to container port 8000
fastapi-docker-exampleImage to run

Now open:

To see logs:

bash
docker logs -f fastapi-docker

To stop and remove the container:

bash
docker stop fastapi-docker
docker rm fastapi-docker

Handling Code Changes During Development

Rebuilding the image after every small code change is slow.
For development you usually:

Update the Dockerfile to use a development style entrypoint:

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

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

In Docker you usually pass these as environment variables.

Modify app/main.py to read an environment variable:

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

bash
docker run -d \
  --name fastapi-env \
  -p 8000:8000 \
  -e GREETING="Hi" \
  fastapi-docker-example

Or load from a file:

Create env.list:

text
GREETING=Howdy

Run:

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

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

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:

text
fastapi==0.111.0
uvicorn[standard]==0.30.0
gunicorn==22.0.0

A simple Gunicorn command:

bash
gunicorn -k uvicorn.workers.UvicornWorker app.main:app -b 0.0.0.0:8000 -w 4

Update Dockerfile:

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:

Dockerfile
ENV WORKERS=4
CMD ["sh", "-c", "gunicorn app.main:app -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 -w ${WORKERS}"]

Then run:

bash
docker run -d \
  --name fastapi-prod \
  -p 8000:8000 \
  -e WORKERS=2 \
  fastapi-docker-example

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

yaml
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-stopped

Then run:

bash
docker compose up --build

Stop:

bash
docker compose down

Common Gotchas and Tips

1. Container starts then exits immediately

Check the logs:

bash
docker logs fastapi-docker

Common causes:

2. “Connection refused” when opening localhost:8000

Check:

  1. Did you use --host 0.0.0.0 inside the container?
  2. Did you map the port? For example -p 8000:8000.
  3. Is the container still running? docker ps.

3. File changes not reflected

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:

These skills are the foundation for the upcoming chapters where you add databases, Redis, and other services, and deploy complete backend stacks.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!