Docker and the Makefile

You have a typed service, a lifespan that loads the model once, Depends wiring, BackgroundTasks, auto-docs, and CORS. The last piece is shipping: a Dockerfile that builds a reproducible image, and a Makefile that hides the incantations behind simple targets.

Dockerfile
dockerfile
FROM python:3.11-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

RUN apt-get update && apt-get install -y \
    curl \
    && rm -rf /var/lib/apt/lists/*

COPY pyproject.toml ./
RUN uv sync --frozen

COPY . .

ENV PYTHONUNBUFFERED=1
ENV PATH="/app/.venv/bin:$PATH"

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Slim Python base, uv for fast installs, copy pyproject first so Docker caches the dependency layer, then copy the rest. No reload flag because this is the production image.

Makefile
makefile
setup:
	@cp -n .env.example .env || true
	@uv sync

run:
	@uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000

dev: setup run

build:
	docker compose build

up:
	docker compose up -d

down:
	docker compose down

A Makefile gives you verbs your teammates can memorize in a minute. make dev gets them from clone to running server. make up and make down wrap Docker compose.

The lifespan runs once per worker process. If you start uvicorn with multiple workers, each worker loads its own copy of the client. That is usually fine. For a very large model you either keep a single worker, share state over a network service, or move to a dedicated serving layer. For the bulk of services, per-worker is exactly the tradeoff you want.

AI prompt: Try it: review your Dockerfile

Loading practice…

Checkpoint: Final checkpoint

Loading practice…