Python Admin

O que Todo Dev Deve Saber sobre Deploy de APIs Python: Uvicorn, Gunicorn, Docker e Nginx Já leu

Entendendo a Arquitetura de Deploy de APIs Python Quando você desenvolve uma API em Python usando frameworks como FastAPI ou Flask, está trabalhando em um ambiente local onde tudo funciona perfeitamente. No entanto, colocar essa aplicação em produção requer uma cadeia de ferramentas bem definida. O deploy de uma API Python não é apenas "rodar o arquivo principal" em um servidor remoto. É preciso entender que você está lidando com um aplicativo que precisa ser resiliente, escalável e seguro. A arquitetura típica de deploy envolve uma camada de aplicação (seu código Python), um servidor ASGI ou WSGI (Uvicorn ou Gunicorn), um orquestrador de containers (Docker), e finalmente um proxy reverso (Nginx). Cada uma dessas camadas tem um propósito específico e fundamental. Sem entender essa separação de responsabilidades, você terá dificuldades para debugar problemas, escalar a aplicação ou implementar features de segurança. Fundamentos: Uvicorn, Gunicorn e ASGI vs WSGI O que é ASGI e por que Uvicorn? ASGI (Asynchronous Server Gateway

Entendendo a Arquitetura de Deploy de APIs Python

Quando você desenvolve uma API em Python usando frameworks como FastAPI ou Flask, está trabalhando em um ambiente local onde tudo funciona perfeitamente. No entanto, colocar essa aplicação em produção requer uma cadeia de ferramentas bem definida. O deploy de uma API Python não é apenas "rodar o arquivo principal" em um servidor remoto. É preciso entender que você está lidando com um aplicativo que precisa ser resiliente, escalável e seguro.

A arquitetura típica de deploy envolve uma camada de aplicação (seu código Python), um servidor ASGI ou WSGI (Uvicorn ou Gunicorn), um orquestrador de containers (Docker), e finalmente um proxy reverso (Nginx). Cada uma dessas camadas tem um propósito específico e fundamental. Sem entender essa separação de responsabilidades, você terá dificuldades para debugar problemas, escalar a aplicação ou implementar features de segurança.

Fundamentos: Uvicorn, Gunicorn e ASGI vs WSGI

O que é ASGI e por que Uvicorn?

ASGI (Asynchronous Server Gateway Interface) é uma especificação moderna que permite que seu código Python execute operações assíncronas de forma nativa. Frameworks como FastAPI foram projetados para ASGI. O Uvicorn é um servidor ASGI de alta performance escrito em Python, com suporte a async/await nativo. Diferentemente do WSGI (sincronamente), ASGI permite que um único processo manipule múltiplas requisições concorrentemente sem bloquear.

Quando você usa FastAPI e simplesmente executa uvicorn main:app --reload, está rodando em modo desenvolvimento. O servidor não é otimizado para produção: usa apenas um worker, não possui múltiplos processos, e qualquer erro de código crasheia tudo. Em produção, você precisa de múltiplos workers (processos) rodando em paralelo.

Por que Gunicorn é frequentemente melhor em produção?

Gunicorn (Green Unicorn) é um gerenciador de aplicações WSGI/ASGI que roda múltiplos workers (processos filhos) gerenciados por um master process. Cada worker é um processo completamente independente que pode servir requisições. Se um worker morrer, o Gunicorn automaticamente reinicia outro. Isso oferece robustez que Uvicorn sozinho não fornece.

A recomendação da própria comunidade FastAPI é usar Gunicorn como gerenciador de workers Uvicorn. Isso combina o melhor dos dois mundos: a eficiência do Uvicorn para ASGI com a robustez e gerenciamento de processos do Gunicorn.

Veja este exemplo funcional:

# main.py - API FastAPI simples
from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.get("/slow")
async def slow_endpoint():
    await asyncio.sleep(2)
    return {"status": "completed"}

@app.get("/health")
async def health():
    return {"status": "healthy"}

Para rodar em produção com Gunicorn gerenciando Uvicorn:

gunicorn main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --access-logfile - \
  --error-logfile -

O parâmetro --workers 4 inicia 4 processos paralelos. Cada um roda uma instância do Uvicorn. Se você tem 4 cores de CPU, essa é uma boa escolha inicial (cores + 1 também é uma métrica comum). O --worker-class uvicorn.workers.UvicornWorker diz ao Gunicorn para usar Uvicorn como o worker, não o padrão síncrono.

Docker: Containerização e Isolamento

Por que containerizar sua aplicação?

Docker garante que sua aplicação rode exatamente igual em qualquer máquina: desenvolvimento local, CI/CD, servidor de staging ou produção. Você não quer ouvir "funciona na minha máquina". Com Docker, a máquina é a mesma em todo lugar. Além disso, Docker facilita a escalabilidade horizontal: você pode rodar N containers da mesma imagem em diferentes servidores.

Um Dockerfile define como construir uma imagem Docker. A imagem é como um "snapshot" da sua aplicação com todas as dependências. Quando você roda essa imagem, ela cria um container — uma instância isolada e executável.

Segue um Dockerfile bem estruturado e pronto para produção:

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# Copiar apenas requirements primeiro (aproveita cache do Docker)
COPY requirements.txt .

# Instalar dependências
RUN pip install --no-cache-dir -r requirements.txt

# Copiar código fonte
COPY . .

# Usuário não-root por segurança
RUN useradd -m appuser
USER appuser

# Expor porta
EXPOSE 8000

# Comando para rodar a aplicação
CMD ["gunicorn", "main:app", \
     "--workers", "4", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000"]

E o arquivo requirements.txt:

fastapi==0.104.1
uvicorn==0.24.0
gunicorn==21.2.0

Para construir a imagem:

docker build -t minha-api:1.0 .

Para rodar um container:

docker run -d -p 8000:8000 --name api-container minha-api:1.0

O -d roda em background, -p 8000:8000 mapeia a porta do container para sua máquina.

Docker Compose para ambientes locais

Se sua aplicação depende de banco de dados ou cache, usar Docker Compose evita ter que instalar Postgres, Redis etc localmente. Define tudo em um arquivo docker-compose.yml:

# docker-compose.yml
version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
    depends_on:
      - db
    volumes:
      - .:/app  # Hot reload em desenvolvimento

  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Execute com docker-compose up. Todos os serviços sobem juntos, conectados automaticamente por rede interna.

Nginx: Proxy Reverso e Balanceamento de Carga

O papel do Nginx na arquitetura

Nginx é um proxy reverso extremamente eficiente que fica na frente dos seus workers Gunicorn/Uvicorn. Ele recebe todas as requisições HTTP, decide para qual worker enviá-las (load balancing), comprime respostas, serve arquivos estáticos (sem sobrecarregar sua API) e oferece camada de segurança. Nginx é escrito em C e é muito mais rápido que fazer tudo em Python.

A arquitetura real de produção fica assim: Cliente → Nginx (porta 80/443) → Load Balancer → Workers Gunicorn (porta 8000) → Código FastAPI.

Segue uma configuração Nginx funcional:

# nginx.conf
upstream api_backend {
    # Distribui requisições entre 4 workers
    server localhost:8000;
    server localhost:8001;
    server localhost:8002;
    server localhost:8003;
}

server {
    listen 80;
    server_name api.example.com;

    # Redirecionar HTTP para HTTPS em produção
    # return 301 https://$server_name$request_uri;

    client_max_body_size 10M;

    # Servir arquivos estáticos sem passar pelo Python
    location /static/ {
        alias /var/www/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Proxy reverso para a API
    location / {
        proxy_pass http://api_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;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # WebSocket support (importante para APIs real-time)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Health check endpoint
    location /health {
        proxy_pass http://api_backend;
        access_log off;
    }
}

Se você está usando Docker Compose com Nginx e múltiplos containers da API, a configuração muda ligeiramente:

upstream api_backend {
    server api:8000;  # Nome do serviço Docker + porta interna
    # Se usar múltiplos containers, Docker cuida do balanceamento automaticamente
}

server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://api_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;
    }
}

SSL/TLS com Let's Encrypt

Em produção, sempre use HTTPS. O Certbot automatiza a obtenção de certificados Let's Encrypt:

sudo apt-get install certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com

Isso gera certificados e modifica automaticamente o Nginx para usar HTTPS. Renova automaticamente antes do vencimento.

Colocando Tudo Junto: Um Deploy Completo

Arquitetura final com Docker Compose

# docker-compose.prod.yml
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./static:/var/www/static:ro
      - ./certs:/etc/nginx/certs:ro  # Certificados SSL
    depends_on:
      - api
    networks:
      - prodnetwork

  api:
    build:
      context: .
      dockerfile: Dockerfile
    expose:
      - "8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - LOG_LEVEL=info
    depends_on:
      - db
    restart: always
    networks:
      - prodnetwork
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: always
    networks:
      - prodnetwork
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  prodnetwork:
    driver: bridge

volumes:
  postgres_data:

Deploy em um servidor:

# SSH no servidor
ssh user@seu-servidor.com

# Clone do repositório
git clone https://seu-repo.git
cd seu-repo

# Build das imagens
docker-compose -f docker-compose.prod.yml build

# Iniciar em background
docker-compose -f docker-compose.prod.yml up -d

# Verificar logs
docker-compose -f docker-compose.prod.yml logs -f api

# Ver status
docker-compose -f docker-compose.prod.yml ps

Monitoramento básico

Adicione à sua aplicação FastAPI:

# main.py - com métricas
from fastapi import FastAPI
from prometheus_client import Counter, Histogram, generate_latest
import time

app = FastAPI()

request_count = Counter(
    'api_requests_total',
    'Total API requests',
    ['method', 'endpoint']
)

request_duration = Histogram(
    'api_request_duration_seconds',
    'API request duration',
    ['method', 'endpoint']
)

@app.middleware("http")
async def add_metrics(request, call_next):
    start = time.time()
    response = await call_next(request)
    duration = time.time() - start

    request_count.labels(
        method=request.method,
        endpoint=request.url.path
    ).inc()

    request_duration.labels(
        method=request.method,
        endpoint=request.url.path
    ).observe(duration)

    return response

@app.get("/metrics")
async def metrics():
    return generate_latest()

@app.get("/")
async def root():
    return {"message": "API running"}

@app.get("/health")
async def health():
    return {"status": "ok"}

Com pip install prometheus-client, você expõe métricas em /metrics que podem ser coletadas por Prometheus e visualizadas no Grafana.

Conclusão

Três pontos fundamentais que diferem um deploy amador de um profissional:

  1. Separação de responsabilidades: Uvicorn roda o código, Gunicorn gerencia workers, Nginx roteia requisições. Cada ferramenta faz bem uma coisa. Não tente fazer Uvicorn fazer tudo sozinho em produção.

  2. Containerização não é opcional: Docker garante reproducibilidade e é o padrão da indústria. Além de facilitar deploy, permite escalar horizontalmente. Um container que funciona localmente funciona em qualquer lugar.

  3. Arquitetura em camadas protege seus assets: Nginx na frente absorve conexões lentas, trata SSL, comprime responses. Seus workers Python ficam livres para processar lógica, não I/O de rede.

Referências


Artigos relacionados