Pular para o conteúdo

    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

    ItemPor 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 ambienteNada 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ãoA plataforma coleta a saída, e não arquivos
    CORS com origens exatasCapítulo 22
    Banco gerenciado e migraçõesO disco do servidor costuma ser temporário
    HTTPSEm 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:

    Terminal
    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:

    Terminal
    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:

    avancado/cap30_deploy.pylinhas 10 a 49
    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"]))
    
    Saída
    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:

    Dockerfile
    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):

    1. Suba o código para um repositório no GitHub (sem .env e sem .venv).
    2. Crie um serviço web na plataforma, apontando para o repositório.
    3. Informe o comando de instalação (por exemplo, uv sync ou pip install -r requirements.txt).
    4. 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.
    5. 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.
    6. Faça o deploy, e abra /saude e /docs no 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 10000 no comando de início. O correto é ler a porta da variável $PORT que a plataforma define, em vez de fixar um número.

    Documentação em produção

    A /docs expõ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 com FastAPI(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.