polohi
polohi

Reputation: 61

Running Alembic Migrations on FastAPI Startup

I am attempting to run Alembic migrations every time my FastAPI application starts up. I am deploying my FastAPI application using a Docker container.

Here is the code I have implemented to achieve this:

from alembic.config import Config
from alembic import command

def run_migrations():
    alembic_cfg = Config("alembic.ini")
    command.upgrade(alembic_cfg, "head")

@app.on_event("startup")
async def startup_event():
    run_migrations()

However, I am facing an issue where the Docker container stops once the startup is complete. Additionally, I noticed that the logger changes to Alembic instead of FastAPI.

I would like to understand how I can ensure that the migrations run successfully without stopping the container and while maintaining FastAPI logging. Any insights or suggestions would be appreciated.

Upvotes: 6

Views: 8423

Answers (3)

Gaston Barboza
Gaston Barboza

Reputation: 1

axel's answer works perfectly for regular sqlalchemy engines. However, if you are using an async engine, you will need to use

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from alembic.config import Config
from alembic import command

log = logging.getLogger("uvicorn")


async def run_migrations():
    alembic_cfg = Config("alembic.ini")
    await asyncio.to_thread(command.upgrade, alembic_cfg, "head")


@asynccontextmanager
async def lifespan(app_: FastAPI):
    log.info("Starting up...")
    log.info("run alembic upgrade head...")
    await run_migrations()
    yield
    log.info("Shutting down...")


app = FastAPI(lifespan=lifespan)

i.e., it should be asyncio.to_thread(command.upgrade, alembic_cfg, "head").

This is because alembic init -t async alembic generates something like

def do_run_migrations(connection: Connection) -> None:
    context.configure(connection=connection, target_metadata=target_metadata)
    with context.begin_transaction():
        context.run_migrations()


async def run_async_migrations() -> None:
    connectable = async_engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    async with connectable.connect() as connection:
        await connection.run_sync(do_run_migrations)

    await connectable.dispose()


def run_migrations_online() -> None:
    asyncio.run(run_async_migrations())

which requires a new thread to be run. Trying to run it in the current event loop does not work, because alembic does not use an async context manager (see this github issue).

Upvotes: 0

axel
axel

Reputation: 51

"startup" events are deprecated according to the documentation: https://fastapi.tiangolo.com/advanced/events/#alternative-events-deprecated.

The lifespan parameter of the FastAPI app is the way to handle startup and shutdown: https://fastapi.tiangolo.com/advanced/events/#lifespan.

You could do this, I tested it with a Docker command that only runs uvicorn:

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from alembic.config import Config
from alembic import command

log = logging.getLogger("uvicorn")


def run_migrations():
    alembic_cfg = Config("alembic.ini")
    command.upgrade(alembic_cfg, "head")


@asynccontextmanager
async def lifespan(app_: FastAPI):
    log.info("Starting up...")
    log.info("run alembic upgrade head...")
    run_migrations()
    yield
    log.info("Shutting down...")


app = FastAPI(lifespan=lifespan)

Upvotes: 4

Sanjai
Sanjai

Reputation: 23

I was also stuck in the same part. In my case, there was a error while running alembic upgrade. It was not shown in logs. It silently terminates the program because of which the fastapi server stops.

Try to run the alembic upgrade head command manually or increase the verbosity while calling upgrade, you will come to know about the error.

Upvotes: 0

Related Questions