FastAPI Dockerfile with Gunicorn and Uvicorn Workers

This FastAPI Dockerfile runs Gunicorn as a process manager with Uvicorn workers on python:3.13-slim. Dependencies are installed in a builder stage, and the app runs as a non-root user on port 8000. Use it when one container should serve requests with several worker processes, for example on a single VM or a platform that bills per container.

CI status
Build passing
Image size (uncompressed)
136.2 MB
Build time
14 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
COPY requirements.txt .
RUN pip install --no-cache-dir --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 ["gunicorn", "app.main:app", "--worker-class", "uvicorn_worker.UvicornWorker", "--bind", "0.0.0.0:8000", "--workers", "2"]
.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

    A throwaway stage that installs packages. Pip's leftovers and any build tooling stay behind when the runtime image is assembled.

  2. RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

    Installs everything into one prefix without pip's cache, so a single directory can be copied into the final stage.

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

    Puts the installed packages and the gunicorn executable where the system Python and PATH already look.

  4. ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1

    Skips .pyc files and flushes output immediately, so worker logs appear in docker logs without buffering.

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

    Runs Gunicorn and every worker as an unprivileged user with a fixed UID.

  6. CMD ["gunicorn", "app.main:app", "--worker-class", "uvicorn_worker.UvicornWorker", "--bind", "0.0.0.0:8000", "--workers", "2"]

    Gunicorn supervises two worker processes and restarts any that crash. The uvicorn-worker class runs FastAPI's async event loop inside each one.

Build and run

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

Build from the project root, then open http://localhost:8000/docs. The startup log shows one line per booted worker, which confirms Gunicorn spawned them.

Other FastAPI Dockerfile variants

Plain Uvicorn with one process is simpler and fits orchestrators that scale by adding containers. Gunicorn with Uvicorn workers fits setups where each container should use several CPU cores. Both are listed in the table below with their measured sizes.

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

Troubleshooting

Error: class uri 'uvicorn_worker.UvicornWorker' invalid or not found
The uvicorn-worker package is missing. Add it to requirements.txt. The older uvicorn.workers module is deprecated in recent Uvicorn releases.
Workers are killed with WORKER TIMEOUT during slow requests
Gunicorn's default timeout is shorter than your slowest request. Raise it with --timeout, or move long-running jobs to a background queue.
Memory use grows with every worker you add
Each worker is a separate Python process with its own copy of the app. Size --workers to the container's memory limit, not only to its CPU count.

FAQ

How many workers should I configure?
Start with one or two per CPU core given to the container, then load-test. Async FastAPI apps often need fewer workers than synchronous frameworks for the same traffic.
Why not just run more Uvicorn containers?
On Kubernetes that is often better. On a single host or a per-container billing platform, several workers in one container use the available cores more cheaply.
Does Gunicorn work with FastAPI's lifespan events?
Yes. Each Uvicorn worker runs the app's startup and shutdown events independently, so connection pools are created once per worker process.