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
| Item | Why |
|---|---|
Dependencies locked (uv.lock or requirements.txt) | Production installs the same versions you tested |
| Secrets in environment variables | No 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 output | The platform collects the output, not files |
| CORS with exact origins | Chapter 22 |
| Database managed and migrations | The server disk is usually temporary |
| HTTPS | In 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:
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:
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:
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
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:
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):
- Upload the code to a repository on GitHub (without
.envand without.venv). - Create a web service on the platform, pointing to the repository.
- Provide the installation command (for example,
uv syncorpip install -r requirements.txt). - Provide the start command, using the port that the platform provides in
$PORT:fastapi run main.py --host 0.0.0.0 --port $PORT. - Register the environment variables (the secret key, the database URL, the CORS origins) through the platform's panel, not in the code.
- Deploy, and open
/saudeand/docsat 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
10000in the start command. The correct approach is to read the port from the variable$PORTthat the platform defines, instead of fixing a number.
Production documentation
The
/docsexposes the entire structure of your API. In a public API, this may be desirable. In an internal API, I usually turn it off withFastAPI(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.