All posts
·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-slim is ~50MB vs python:3.12 at ~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 gunicorn with uvicorn.workers.UvicornWorker for multi-process. Uvicorn alone is single-process.
  • Set workers to (CPU * 2) + 1 and tune based on real CPU usage.
  • Add a healthcheck endpoint and configure HEALTHCHECK in the Dockerfile (or in your orchestrator).
  • Pin Python version explicitly. python:3.12 floats; python:3.12.5-slim does 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.

Got a workflow problem?

Let's talk about whether n8n, a custom backend, or a hybrid fits your case.

A 30-minute discovery call. Free, honest, you leave with a written direction either way.

Start QuizBook a Call