Skip to content

    Chapter 30, Advanced

    Deploy

    Putting the API live is preparing the project to run on a machine that is not yours, with no one to fix anything. Most of the work is before the deploy, not in the final click.

    The checklist before publishing

    ItemWhy
    Dependencies locked (uv.lock or requirements.txt)Production installs the same versions you tested
    Secrets in environment variablesNo keys in the code or in Git (chapter 23)
    .gitignore with .env, .venv, __pycache__To avoid publishing secrets or junk
    Route of health (/saude)The platform needs to know if the application is alive
    Logs to the standard outputThe platform collects the output, not files
    CORS with exact originsChapter 22
    Database managed and migrationsThe server disk is usually temporary
    HTTPSIn general, the platform provides it

    For the first line, the uv.lock already logs the exact versions. Platforms that expect a requirements.txt receive one with the command:

    Terminal
    uv export --no-dev --no-hashes -o requirements.txt
    

    Running in production

    In development, the fastapi dev reloads the code on every change. In production, the command is different: without reloading, listening on all interfaces, and on the port that the platform indicates:

    Terminal
    fastapi run main.py --host 0.0.0.0 --port $PORT
    

    The uvicorn directly does the same, and accepts more options, such as --workers (multiple processes) and --proxy-headers (trusting the proxy headers). It is possible to verify that both commands actually serve the application, running each on a free port, calling the health route and shutting down:

    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"]))
    
    Output
    fastapi run: 200
    uvicorn:     200
    

    Multiple processes

    A Python process uses one CPU core. To take advantage of the entire machine, multiple processes (--workers 4), and a common rule is one per core. Remember what this implies, and what we saw in chapters 8 and 28: each process has its own memory. A dictionary in memory, a cache, or a limit counter is not shared. State needs to reside in a database or in a Redis.

    A container

    A container packages the application and its dependencies, and runs the same anywhere. This is the format of a Dockerfile with the uv, which I use as a starting point:

    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"]
    

    This file is a model (chapter 60 of the Python course details Docker): I did not build the image in this course, and the HEALTHCHECK needs to be httpx2 installed in the image. The important points are: copy pyproject.toml and uv.lock before from the code (the dependencies are cached between builds), install with --frozen (versions of uv.lock, without recalculating), and run as a regular user, not as root.

    Publish on a platform

    The path is similar in almost all (Render, Railway, Fly.io, a cloud provider):

    1. Upload the code to a repository on GitHub (without .env and without .venv).
    2. Create a web service on the platform, pointing to the repository.
    3. Provide the installation command (for example, uv sync or pip install -r requirements.txt).
    4. Provide the start command, using the port that the platform provides in $PORT: fastapi run main.py --host 0.0.0.0 --port $PORT.
    5. Register the environment variables (the secret key, the database URL, the CORS origins) through the platform's panel, not in the code.
    6. Deploy, and open /saude and /docs at the public address.

    I did not check the interface of any platform

    Button names, free plans, and limits change frequently, and I did not test them here. The steps above are the general flow; check the current documentation of the platform you choose. One difference that the old material brings: the original course fixes the port 10000 in the start command. The correct approach is to read the port from the variable $PORT that the platform defines, instead of fixing a number.

    Production documentation

    The /docs exposes the entire structure of your API. In a public API, this may be desirable. In an internal API, I usually turn it off with FastAPI(docs_url=None, redoc_url=None, openapi_url=None), or protect it with authentication.

    Exercise 1

    A readiness route

    Create GET /pronto, which returns 200 with {"pronto": true} when a module variable BANCO_OK is true, and 503 with {"pronto": false} otherwise. This is what platforms use to know if they can send traffic to the instance.