·5 min read
Containerizing FastAPI with Docker: a sane setup
A minimal, production-ready Dockerfile for FastAPI — multi-stage build, non-root user, sensible defaults, and a 100MB image.
DockerFastAPIdeployment
The bad Dockerfile
FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
This works. It also produces a 900MB image that runs as root and rebuilds from scratch every time a comment changes.
You can do better in 15 lines.
The good Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
FROM python:3.12-slim
RUN useradd -m -u 1000 app
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --chown=app:app . .
USER app
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
About 120MB. Non-root. Layer-cached.
What each part does
- Multi-stage: install dependencies in a builder stage, copy the venv into a clean runtime. The runtime image does not need pip or build tools.
- Slim base:
python:3.12-slimis ~50MB vspython:3.12at ~400MB. - uv: 10x faster than pip, lockfile-aware, and the right choice in 2025.
- Non-root user: required for serious production environments and a defense-in-depth win even on permissive ones.
- Layer caching: copy lockfile before code, so dependency installs are cached when only code changes.
PYTHONUNBUFFERED: makes Python flush stdout immediately. Without it, logs lag.
Production extras
- Use
gunicornwithuvicorn.workers.UvicornWorkerfor multi-process. Uvicorn alone is single-process. - Set workers to
(CPU * 2) + 1and tune based on real CPU usage. - Add a healthcheck endpoint and configure
HEALTHCHECKin the Dockerfile (or in your orchestrator). - Pin Python version explicitly.
python:3.12floats;python:3.12.5-slimdoes not.
What we will not do
We do not put migrations in the Dockerfile entrypoint. Migrations run as a separate step in the deploy pipeline. Mixing them with app startup means every replica races to migrate, and one of them wins.
Keep the container boring. Boring deploys.