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 VIEWS tuple keeps view definitions free of registration side effects and makes main.py the one application composition boundary; see Project structure.

  • fr.configure(app, ...) installs the default exception handlers (currently the translator that turns IntegrityError into a 409 response; see Database conflicts). Pass install_default_exception_handlers=False to opt out. health="/health" mounts the liveness endpoint.

  • The engine belongs to the app the factory built: engine.dispose() in lifespan cleans 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 through fr.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.