Setup

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

For this example, we’ll assume you are migrating an Axum project.

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 to be held in Secrets.dev.toml) which will be placed at the Cargo.toml level - you’ll want to use the shuttle-secrets crate to be able to use the new secrets:

cargo add shuttle-secrets

Your secrets file will look the same as your previous .env file, so you won’t need to change anything.

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

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 to you in the form of a SQLx connection.

Migrating 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 before:

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

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

    let pool = sqlx::Pool::connect(&url).await.unwrap();

    sqlx::migrate!()
        .run(&pool)
        .await
        .expect("Migrations failed :(");

    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_secrets::Secrets] secrets: shuttle_secrets::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.

Deploying

To ensure that you get a unique project name, create a Shuttle.toml file at the Cargo.toml level to name your project to whatever you like.

name = "my-unique-app-name-here"

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

cargo shuttle project start
cargo shuttle project deploy

Your project should now be deployed!