# FastAPI Starter — uv (env), Gunicorn + UvicornWorker, Docker, JWT A compact, production-ready starter for FastAPI using **uv** as the Python environment manager, **Gunicorn** with **uvicorn.workers.UvicornWorker** for production, and JWT authentication. Includes Dockerfile and `docker-compose.yml` and step-by-step commands for local dev with `uv`. --- ## Project layout ``` fastapi-starter/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── config.py │ ├── api/ │ │ ├── __init__.py │ │ └── routes.py │ ├── auth.py │ └── logger.py ├── pyproject.toml ├── .env ├── Dockerfile ├── docker-compose.yml └── gunicorn_conf.py ``` --- ## pyproject.toml ```toml [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "fastapi-starter" version = "0.1.0" description = "FastAPI starter with uv, gunicorn+uvicorn worker, Docker, JWT" dependencies = [ "fastapi>=0.95", "uvicorn[standard]>=0.23", "gunicorn>=21.2", "python-jose[cryptography]>=3.3.0", "passlib[bcrypt]>=1.7", "python-dotenv>=1.0", "pydantic>=1.10" ] [tool.uv] # optional uv-specific settings can go here ``` --- ## .env (example) ``` APP_NAME=FastAPI Starter SECRET_KEY=replace-with-strong-random-key ACCESS_TOKEN_EXPIRE_MINUTES=60 HOST=0.0.0.0 PORT=8000 ``` --- ## app/config.py ```python from pydantic import BaseSettings class Settings(BaseSettings): app_name: str = "FastAPI Starter" secret_key: str access_token_expire_minutes: int = 60 host: str = "0.0.0.0" port: int = 8000 class Config: env_file = ".env" settings = Settings() ``` --- ## app/logger.py ```python import logging def setup_logger(): logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) ``` --- ## app/auth.py ```python from datetime import datetime, timedelta from typing import Optional from jose import jwt from passlib.context import CryptContext from fastapi import HTTPException, Depends from fastapi.security import OAuth2PasswordBearer from app.config import settings pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") ALGORITHM = "HS256" # In a real app, replace with DB user lookup fake_user_db = { "alice": { "username": "alice", "hashed_password": pwd_context.hash("secret"), } } def verify_password(plain: str, hashed: str) -> bool: return pwd_context.verify(plain, hashed) def authenticate_user(username: str, password: str): user = fake_user_db.get(username) if not user: return None if not verify_password(password, user["hashed_password"]): return None return user def create_access_token(subject: str, expires_delta: Optional[timedelta] = None): to_encode = {"sub": subject} expire = datetime.utcnow() + (expires_delta or timedelta(minutes=settings.access_token_expire_minutes)) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=ALGORITHM) return encoded_jwt async def get_current_user(token: str = Depends(oauth2_scheme)): try: payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise HTTPException(status_code=401, detail="Invalid authentication credentials") except Exception: raise HTTPException(status_code=401, detail="Invalid authentication credentials") user = fake_user_db.get(username) if user is None: raise HTTPException(status_code=401, detail="User not found") return user ``` --- ## app/api/routes.py ```python from fastapi import APIRouter, Depends from fastapi.security import OAuth2PasswordRequestForm from datetime import timedelta from app.auth import authenticate_user, create_access_token, get_current_user router = APIRouter() @router.post('/token') async def login(form_data: OAuth2PasswordRequestForm = Depends()): user = authenticate_user(form_data.username, form_data.password) if not user: return {"error": "invalid credentials"} access_token = create_access_token(subject=form_data.username) return {"access_token": access_token, "token_type": "bearer"} @router.get('/private') async def private_route(current_user = Depends(get_current_user)): return {"hello": f"{current_user['username']} — this is protected"} @router.get('/public') async def public_route(): return {"hello": "public"} ``` --- ## app/main.py ```python from fastapi import FastAPI from app.api.routes import router as api_router from app.logger import setup_logger from app.config import settings app = FastAPI(title=settings.app_name) setup_logger() app.include_router(api_router) @app.get('/health') def health(): return {"status": "ok"} ``` --- ## gunicorn_conf.py (optional tuning) ```python import multiprocessing workers = (multiprocessing.cpu_count() * 2) + 1 bind = "0.0.0.0:8000" worker_class = "uvicorn.workers.UvicornWorker" timeout = 30 ``` --- ## Dockerfile (uses uv for environment management) ```Dockerfile # Use a small base FROM python:3.11-slim as base ENV VENV_PATH=/opt/venv ENV PATH="$VENV_PATH/bin:$PATH" # Install necessary build deps to get uv installed (curl, ca-certificates) RUN apt-get update && apt-get install -y curl build-essential ca-certificates --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Install uv (Astral's uv) via the official install script RUN curl -LsSf https://astral.sh/uv/install.sh | sh WORKDIR /app # Copy project files COPY pyproject.toml . COPY .env . COPY app/ ./app/ # Use uv to create venv and install dependencies into it (no dev deps) RUN uv venv --force && uv install --no-dev # Expose EXPOSE 8000 # Production entrypoint using gunicorn with Uvicorn worker CMD ["gunicorn", "-c", "gunicorn_conf.py", "app.main:app"] ``` > Note: Docker builds will download the `uv` installer. If your environment disallows remote installers in builds, you can install dependencies with pip instead (fallback instructions below). --- ## docker-compose.yml ```yaml version: '3.8' services: web: build: . ports: - "8000:8000" environment: - SECRET_KEY=${SECRET_KEY} - ACCESS_TOKEN_EXPIRE_MINUTES=${ACCESS_TOKEN_EXPIRE_MINUTES} restart: unless-stopped ``` --- ## Local Developer workflow (with uv) 1. Install `uv` (see docs). Example: `curl -LsSf https://astral.sh/uv/install.sh | sh`. 2. Create a venv for the project and install dependencies: ```bash # From project root uv venv # creates/updates .venv uv install # installs deps from pyproject.toml ``` 3. Run locally in dev mode: ```bash uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ``` 4. Produce a lockfile for reproducible installs (recommended): ```bash uv lock ``` --- ## Production (Docker) ```bash docker build -t fastapi-starter:latest . docker run -e SECRET_KEY=supersecret -p 8000:8000 fastapi-starter:latest # or with docker-compose docker compose up --build -d ``` --- ## Fallback Dockerfile (if you prefer pip) If you don't want to use the `uv` installer inside Docker, use a classic pip-based Dockerfile: ```Dockerfile FROM python:3.11-slim ENV PATH="/opt/venv/bin:$PATH" RUN python -m venv /opt/venv RUN apt-get update && apt-get install -y build-essential --no-install-recommends && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY pyproject.toml . COPY app/ ./app/ RUN /opt/venv/bin/pip install --upgrade pip RUN /opt/venv/bin/pip install . EXPOSE 8000 CMD ["gunicorn", "-c", "gunicorn_conf.py", "app.main:app"] ``` --- ## Security & production notes * **SECRET_KEY** must be strong (use `openssl rand -hex 32` or similar) and stored as a secret in your deployment system (Kubernetes secret, Docker secret, cloud secret manager). * Use HTTPS termination on the load balancer. Keep the Gunicorn/uvicorn worker behind it. * Tune Gunicorn worker count and timeout according to your CPU/RAM and request profile. * Replace the `fake_user_db` with a proper user database and password reset flow. * Use rotating signing keys or key IDs (kid) if you need key rotation for JWTs. --- ## Next steps I can do for you * Add database integration (SQLAlchemy + Alembic) and dependency injection patterns * Add role-based auth and refresh-token support * Add tests (pytest + tox/uv) and CI (GitHub Actions) * Add detailed health-check and metrics (Prometheus) --- *Document generated for you — edit or ask for additions and I can update it.*