Deploying#
This page covers the parts of a production deployment that are specific to FastAPI-Restly. Everything else (uvicorn workers, gunicorn, TLS, reverse proxies, Docker, behind-a-proxy headers) is already covered well in FastAPI’s deployment docs and is not duplicated here.
Database configuration#
In production, drive the engine from environment variables. A small
pydantic-settings shim keeps the wiring obvious and 12-factor friendly:
import fastapi_restly as fr
from pydantic_settings import BaseSettings
class Settings(fr.utils.CurrentSettingsMixin, BaseSettings):
database_url: str
db_pool_size: int = 5
db_max_overflow: int = 10
CurrentSettingsMixin gives
the class one shared instance without building it at import, so importing your
package never requires a configured environment. Settings.current reads
DATABASE_URL and the pool fields the first time something asks. The
production template below reads it inside
create_app() and passes the values into
fr.configure() through an explicit
engine, which is what lets you size the pool. Settings.use(...) installs an
instance instead, which is how a test suite hands over settings it built
itself; see Test APIs with RestlyTestClient and Fixtures.
Sizing is the part Restly leaves to you. Given a PostgreSQL URL it already sets
pool_pre_ping=True and pool_recycle=1800, so the template below repeats
pool_pre_ping only to keep it visible next to the settings it belongs with;
see Engine Defaults for the full list
and for how to decline it. If your app already has an engine, pass that one
instead; see
Reuse Your Existing Engine.
Migrations with Alembic#
In production, never call metadata.create_all(); use Alembic instead.
For the recommended postgresql+asyncpg setup, initialise Alembic with its
async template once in your project root:
alembic init -t async alembic
The generic template created by alembic init alembic builds a synchronous
engine and cannot open an async URL. Use it only when Alembic receives a
separate synchronous database URL. Alembic’s
asyncio recipe
shows the same template and how to adapt an existing environment.
Point alembic/env.py at the metadata of whichever declarative base your
models inherit from (typically fr.DataclassBase).
Metadata covers the models that have been imported, so env.py needs a module
that has seen all of them. That is main.py: it reaches view registration,
which reaches every view, and each view imports its model. Because the factory
builds nothing at import time, this costs the imports and nothing else:
# alembic/env.py
import fastapi_restly as fr
import app.main # noqa: F401 (imports every view, and each view its model)
target_metadata = fr.DataclassBase.metadata
Models that no view reaches, such as an outbox or audit table, need importing
wherever they are used, at module level rather than inside a function. Run
alembic check in CI to catch one that is missed: it reports the absent model
as a dropped table rather than failing quietly.
Restly’s declarative bases map plain Mapped[datetime] columns to
DateTime(timezone=True). On PostgreSQL, upgrading an existing naive timestamp
column requires an explicit interpretation of the stored values. If they
represent UTC, use a reviewed migration such as:
import sqlalchemy as sa
from alembic import op
op.alter_column(
"event",
"occurred_at",
type_=sa.DateTime(timezone=True),
postgresql_using="occurred_at AT TIME ZONE 'UTC'",
)
A bare type change can reinterpret values in the server’s local timezone. Use
mapped_column(DateTime()) on the model only when the column intentionally
stores a timezone-free wall-clock value.
Run migrations as part of your release or startup pipeline:
alembic upgrade head
To have tests exercise the same migration path, pass alembic_upgrade=True to
fr.testing.configure_tests().
Restly then runs alembic upgrade head against the test database before the
first test. See
Test databases and migrations.
A production main.py template#
With settings and migrations in place, the pieces combine into one small application factory:
from contextlib import asynccontextmanager
import fastapi_restly as fr
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from .settings import Settings
from .tasks.views import TaskView
from .users.views import UserView
VIEWS = (TaskView, UserView)
def create_app() -> FastAPI:
settings = Settings.current
engine = create_async_engine(
settings.database_url,
pool_size=settings.db_pool_size,
max_overflow=settings.db_max_overflow,
pool_pre_ping=True,
)
@asynccontextmanager
async def lifespan(_app: FastAPI):
try:
yield
finally:
await engine.dispose()
app = FastAPI(lifespan=lifespan)
fr.configure(app, async_engine=engine, health="/health")
for view in VIEWS:
fr.include_view(app, view)
return app
Note four details in this template:
The factory keeps configuration out of module import: settings are read and the engine is built when
create_app()runs, so a test suite can install its own settings first and call the same factory. See Test APIs with RestlyTestClient and Fixtures.The
VIEWStuple keeps view definitions free of registration side effects and makesmain.pythe one application composition boundary; see Project structure.fr.configure(app, ...)installs the default exception handlers (currently the translator that turnsIntegrityErrorinto a 409 response; see Database conflicts). Passinstall_default_exception_handlers=Falseto opt out.health="/health"mounts the liveness endpoint.The engine belongs to the app the factory built:
engine.dispose()inlifespancleans up the connection pool on shutdown so workers exit promptly. Restly never disposes an engine itself, so an application that configures from a URL instead reaches its engine throughfr.db.get_async_engine(); see Engine Disposal.
Running the app#
Use any production ASGI runner. The most common options are uvicorn and gunicorn with uvicorn workers. See FastAPI’s deployment docs for the full picture, including TLS, reverse proxies, and Docker.
A minimal invocation runs the factory directly:
uvicorn "app.main:create_app" --factory --host 0.0.0.0 --port 8000 --workers 4
When your platform expects an application object rather than a factory, put it
in its own module and leave main.py alone:
# app/asgi.py
from .main import create_app
app = create_app()
uvicorn app.asgi:app then works, while main.py stays free to import. The
FastAPI CLI cannot call a factory, so name that module for it and
fastapi dev and fastapi run work too:
[tool.fastapi]
entrypoint = "app.asgi:app"
Do not put app = create_app() at the bottom of main.py instead: with
required settings that makes importing the module require DATABASE_URL
everywhere, including conftest.py and alembic/env.py. Nothing inside the
package should import asgi.py; only the server does. Either way, build the
application once per process: Restly’s session configuration is process-wide,
so a later factory call re-points every earlier app’s requests at the new
database too. See how a factory’s apps share one
configuration.
Sync RestView endpoints run on FastAPI’s threadpool, so worker count
still has the usual effect; async AsyncRestView endpoints share the
event loop within a worker. Do not use --reload in production.
Health checks#
Naming a path in fr.configure() mounts an
endpoint there:
fr.configure(app, async_engine=engine, health="/health")
GET /health then answers 200 with {"status": "ok"}, and appears in /docs
like any other route. The path is yours to choose; /healthz and /up are
common alternatives. Omit the argument and there is no such route, and if your
application already has one at that path, Restly leaves it alone.
This is a liveness check: it reports that the process is up and answering, and makes no database round-trip. It suits a probe that restarts the container on failure:
livenessProbe:
httpGet:
path: /health
port: 8000
A readiness probe, which takes the instance out of rotation rather than restarting it, is the place to check the database and other dependencies. Write that as an ordinary route so its checks and timeouts stay yours.
See also#
Use Restly in an Existing Project: wiring Restly into an app that already has an engine, sessions, and models.
Examples: the production-shaped SaaS example to compare against. It includes PostgreSQL Compose services, validated settings, an application-owned async engine, migrations, and migration-backed tests.