Skip to content

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"]
InstructionWhy
FROM python:3.13-slimOfficial Python image, "slim" variant: Debian with only what Python needs. Much smaller than the full python:3.13 image.
COPY requirements.txt + RUN pip installInstalled 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-dirDon'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 .
Output (selected lines)
#8 7.036 Installing collected packages: typing-extensions, idna, h11, click, annotated-types, annotated-doc, uvicorn, typing-inspection, pydantic-core, anyio, starlette, pydantic, fastapi
#8 8.288 Successfully installed annotated-doc-0.0.5 annotated-types-0.8.0 anyio-4.16.0 click-8.5.0 fastapi-0.121.0 h11-0.16.0 idna-3.20 pydantic-2.14.2 pydantic-core-2.50.2 starlette-0.49.3 typing-extensions-4.16.0 typing-inspection-0.4.4 uvicorn-0.38.0
#8 8.288 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager...
#8 DONE 8.5s
#9 [5/5] COPY main.py .
#9 DONE 1.0s
#10 naming to docker.io/library/python-fastapi:1.0 done

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 install at 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
Output
IMAGE                ID             DISK USAGE   CONTENT SIZE   EXTRA
python-fastapi:1.0   32ee3c312cd3        212MB         51.8MB   U

Step 4: Run it on port 8002

docker run -d -p 8002:8000 --name python-fastapi python-fastapi:1.0
docker logs python-fastapi
Output
INFO:     Started server process [1]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

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:

Output
HTTP/1.1 200 OK
server: uvicorn
content-type: application/json

{"message":"Hello from Python FastAPI running inside a Docker container!"}

http://localhost:8002/info:

Output
{"machine":"c22d2b3c7b39","python":"3.13.18","os":"Linux-6.6.87.2-microsoft-standard-WSL2-x86_64-with-glibc2.41","time":"2026-11-04T13:45:00.630876+00:00"}

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
Output
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

The container is running and the log looks healthy. But from the browser:

Output (curl http://localhost:8002/)
curl: (52) Empty reply from server

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())"
Output
{"message":"Hello from Python FastAPI running inside a Docker container!"}

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

SymptomCause and fix
"Empty reply" or "connection reset" in the browser, container is UpUvicorn is on 127.0.0.1. Use --host 0.0.0.0.
Same symptom, host is 0.0.0.0Wrong container port in -p. It must match --port (8000 here).
Container exits immediately, ModuleNotFoundError in the logsA package is missing from requirements.txt, or the module path in main:app is wrong.
Every code change reinstalls all packagesThe code was copied before pip install. Copy requirements.txt first.
Code changes don't appearThe 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 install layer, the code and a CMD.
  • docker build -t python-fastapi:1.0 . then docker 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.