Deploy a FastAPI service
FastAPI deploys here without configuration when your project matches the layout detection expects, and needs exactly one setting when it does not. Most real projects do not match, so this guide starts there.
What detection looks for
Three things have to be true for a start command to be derived automatically:
requirements.txt(or apyproject.toml) exists, so the project reads as Python.uvicornis among the dependencies.main.pysits at the repository root, with the FastAPI instance namedapp.
When all three hold, the build produces this:
uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}
zeitlos/fastapi-example(opens in a new tab) is that layout in three files:
mkdir my-api && cd my-api
printf 'fastapi\nuvicorn[standard]\n' > requirements.txt
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Deployed on Lucity"}
When your app is not at the root
The common production layout puts the app in a package, app/main.py or src/api/main.py, and adds a create_app() factory. Detection does not find main.py at the root, so it derives nothing and the service has no command to run.
Set the start command yourself in the service settings, and it is used verbatim:
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
That is not a workaround. Once you have set it, it survives every future build, and you control the worker count, the log level and everything else uvicorn accepts.
Workers, and when to add them
Uvicorn runs a single process by default. That is usually the right choice here: to add capacity, raise the replica count on the service and let the platform spread traffic across containers that can be scheduled on different nodes.
Workers inside one container still make sense when your app is CPU-bound in Python and you want to use a container's full CPU allocation:
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000} --workers 4
Match the worker count to the CPU you allocated, not to the node's core count.
Async database access
Add a PostgreSQL database and link DATABASE_URL to its fqdn-uri. FastAPI's concurrency model means an async driver is worth having:
import os
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
url = os.environ["DATABASE_URL"].replace("postgresql://", "postgresql+asyncpg://", 1)
engine = create_async_engine(url, pool_size=5, max_overflow=5)
session = async_sessionmaker(engine, expire_on_commit=False)
The dynamic variable gives you a postgresql:// URL, which SQLAlchemy reads as the sync driver, hence the rewrite. Keep the pool small and remember it is per replica.
Migrations belong in the start command
Nothing runs Alembic for you. Chain it ahead of the server so a rollout cannot serve traffic against an old schema:
alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
Every replica will run it on start. Alembic handles that safely, but a long migration delays every start, so run big ones separately.
Next steps
- Services for replicas, resources and health checks
- PostgreSQL for connection details and backups
- Variables for configuration and secrets