# 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.*