~/posts/aula-containerizar-api-python.md
Aula prática: containerizar uma API Python em produção
Dockerfile multi-stage, gunicorn + uvicorn workers, healthcheck. O passo-a-passo que usamos no laboratório.
Containerizar uma API Python parece simples — até você precisar de logs estruturados, healthcheck, graceful shutdown e multi-stage build para uma imagem final < 100MB. Esta aula reproduz o template que usamos no lab.
O Dockerfile final (que vamos construir)
# ===== Stage 1: build =====
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# ===== Stage 2: runtime =====
FROM python:3.12-slim
WORKDIR /app
# Non-root user
RUN useradd -m -u 1000 -s /bin/bash app
# Copy installed deps from builder
COPY --from=builder /root/.local /home/app/.local
ENV PATH=/home/app/.local/bin:$PATH
# Copy app code
COPY --chown=app:app . .
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", "--access-logfile", "-", \
"main:app"]
Resultado: imagem final de ~92MB.
Passo 1 — Multi-stage cuts your image in half
O truque do multi-stage é instalar dependências em um stage descartável (builder), e copiar apenas o resultado para o stage final. Sem isso, sua imagem carrega gcc, headers do Python, cache do pip — tudo desnecessário em runtime.
| Approach | Tamanho final |
|---|---|
python:3.12 + pip install |
980 MB |
python:3.12-slim + pip install |
280 MB |
python:3.12-slim + multi-stage |
92 MB |
python:3.12-alpine + multi-stage |
68 MB ⚠️ |
Alpine é menor, mas dá problemas com libs que dependem de glibc (numpy, scipy, pandas). Para APIs simples, vale. Para data-science, fique no slim.
Passo 2 — Por que gunicorn + uvicorn workers?
FastAPI é ASGI (async). Uvicorn é o servidor ASGI de referência. Mas em produção:
- Uvicorn sozinho: processo único. Se ele crashar, sua API morre.
- Gunicorn como process manager: fork de N workers, restart automático, graceful shutdown.
- Workers do tipo
UvicornWorker: gunicorn gerencia, uvicorn executa o async.
A combinação dá robustez de process manager + performance async. É o padrão recomendado pela própria documentação do FastAPI.
Passo 3 — Healthcheck que faz sentido
@app.get("/health")
async def health():
# 1. Processo está respondendo? Sim (a função executou)
# 2. Conexão com banco está ok?
try:
await db.execute("SELECT 1")
except Exception:
return Response(status_code=503)
return {"status": "ok"}
Esse /health é usado tanto pelo HEALTHCHECK do Dockerfile quanto pelo liveness/readiness do Kubernetes. Não retorne sempre 200: o healthcheck precisa falhar se o banco cair, senão o orchestrator nunca remove o pod doente do load balancer.
Passo 4 — Logs estruturados
import logging, json
class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"ts": self.formatTime(record),
"level": record.levelname,
"msg": record.getMessage(),
"module": record.module,
})
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
Logs JSON são indispensáveis para qualquer ferramenta de observability (CloudWatch, Datadog, Grafana Loki) extrair campos.
Passo 5 — Graceful shutdown
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
# startup
yield
# shutdown: close connections, flush logs
await db.close()
app = FastAPI(lifespan=lifespan)
Sem isso, ao escalar para baixo, conexões DB ficam abertas no servidor, requests em vôo são abortadas.
Resultado
Imagem final pronta para:
- ECS Fargate
- Cloud Run
- Kubernetes
- Docker Compose em produção (com Caddy/Nginx na frente)
E roda em qualquer plataforma sem mudança.