FastAPI Dockerfile for Production: Slim, Multi-Stage, Uvicorn

This FastAPI Dockerfile installs your dependencies in a throwaway builder stage and copies only the installed packages into a clean python:3.13-slim runtime. The app runs under Uvicorn as an unprivileged user on port 8000. It suits APIs deployed behind a load balancer or reverse proxy that need a small image without compilers or caches.

CI status
Build passing
Image size (uncompressed)
136.2 MB
Build time
13 s
Runtime image
python:3.13-slim
Port
8000
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS builder
WORKDIR /app
ENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt

FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN useradd --create-home --uid 10001 app
USER app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
.dockerignore
.git
__pycache__/
*.pyc
.venv/
.env
.pytest_cache/
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM python:3.13-slim AS builder

    Starts a named builder stage. Everything installed here, including pip's working files and any build tools, stays behind when the final image is assembled.

  2. ENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1

    Stops pip from writing its download cache and from checking for a newer pip on every build, which keeps layers smaller and builds quieter.

  3. COPY requirements.txt . RUN pip install --prefix=/install -r requirements.txt

    Copying only requirements.txt first means this expensive layer is reused until your dependencies change. Installing into /install keeps the packages in one directory that is easy to copy out.

  4. COPY --from=builder /install /usr/local

    Brings the installed packages into the runtime stage at the location Python already searches, so no PYTHONPATH tweaks are needed.

  5. ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1

    Skips writing .pyc files into the container and flushes stdout immediately, so your logs show up in docker logs without delay.

  6. RUN useradd --create-home --uid 10001 app USER app

    Creates a dedicated user with a fixed UID and switches to it. A compromised process then has no root privileges inside the container.

  7. CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]

    Binds Uvicorn to all interfaces so the published port is reachable. --proxy-headers makes FastAPI trust X-Forwarded-* headers from the proxy in front of it.

Build and run

terminal
docker build -t fastapi-app .
docker run --rm -p 8000:8000 fastapi-app

Run both commands from the project root, where the Dockerfile and .dockerignore live. Then open http://localhost:8000/docs to check that the interactive API docs load from inside the container.

Other FastAPI Dockerfile variants

This slim, single-process Uvicorn setup is the default for FastAPI. It is a good fit when your platform scales containers horizontally and you would rather run more small containers than more workers inside one. More FastAPI variants will appear in the table below once they pass CI.

VariantRuntime imageImage sizeCI
Gunicorn + Uvicorn workerspython:3.13-slim136.2 MBPassing
Python slim + Uvicorn (multi-stage)python:3.13-slim136.2 MBPassing

Troubleshooting

ModuleNotFoundError: No module named 'app'
The CMD expects a package called app with main.py inside it. Either move your code to that layout or change app.main:app to match your module path.
uvicorn: executable file not found in $PATH
uvicorn is missing from requirements.txt, so the builder stage never installed it. Add it, rebuild, and confirm it is listed in your pinned dependencies.
Redirects use http:// even though the site is served over HTTPS
Your proxy must send X-Forwarded-Proto. --proxy-headers already trusts it, but some setups also need Uvicorn's forwarded-allow-ips option set to the proxy's address.

FAQ

Why not use the full python image instead of slim?
The full image ships compilers and development headers that your API never needs at runtime. Slim keeps the attack surface and pull time down. If a dependency needs to compile, it can do so in the builder stage.
Should I run Gunicorn with Uvicorn workers?
Inside containers you usually scale by running more replicas rather than more workers per container, so plain Uvicorn is simpler. A Gunicorn-managed variant makes sense on a single large host.
How do I add a health check?
Expose a lightweight route such as /health in FastAPI and point your orchestrator's HTTP probe at port 8000. That works without adding curl to the slim image.