FastAPI — deployment produkcyjny na VPS z Uvicorn i Nginx
Opublikowano: 10 kwietnia 2026 · Kategoria: VPS
FastAPI to najszybszy framework webowy dla Pythona — async z natury, z automatyczną dokumentacją OpenAPI i walidacją przez Pydantic. W benchmarkach wyprzedza Flask nawet 5-krotnie przy operacjach I/O. Jednak wdrożenie na VPS wymaga kilku kroków: Uvicorn (serwer ASGI), Gunicorn (process manager), Nginx (reverse proxy) i systemd (autostart). Ten artykuł przeprowadza cię przez cały proces od instalacji do działającego API w produkcji, z async SQLAlchemy i JWT authentication.
Instalacja i środowisko wirtualne
# Ubuntu 22.04 / 24.04 — przygotowanie srodowiska
sudo apt update && sudo apt install -y python3.11 python3.11-venv python3-pip
# Konto aplikacji (bez sudo, bez logowania)
sudo useradd -m -s /bin/bash fastapi
sudo su - fastapi
# Venv i zalenosci
python3.11 -m venv /home/fastapi/venv
source /home/fastapi/venv/bin/activate
pip install fastapi "uvicorn[standard]" gunicorn \
sqlalchemy[asyncio] asyncpg alembic \
python-jose[cryptography] passlib[bcrypt] \
pydantic-settings python-multipart
# Struktura projektu
mkdir -p /home/fastapi/app/{routers,models,schemas,core,db}
cd /home/fastapi/app Struktura aplikacji FastAPI
Dobra architektura FastAPI opiera się na routerach (osobne pliki per feature), zależnościach (Depends) i schematach Pydantic oddzielonych od modeli ORM. Poniżej minimalna, ale kompletna struktura z async endpointami i przykładowym routerem.
# main.py — entry point
from fastapi import FastAPI
from contextlib import asynccontextmanager
from app.db.session import engine
from app.models import Base
from app.routers import users, items
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: mozna tutaj inicjalizowac polaczenia
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
# Shutdown: cleanup
await engine.dispose()
app = FastAPI(
title="Moje API",
version="1.0.0",
lifespan=lifespan,
)
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(items.router, prefix="/items", tags=["items"])
@app.get("/health")
async def health():
return {"status": "ok"}
# routers/users.py — przykladowy router z async
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app import crud, schemas
router = APIRouter()
@router.get("/", response_model=list[schemas.User])
async def read_users(
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db),
):
users = await crud.get_users(db, skip=skip, limit=limit)
return users
@router.post("/", response_model=schemas.User, status_code=201)
async def create_user(
user: schemas.UserCreate,
db: AsyncSession = Depends(get_db),
):
db_user = await crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return await crud.create_user(db=db, user=user) SQLAlchemy async ORM + asyncpg
# db/session.py — async engine i session
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.core.config import settings
engine = create_async_engine(
settings.DATABASE_URL, # postgresql+asyncpg://user:pass@localhost/db
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
echo=False, # True w dev, False w prod
)
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# models/user.py — model ORM
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from app.db.base_class import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# crud/users.py — operacje async na bazie
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.user import User
from app.schemas.user import UserCreate
from app.core.security import get_password_hash
async def get_user_by_email(db: AsyncSession, email: str):
result = await db.execute(select(User).where(User.email == email))
return result.scalar_one_or_none()
async def create_user(db: AsyncSession, user: UserCreate):
hashed = get_password_hash(user.password)
db_user = User(email=user.email, hashed_password=hashed)
db.add(db_user)
await db.flush()
await db.refresh(db_user)
return db_user Gunicorn + UvicornWorker — konfiguracja produkcyjna
Uvicorn sam w sobie nie potrafi zarządzać wieloma procesami — do tego służy Gunicorn z
workerem UvicornWorker. Gunicorn obsługuje sygnały (SIGTERM, SIGHUP), graceful shutdown i
auto-restart przy awarii. Plik konfiguracyjny gunicorn.conf.py jest czytelniejszy
niż długie flagi w CLI.
# /home/fastapi/app/gunicorn.conf.py bind = "unix:/run/fastapi/fastapi.sock" workers = 4 # 2 * CPU + 1 dla I/O-bound worker_class = "uvicorn.workers.UvicornWorker" worker_connections = 1000 timeout = 30 keepalive = 5 max_requests = 1000 # restart workera po N requestach (zapobiega memory leaks) max_requests_jitter = 100 preload_app = True # wspolny memory dla forked workers accesslog = "/home/fastapi/logs/access.log" errorlog = "/home/fastapi/logs/error.log" loglevel = "info" proc_name = "fastapi-app" # Test uruchomienia (z katalogu aplikacji): # source /home/fastapi/venv/bin/activate # gunicorn main:app -c gunicorn.conf.py
Systemd service — autostart i restart
# /etc/systemd/system/fastapi.service [Unit] Description=FastAPI Gunicorn After=network.target postgresql.service Wants=postgresql.service [Service] User=fastapi Group=fastapi WorkingDirectory=/home/fastapi/app RuntimeDirectory=fastapi EnvironmentFile=/home/fastapi/app/.env ExecStart=/home/fastapi/venv/bin/gunicorn main:app -c gunicorn.conf.py ExecReload=/bin/kill -s HUP $MAINPID KillMode=mixed TimeoutStopSec=30 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target # Wlaczenie i start sudo systemctl daemon-reload sudo systemctl enable --now fastapi sudo systemctl status fastapi # Zero-downtime reload (wymusza nowe workery bez przerwy) sudo systemctl reload fastapi # Logi journalctl -u fastapi -f
Nginx reverse proxy z rate limiting
# /etc/nginx/sites-available/fastapi
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
upstream fastapi_backend {
server unix:/run/fastapi/fastapi.sock fail_timeout=0;
}
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Nagłówki bezpieczenstwa
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Referrer-Policy strict-origin-when-cross-origin;
location / {
limit_req zone=api burst=50 nodelay;
proxy_pass http://fastapi_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_buffering off;
}
location /docs {
# Ogranicz dostep do dokumentacji w prod
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
proxy_pass http://fastapi_backend;
}
}
# Test i przeladowanie Nginx
sudo nginx -t && sudo systemctl reload nginx JWT Authentication — wzorzec produkcyjny
# core/security.py
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
# core/deps.py — dependency injection
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from jose import JWTError, jwt
from app.core.config import settings
from app.db.session import get_db
from app import crud
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
user_id: int = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await crud.get_user(db, user_id=int(user_id))
if user is None or not user.is_active:
raise credentials_exception
return user Porównanie FastAPI vs Flask vs Django
| Cecha | FastAPI | Flask | Django REST |
|---|---|---|---|
| Wydajność (req/s) | ~50 000 (async) | ~8 000 (sync) | ~6 000 (sync) |
| Async / ASGI | Natywny async | Opcjonalny (gevent) | ASGI od Django 3.1 |
| Walidacja danych | Pydantic v2 (built-in) | Marshmallow / WTForms | DRF Serializers |
| Dokumentacja API | OpenAPI auto (Swagger) | Flasgger (ręcznie) | drf-spectacular |
| Krzywa nauki | Niska–Średnia | Niska | Wysoka (ekosystem) |
| Wbudowane funkcje | Tylko API | Tylko API | ORM, admin, auth |
| Typowe użycie | REST API, ML serving | Proste API, mikrousługi | Full-stack, CMS |