Capítulo 30, Avançado
Deploy
Colocar a API no ar é preparar o projeto para rodar em uma máquina que não é a sua, sem ninguém para consertar nada. A maior parte do trabalho é antes do deploy, e não no clique final.
A lista de verificação antes de publicar
| Item | Por quê |
|---|---|
Dependências travadas (uv.lock ou requirements.txt) | A produção instala as mesmas versões que você testou |
| Segredos em variáveis de ambiente | Nada de chave no código nem no Git (capítulo 23) |
.gitignore com .env, .venv, __pycache__ | Para não publicar segredos nem lixo |
Rota de saúde (/saude) | A plataforma precisa saber se a aplicação está viva |
| Logs para a saída padrão | A plataforma coleta a saída, e não arquivos |
| CORS com origens exatas | Capítulo 22 |
| Banco gerenciado e migrações | O disco do servidor costuma ser temporário |
| HTTPS | Em geral, a plataforma o fornece |
Para a primeira linha, o uv.lock já registra as versões exatas. Plataformas que esperam um requirements.txt recebem um com o comando:
uv export --no-dev --no-hashes -o requirements.txt
Rodar em produção
Em desenvolvimento, o fastapi dev recarrega o código a cada alteração. Em produção, o comando é outro: sem recarga, escutando em todas as interfaces, e na porta que a plataforma indicar:
fastapi run main.py --host 0.0.0.0 --port $PORT
O uvicorn direto faz o mesmo, e aceita mais opções, como --workers (vários processos) e --proxy-headers (confiar nos cabeçalhos do proxy). Dá para verificar que os dois comandos realmente servem a aplicação, subindo cada um em uma porta livre, chamando a rota de saúde e encerrando:
import socket
import subprocess
import sys
import time
from pathlib import Path
import httpx2
Path("main_deploy.py").write_text(
"from fastapi import FastAPI\n\napp = FastAPI()\n\n\n"
'@app.get("/saude")\ndef saude():\n return {"status": "ok"}\n',
encoding="utf-8",
)
def porta_livre() -> int:
with socket.socket() as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def servir_e_chamar(comando: list[str]) -> int | None:
porta = porta_livre()
processo = subprocess.Popen(
comando + ["--port", str(porta)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
try:
for _ in range(60):
try:
return httpx2.get(f"http://127.0.0.1:{porta}/saude", timeout=1).status_code
except httpx2.TransportError:
time.sleep(0.2)
return None
finally:
processo.terminate()
processo.wait(timeout=10)
print("fastapi run:", servir_e_chamar([sys.executable, "-m", "fastapi", "run", "main_deploy.py"]))
print("uvicorn: ", servir_e_chamar([sys.executable, "-m", "uvicorn", "main_deploy:app"]))
fastapi run: 200
uvicorn: 200
Vários processos
Um processo Python usa um núcleo de CPU. Para aproveitar a máquina inteira, sobem-se vários processos (--workers 4), e uma regra comum é um por núcleo. Lembre-se do que isso implica, e que vimos nos capítulos 8 e 28: cada processo tem a sua própria memória. Um dicionário em memória, um cache ou um contador de limite não é compartilhado. Estado precisa morar em um banco ou em um Redis.
Um contêiner
Um contêiner empacota a aplicação e as dependências, e roda igual em qualquer lugar. Este é o formato de um Dockerfile com o uv, que eu uso como ponto de partida:
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
COPY app ./app
RUN useradd --create-home executor
USER executor
EXPOSE 8000
HEALTHCHECK CMD python -c "import httpx2; httpx2.get('http://127.0.0.1:8000/saude').raise_for_status()"
CMD ["uv", "run", "--no-dev", "fastapi", "run", "app/main.py", "--host", "0.0.0.0", "--port", "8000"]
Esse arquivo é um modelo (o capítulo 60 do curso de Python detalha o Docker): eu não construí a imagem neste curso, e o HEALTHCHECK precisa do httpx2 instalado na imagem. Os pontos que importam são: copiar pyproject.toml e uv.lock antes do código (as dependências ficam em cache entre os builds), instalar com --frozen (versões do uv.lock, sem recalcular), e rodar como usuário comum, e não como root.
Publicar em uma plataforma
O caminho é parecido em quase todas (Render, Railway, Fly.io, um provedor de nuvem):
- Suba o código para um repositório no GitHub (sem
.enve sem.venv). - Crie um serviço web na plataforma, apontando para o repositório.
- Informe o comando de instalação (por exemplo,
uv syncoupip install -r requirements.txt). - Informe o comando de início, usando a porta que a plataforma fornece em
$PORT:fastapi run main.py --host 0.0.0.0 --port $PORT. - Cadastre as variáveis de ambiente (a chave secreta, a URL do banco, as origens do CORS) pelo painel da plataforma, e não no código.
- Faça o deploy, e abra
/saudee/docsno endereço público.
Eu não verifiquei a interface de nenhuma plataforma
Os nomes de botões, os planos gratuitos e os limites mudam com frequência, e eu não os testei aqui. Os passos acima são o fluxo geral; confira a documentação atual da plataforma que escolher. Uma diferença que o material antigo traz: o curso original fixa a porta
10000no comando de início. O correto é ler a porta da variável$PORTque a plataforma define, em vez de fixar um número.
Documentação em produção
A
/docsexpõe a estrutura inteira da sua API. Em uma API pública, isso pode ser desejável. Em uma API interna, eu costumo desligá-la comFastAPI(docs_url=None, redoc_url=None, openapi_url=None), ou protegê-la com autenticação.
Exercício 1
Uma rota de prontidão
Crie GET /pronto, que devolva 200 com {"pronto": true} quando uma variável de módulo BANCO_OK for verdadeira, e 503 com {"pronto": false} caso contrário. É o que plataformas usam para saber se podem mandar tráfego para a instância.