--- name: fastapi-uv-docker description: 'Audit and migrate an existing Python project to best practices for a cloud-native ASGI FastAPI app managed with uv and run with uvicorn in Docker. Use when: conforming a project to production standards, setting up src layout, configuring pyproject.toml, writing multi-stage Dockerfiles, wiring lifespan and settings, adding health endpoints, enforcing non-root container user, migrating from requirements.txt to uv.' argument-hint: 'What is the current state of the project (bare Python, requirements.txt, pip, etc.)?' x-personal-mcp: id: fastapi-uv-docker version: 1.0.0 tags: - fastapi - uv - docker capabilities: - resource://skills/fastapi-uv-docker/document depends_on: [] references: fastapi-best-practices: path: references/fastapi-best-practices.md mime_type: text/markdown title: FastAPI Best Practices uv-project-layout: path: references/uv-project-layout.md mime_type: text/markdown title: uv Project Layout uvicorn-settings: path: references/uvicorn-settings.md mime_type: text/markdown title: Uvicorn Settings docker-cloud-native: path: references/docker-cloud-native.md mime_type: text/markdown title: Docker Cloud Native --- # FastAPI Project Best Practices Bring an existing Python project into full conformance with cloud-native best practices: **FastAPI + uv + uvicorn + Docker**. This skill audits the current state, produces a gap list, and walks through each conformance area in priority order. ## When to Use - Migrating an existing app from `pip`/`requirements.txt` to `uv` - Conforming a FastAPI app to src-layout and app-factory structure - Writing a production-grade Dockerfile with multi-stage builds - Wiring Pydantic settings from environment variables - Adding health endpoints and graceful lifespan shutdown - Enforcing non-root container user and proper signal handling - Setting up `docker-compose.yml` for local dev and CI ## Progressive Loading References Load these references only when needed: - FastAPI patterns and app structure: [FastAPI best practices](./references/fastapi-best-practices.md) - uv project layout and dependency management: [uv project layout](./references/uv-project-layout.md) - uvicorn CLI settings reference: [uvicorn settings](./references/uvicorn-settings.md) - Docker and cloud-native patterns: [Docker cloud-native patterns](./references/docker-cloud-native.md) --- ## Procedure ### Step 0: Audit the Project Before making changes, map the current state across six areas. Produce a short gap list for each. | Area | Check | |------|-------| | **Project manager** | Is `uv` used? Is `pyproject.toml` present? Is `uv.lock` committed? | | **Package layout** | Is a `src/` layout used? Is the package installable? | | **App structure** | Is `create_app()` factory used? Is lifespan wired? Are routers registered via `APIRouter`? | | **Configuration** | Are settings loaded from env via Pydantic `BaseSettings`? Are secrets out of code? | | **Container** | Is there a `Dockerfile`? Multi-stage? Non-root user? `.dockerignore` present? | | **Cloud-native** | Is there a `/healthz` endpoint? Graceful shutdown? Structured logs? | Load the [FastAPI best practices reference](./references/fastapi-best-practices.md) for structure rules. Load the [uv project layout reference](./references/uv-project-layout.md) for uv migration rules. Load the [uvicorn settings reference](./references/uvicorn-settings.md) for uvicorn CLI reference. Completion check: You can name every gap before touching any file. --- ### Step 1: Migrate to uv and Establish pyproject.toml **If the project uses `requirements.txt` / `setup.py` / `setup.cfg` / `pip`:** 1. Initialize uv if not present: `uv init` (or `uv init --lib` for importable package). 2. Import existing requirements: `uv add -r requirements.txt`. 3. Remove `requirements.txt`, `setup.py`, `setup.cfg`, and any `Pipfile`. 4. Ensure `.python-version` is committed with the target Python version. 5. Commit `uv.lock` — it is the source of truth for reproducible installs. 6. Add `.venv` to `.gitignore` and `.dockerignore`. **Canonical `pyproject.toml` shape:** ```toml [project] name = "my-app" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "fastapi[standard]>=0.115", "uvicorn[standard]>=0.34", "pydantic-settings>=2.0", ] [project.scripts] my-app = "my_app.main:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.uv] dev-dependencies = [ "pytest>=8", "httpx>=0.27", "pytest-asyncio>=0.24", ] [tool.hatch.build.targets.wheel] packages = ["src/my_app"] ``` **Commands:** ```bash uv add fastapi[standard] uvicorn[standard] pydantic-settings uv add --dev pytest httpx pytest-asyncio uv sync # creates .venv and installs all deps uv run pytest # run tests via uv ``` Completion check: `uv run python -c "import my_app"` succeeds. --- ### Step 2: Establish src Layout Move the package under `src/` to prevent import confusion between installed and local code. ``` . ├── pyproject.toml ├── uv.lock ├── .python-version ├── .env.example ├── README.md ├── src/ │ └── my_app/ │ ├── __init__.py │ ├── main.py # create_app() + entry point │ ├── config.py # Pydantic BaseSettings │ ├── lifespan.py # @asynccontextmanager lifespan │ ├── api/ │ │ ├── __init__.py │ │ ├── health.py # GET /healthz │ │ └── v1/ │ │ └── __init__.py │ └── services/ │ └── __init__.py ├── tests/ │ ├── conftest.py │ └── test_health.py ├── Dockerfile ├── .dockerignore └── docker-compose.yml ``` Completion check: `uv run python -m my_app` starts the server. --- ### Step 3: Wire the FastAPI App Factory Load the [FastAPI best practices reference](./references/fastapi-best-practices.md) for the full patterns. Key rules: **`src/my_app/main.py`:** ```python from contextlib import asynccontextmanager from fastapi import FastAPI from my_app.api.health import router as health_router from my_app.config import Settings def create_app(settings: Settings | None = None) -> FastAPI: if settings is None: settings = Settings() @asynccontextmanager async def lifespan(app: FastAPI): # startup: open DB pools, load models, etc. app.state.settings = settings yield # shutdown: close connections app = FastAPI( title="My App", lifespan=lifespan, docs_url="/docs" if settings.debug else None, redoc_url=None, ) app.include_router(health_router) return app app = create_app() ``` **`src/my_app/api/health.py`:** ```python from fastapi import APIRouter from fastapi.responses import JSONResponse router = APIRouter(tags=["ops"]) @router.get("/healthz", include_in_schema=False) async def health() -> JSONResponse: return JSONResponse({"status": "ok"}) ``` **`src/my_app/config.py`:** ```python from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") debug: bool = False host: str = "0.0.0.0" port: int = 8000 log_level: str = "info" ``` Completion check: `uv run uvicorn my_app.main:app --reload` starts with no import errors. --- ### Step 4: uvicorn Production Configuration Load the [uvicorn settings reference](./references/uvicorn-settings.md) for the full settings reference. **Never** configure uvicorn inside application code. Pass all settings via CLI or environment variables (`UVICORN_*` prefix). ```bash # Development — reload only; never in production uv run uvicorn my_app.main:app \ --reload \ --host 127.0.0.1 \ --port 8000 \ --log-level debug # Production — single process (orchestrator handles replication) uv run uvicorn my_app.main:app \ --host 0.0.0.0 \ --port 8000 \ --workers 1 \ --loop auto \ --http auto \ --log-level info \ --proxy-headers \ --forwarded-allow-ips '*' \ --timeout-graceful-shutdown 30 ``` **Key flags for production:** | Flag | Value | Reason | |------|-------|--------| | `--host 0.0.0.0` | Required in containers | Bind to all interfaces, not just loopback | | `--workers 1` | Kubernetes/Cloud Run | Orchestrator replicates containers | | `--loop auto` | Default | Uses `uvloop` when available (install `uvicorn[standard]`) | | `--http auto` | Default | Uses `httptools` when available | | `--proxy-headers` | Behind any proxy | Trusts `X-Forwarded-For`, `X-Forwarded-Proto` | | `--forwarded-allow-ips '*'` | Container/K8s | Trusts proxy headers from all IPs (safe when inside a trusted network) | | `--timeout-graceful-shutdown 30` | Prod | Seconds to wait before force-closing requests on shutdown | | `--no-access-log` | High-traffic prod | Disable per-request logs if using structured app-level logging | **Environment variable equivalents** (useful in `docker-compose.yml` / K8s manifests): ```bash UVICORN_HOST=0.0.0.0 UVICORN_PORT=8000 UVICORN_WORKERS=1 UVICORN_LOG_LEVEL=info UVICORN_PROXY_HEADERS=true UVICORN_FORWARDED_ALLOW_IPS=* ``` **`--reload` and `--workers` are mutually exclusive** — never combine them. **When to use `--workers > 1`:** Only for Docker Compose on a single host where orchestrator-level replication is not available. For Kubernetes / Cloud Run / Fargate: always `--workers 1` and scale via replicas — this gives predictable per-container memory and cleaner crash isolation. Completion check: `curl http://localhost:8000/healthz` returns `{"status":"ok"}`. --- ### Step 5: Write the Dockerfile Load the [Docker cloud-native patterns reference](./references/docker-cloud-native.md) for the full template and cloud-native rules. Key requirements: - Multi-stage build: `builder` stage installs deps; `runtime` stage is slim. - Pin uv version (copy from official image, not `latest`). - Use `uv sync --locked --no-editable` to install into `.venv`. - Set `ENV PATH="/app/.venv/bin:$PATH"` — do **not** use `uv run` in production `CMD`. - Run as non-root user. - Use `CMD` exec form, never shell form. - Add `HEALTHCHECK`. **Canonical Dockerfile:** ```dockerfile # syntax=docker/dockerfile:1 # ── builder ────────────────────────────────────────────────────────────────── FROM python:3.12-slim-bookworm AS builder # Pin uv version for reproducibility COPY --from=ghcr.io/astral-sh/uv:0.5.27 /uv /uvx /bin/ WORKDIR /app # Install dependencies first (cache layer) RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-editable # Copy source and install project COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-editable # ── runtime ─────────────────────────────────────────────────────────────────── FROM python:3.12-slim-bookworm AS runtime # Non-root user RUN groupadd --system --gid 1001 appgroup && \ useradd --system --uid 1001 --gid appgroup --no-log-init appuser WORKDIR /app # Copy only the virtual environment (not source code) COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv # Copy application source COPY --chown=appuser:appgroup src/ /app/src/ ENV PATH="/app/.venv/bin:$PATH" \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 USER appuser EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')" # Exec form — required for graceful shutdown / lifespan events CMD ["uvicorn", "my_app.main:app", \ "--host", "0.0.0.0", \ "--port", "8000", \ "--workers", "1", \ "--proxy-headers"] ``` Completion check: `docker build -t my-app . && docker run --rm -p 8000:8000 my-app` serves `/healthz`. --- ### Step 6: Write .dockerignore and docker-compose.yml **`.dockerignore`:** ``` .venv/ .git/ .gitignore .env *.pyc __pycache__/ .pytest_cache/ dist/ *.egg-info/ README.md ``` **`docker-compose.yml` (local dev):** ```yaml services: app: build: . ports: - "8000:8000" environment: - DEBUG=true - LOG_LEVEL=debug env_file: - .env develop: watch: - action: sync path: ./src target: /app/src - action: rebuild path: ./pyproject.toml - action: rebuild path: ./uv.lock ``` Completion check: `docker compose up` starts the app; `docker compose watch` enables hot reload. --- ### Step 7: Cloud-Native Checklist Run this final checklist before shipping: - [ ] `GET /healthz` returns 200 with no auth required - [ ] App reads all config from environment (`Settings` with no hardcoded values) - [ ] `.env` is in `.gitignore` and `.dockerignore`; `.env.example` is committed - [ ] `uv.lock` is committed - [ ] `.python-version` is committed - [ ] Dockerfile uses non-root user (`USER appuser`) - [ ] `CMD` uses exec form (list, not string) - [ ] `--proxy-headers` is set in the uvicorn `CMD` if behind a proxy - [ ] `PYTHONUNBUFFERED=1` is set (logs flush immediately) - [ ] `EXPOSE` declares the correct port - [ ] `HEALTHCHECK` is defined - [ ] Multi-stage build — final image contains no build tools or uv binary - [ ] `.venv` is in `.dockerignore` - [ ] No secrets hardcoded in `Dockerfile`, `pyproject.toml`, or source - [ ] `uv sync --locked` in CI (fail if lock is stale) - [ ] Tests pass via `uv run pytest` --- ## Anti-patterns to Fix | Anti-pattern | Correct approach | |---|---| | `requirements.txt` | Use `uv` with `pyproject.toml` and `uv.lock` | | `pip install` in Dockerfile | `uv sync --locked` | | `ENV SECRET_KEY=abc123` in Dockerfile | Inject at runtime via env; never bake secrets | | Shell form `CMD uvicorn ...` | Exec form `CMD ["uvicorn", ...]` | | `FROM tiangolo/uvicorn-gunicorn-fastapi` | Build from scratch with `python:3.x-slim` | | Multiple workers inside K8s container | `--workers 1`; scale via replicas | | Running as root in container | `USER appuser` with explicit UID 1001 | | Startup/shutdown in `@app.on_event` | Use `@asynccontextmanager` lifespan | | Config loaded from `.env` directly in code | Pydantic `BaseSettings` with `env_file` |