Dockerize a Python FastAPI App Step by Step
Docker doesn't care which language your application is written in. In the previous lab you containerised an ASP.NET Core app; now you'll do the same for a Python FastAPI service. You'll build an image called python-fastapi:1.0, run it on port 8002, open its Swagger UI, and reproduce the most common FastAPI-in-Docker mistake: an app that listens on 127.0.0.1.
New to FastAPI itself? Read the Python tutorials first. All outputs below are from a real run on Docker Engine 29 with the python:3.13-slim image.
Step 1: The application files
Create a folder python-fastapi with three files. First, main.py:
import platform
import socket
from datetime import datetime, timezone
from fastapi import FastAPI
app = FastAPI(title="Python FastAPI in Docker")
@app.get("/")
def home():
return {"message": "Hello from Python FastAPI running inside a Docker container!"}
@app.get("/info")
def info():
return {
"machine": socket.gethostname(),
"python": platform.python_version(),
"os": platform.platform(),
"time": datetime.now(timezone.utc).isoformat(),
}
Then requirements.txt, with pinned versions so every build installs exactly the same packages:
fastapi==0.121.0
uvicorn==0.38.0
And a .dockerignore, so your local virtual environment and cache files aren't sent to Docker:
__pycache__/
*.pyc
.venv/
venv/
.env
Dockerfile
.dockerignore
Excluding .env matters: it often contains passwords and API keys, and anything copied into an image can be read by anyone who gets the image. Pass secrets at run time instead (-e or --env-file).
Step 2: The Dockerfile
FROM python:3.13-slim
WORKDIR /app
# Install the dependencies first so this layer is cached between code changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY main.py .
EXPOSE 8000
# 0.0.0.0 makes Uvicorn accept connections coming from outside the container
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
| Instruction | Why |
|---|---|
FROM python:3.13-slim | Official Python image, "slim" variant: Debian with only what Python needs. Much smaller than the full python:3.13 image. |
COPY requirements.txt + RUN pip install | Installed before copying the code, so the slow pip step is cached until requirements.txt changes. Same idea as dotnet restore in the .NET lab. |
--no-cache-dir | Don't keep pip's download cache in the image: smaller image. |
CMD [...] | Start Uvicorn, the ASGI server that runs FastAPI, on port 8000 and on all interfaces. |
Python doesn't need a separate build stage like .NET did: there's nothing to compile, so one stage is enough.
Step 3: Build the image
docker build -t python-fastapi:1.0 .
Two things worth noticing:
- Two pinned packages pulled in eleven more dependencies (Starlette, Pydantic and others). They're all baked into the image, so the container never needs internet access or a
pip installat run time. - The pip "root user" warning is about installing packages system-wide on a normal Linux machine. Inside a single-purpose container it's harmless.
docker images python-fastapi
Step 4: Run it on port 8002
docker run -d -p 8002:8000 --name python-fastapi python-fastapi:1.0
docker logs python-fastapi
Started server process [1]: Uvicorn is PID 1, the container's main process. -p 8002:8000 forwards host port 8002 to Uvicorn's port 8000.
Step 5: Test the endpoints
Open http://localhost:8002:
http://localhost:8002/info:
The OS string is revealing: the container uses the WSL 2 Linux kernel of Docker Desktop (it shares the host's kernel, as described in the introduction), with the Debian user-space of the slim image (glibc 2.41).
FastAPI's interactive documentation works too. Open http://localhost:8002/docs to get the Swagger UI, page title "Python FastAPI in Docker - Swagger UI", where you can try both endpoints.
The 127.0.0.1 mistake (and why 0.0.0.0 is required)
Many tutorials start Uvicorn with uvicorn main:app, whose default host is 127.0.0.1. Let's see what that does in a container. We override the image's CMD by adding a command after the image name:
docker run -d -p 8002:8000 --name fastapi-wrong python-fastapi:1.0 uvicorn main:app --host 127.0.0.1 --port 8000
docker logs fastapi-wrong
The container is running and the log looks healthy. But from the browser:
And from inside the same container, it works:
docker exec fastapi-wrong python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000/').read().decode())"
Why?
Every container has its own network namespace, with its own 127.0.0.1 (loopback) and its own network card (eth0) on Docker's bridge network. When you publish a port, Docker forwards traffic to the container's eth0 address, not to its loopback.
Browser -> host:8002 -> Docker -> container eth0 (172.17.0.x):8000 -> ?
--host 127.0.0.1 app listens only on the container's loopback -> nobody answers on eth0
--host 0.0.0.0 app listens on every interface, including eth0 -> request reaches the app
127.0.0.1 inside a container means "this container only", not "my computer". So a server in a container must listen on 0.0.0.0. The same rule applies to Flask (flask run --host=0.0.0.0), Django (runserver 0.0.0.0:8000) and Node.js. ASP.NET Core images already do this for you (http://[::]:8080).
Remove the broken container:
docker rm -f fastapi-wrong
Common FastAPI-in-Docker problems
| Symptom | Cause and fix |
|---|---|
| "Empty reply" or "connection reset" in the browser, container is Up | Uvicorn is on 127.0.0.1. Use --host 0.0.0.0. |
| Same symptom, host is 0.0.0.0 | Wrong container port in -p. It must match --port (8000 here). |
Container exits immediately, ModuleNotFoundError in the logs | A package is missing from requirements.txt, or the module path in main:app is wrong. |
| Every code change reinstalls all packages | The code was copied before pip install. Copy requirements.txt first. |
| Code changes don't appear | The code is inside the image: rebuild (docker build) and run a new container. |
Interview questions
Why should an application listen on 0.0.0.0 inside a container?
Published ports forward traffic to the container's network interface. An app bound to 127.0.0.1 accepts only connections from inside the same container, so it's unreachable from the host. 0.0.0.0 listens on all interfaces.
How do you override the CMD of an image?
Add the command after the image name: docker run image uvicorn main:app --port 9000. It replaces the Dockerfile's CMD for that container only.
Why copy requirements.txt separately before the code?
So the dependency layer is cached. It's rebuilt only when requirements.txt changes, which makes rebuilding after code changes take seconds.
Summary
- A Python image needs a base image, a cached
pip installlayer, the code and a CMD. docker build -t python-fastapi:1.0 .thendocker run -d -p 8002:8000 python-fastapi:1.0.- Swagger UI is available at
localhost:8002/docs. - Servers in containers must listen on
0.0.0.0, never only on 127.0.0.1.
Next lab: Run the ASP.NET Core and FastAPI containers together and test both from the browser.