This example shows how to migrate an Axum app with a Postgres database.

This guide assumes that you have Shuttle already setup. If you don’t, head on over to our installation guide.

Add Shuttle dependencies

The first step is to add shuttle-runtime and shuttle-axum to your dependencies to be able to use Shuttle’s runtime with the Axum framework.

cargo add shuttle-runtime shuttle-axum

Any secrets you need to use will be kept in a Secrets.toml file (dev secrets in Secrets.dev.toml) which will be placed at the Cargo.toml level.

You can also easily get a provisioned database like so (this example will be for a provisioned PostgreSQL instance specifically):

cargo add shuttle-shared-db --features postgres,sqlx

If you have any database records you’d like to keep, it would be a good idea to export them so that they can be migrated to the new database. You will not need a secrets file if you only need a provisioned Postgres database - this will be automatically be provisioned and given to you in the form of a connection string or an sqlx pool.

Migrate your Code

To be able to run your project on Shuttle, you need to make a few changes to your code. Instead of the tokio::main macro, you will use the shuttle_runtime::main macro and swap out dotenvy for Shuttle’s Postgres annotation:

This is what your main.rs file looks like before:

main.rs
#[tokio::main]
async fn main() {
    dotenvy::dotenv().ok();

    let db_url = dotenvy::var("DATABASE_URL").expect("No database URL was set!");
    let secret = dotenvy::var("MY_SECRET").expect("No secret was set!");

    let pool = sqlx::Pool::connect(&db_url).await.unwrap();
    sqlx::migrate!()
        .run(&pool)
        .await
        .expect("Migrations failed :(");

    // Use secrets for anything that needs them

    let router = create_api_router(pool);
    let addr = SocketAddr::from(([0, 0, 0, 0], 8000));

    Server::bind(&addr)
        .serve(router.into_make_service())
        .await
        .unwrap()
}

And this is what it looks like after:

main.rs
#[shuttle_runtime::main]
pub async fn axum (
    #[shuttle_shared_db::Postgres] pool: PgPool,
    #[shuttle_runtime::Secrets] secrets: shuttle_runtime::SecretStore,
) -> shuttle_axum::ShuttleAxum {
    sqlx::migrate!()
        .run(&pool)
        .await
        .expect("Migrations failed :(");

    // Use secrets for anything that needs them

    let router = create_api_router(pool);

    Ok(router.into())
}

If you need more than a simple router, you’ll want to create a custom struct that holds all of your required app state information inside and then create an impl for the struct - you can find more about that here. Anything outside of your entry point function (the function that uses the shuttle_runtime::main macro) doesn’t need to be changed. If you are using secrets as well as a database connection, you may wish to create a struct that holds both of these values and then pass it into the function that generates the router.

Local run

Test your app locally with:

shuttle run

Deploying

Now all you need to do is to run the following command:

shuttle deploy

Your project should now be deployed!