Anshul Goyal I love to code and use new technologies.

Learn how to deploy a Rust web server to Heroku

6 min read 1720

Deploy a Rust web server to Heroku with Axum, Tokio, and GitHub actions

axum is an async web framework from the Tokio project. It is designed to be a very thin layer over hyper and is compatible with the Tower ecosystem, allowing the use of various middleware provided by tower-http and tower-web.

In this post, we will walk through how you can deploy a Rust web server to Heroku using axum, Tokio, and GitHub Actions for your projects.

Jump ahead:

Setting up a server using axum

axum provides a user-friendly interface to mount routes on a server and pass handler functions.

axum will handle listening to TCP sockets for connections and multiplexing HTTP requests to the correct handler and, as I mentioned, also allows the use of various middleware provided by the aforementioned Tower ecosystem.

use std::{net::SocketAddr, str::FromStr};

use axum::{
    http::StatusCode,
    response::IntoResponse,
    routing::get,
    Router,
    Server,
};


// running the top level future using tokio main
#[tokio::main]
async fn main() {
    // start the server
    run_server().await;
}
async fn run_server() {
    // Router is provided by Axum which allows mounting various routes and handlers.
    let app = Router::new()
        // `route` takes `/` and MethodRouter
        .route("/", 
        // get function create a MethodRouter for a `/` path from the `hello_world`
        get(hello_world))

    // create a socket address from the string address
    let addr = SocketAddr::from_str("0.0.0.0:8080").unwrap();
    // start the server on the address
    // Server is a re-export from the hyper::Server
    Server::bind(&addr)
    // start handling the request using this service
        .serve(app.into_make_service())
        // start polling the future
        .await
        .unwrap();
}

// basic handler that responds with a static string
// Handler function is an async function whose return type is anything that impl IntoResponse
async fn hello_world() -> impl IntoResponse {
    // returning a tuple with HTTP status and the body
    (StatusCode::OK, "hello world!")
}

Here, the Router struct provides a route method to add new routes and respective handlers. In the above example, get is used to create a get handler for the / route.

hello_world is a handler which returns a tuple with the HTTP status and body. This tuple has an implementation for the IntoResponse trait provided by axum.

The Server struct is a re-export of the hyper::Server. As axum attempts to be a very thin wrapper around hyper, you can expect it to provide performance comparable to hyper.

Handling POST requests

The post function is used to create a POST route on the provided path — as with the get function, post also takes a handler and returns MethodRoute.

let app = Router::new()
        // `route` takes `/` and MethodRouter
        .route("/", 
        // post function create a MethodRouter for a `/` path from the `hello_name`
        post(hello_name))

axum provides JSON serializing and deserializing right out of the box. The Json type implements both FromRequest and IntoResponse traits, allowing you to serialize responses and deserialize the request body.

// the input to our `hello_name` handler
// Deserialize trait is required for deserialising bytes to the struct
#[derive(Deserialize)]
struct Request {
    name: String,
}

// the output to our `hello_name` handler
// Serialize trait is required for serialising struct in bytes

#[derive(Serialize)]
struct Response{
    greet:String
}

The Request struct implements the Deserialize trait used by serde_json to deserialize the request body, while the Response struct implements the Serialize trait to serialize the response.

async fn hello_name(
    // this argument tells axum to parse the request body
    // as JSON into a `Request` type
    Json(payload): Json<Request>
) -> impl IntoResponse {
    // insert your application logic here
    let user = Response {
        greet:format!("hello {}",payload.name)
    };
    (StatusCode::CREATED, Json(user))
}

Json is a type provided by axum that internally implements the FromRequest trait and uses the serde and serde_json crate to deserialize the JSON body in the request to the Request struct.

Similar to the GET request handler, the POST handler can also return a tuple with the response status code and response body. Json also implements the IntoResponse trait, allowing it to convert the Response struct into a JSON response.



Extractors

Axum provides extractors as an abstraction to share state across your server and allows access of shared data to handlers.

// creating common state
let app_state = Arc::new(Mutex::new(HashMap::<String,()>::new()));

    let app = Router::new()
        // `GET /` goes to `root`
        .route("/", get(root))
        // `POST /users` goes to `create_user`
        .route("/hello", post(hello_name))
        // Adding the state to the router.
        .layer(Extension(app_state));

Extension wraps the shared state and is responsible for interacting with axum. In the above example, the shared state is wrapped in Arc and Mutex to synchronize the access to the inner state.

async fn hello_name(
    Json(payload): Json<Request>,
    // This will extract out the shared state
    Extension(db):Extension<Arc<Mutex<HashMap<String,()>>>>
) -> impl IntoResponse {
    let user = Response {
        greet:format!("hello {}",payload.name)
    };

   // we can use the shared state
    let mut s=db.lock().unwrap();
    s.insert(payload.name.clone(), ());
    (StatusCode::CREATED, Json(user))
}

Extension also implements the FromRequest trait that will be called by the axum to extract the shared state from the request and pass it to the handler functions.

GitHub Actions

GitHub Actions can be used to test, build, and deploy Rust applications. In this section, we will focus on deploying and testing Rust applications.

# name of the workflow
name: Rust

# run workflow when the condition is met
on:
# run when code is pushed on the `main` branch
  push:
    branches: [ "main" ]
# run when a pull request to the `main` branch
  pull_request:
    branches: [ "main" ]

# env variables
env:
  CARGO_TERM_COLOR: always

# jobs
jobs:
# job name
  build:
  # os to run the job on support macOS and windows also
    runs-on: ubuntu-latest
# steps for job
    steps:
    # this will get the code and set the git
    - uses: actions/[email protected]
    # run the build
    - name: Build
    # using cargo to build
      run: cargo build --release

    # for deployment
    - name: make dir
    # create a directory
      run: mkdir app
    # put the app in it
    - name: copy
      run: mv ./target/release/axum-deom ./app/axum


    # heroku deployment
    - uses: akhileshns/[email protected]
      with:
      # key from repository secrets
        heroku_api_key: ${{secrets.HEROKU_API_KEY}}
        # name of the Heroku app
        heroku_app_name: "axum-demo-try2"
        # email from which the app is uploaded
        heroku_email: "[email protected]"

        # app directory
        appdir: "./app"

        # start command
        procfile: "web: ./axum"
        # buildpack is like environment used to run the app
        buildpack: "https://github.com/ph3nx/heroku-binary-buildpack.git"

GitHub Actions provide support to stable versions of Rust by default. Cargo and rustc are installed by default on all supported operating systems by GitHub Actions — this is an action that run when the code is pushed to the main branch or when a pull request to the main branch is created.

on:
# run when code is pushed on the `main` branch
  push:
    branches: [ "main" ]
# run when a pull request to the `main` branch
  pull_request:
    branches: [ "main" ]

The workflow will first check the code, and then run the Cargo test to run the test on the code. It will then build the code using cargo-build.

The Cargo release will create a binary in the target folder, and the Action then copies the binary from the target folder to the ./app folder for further use in the Heroku deployment step, which we will now proceed to.


More great articles from LogRocket:


Heroku deployment for Rust

Heroku doesn’t have an official buildpack for Rust, so there’s no official build environment for Rust apps with Heroku.

So instead, we will use GitHub Actions to build the app and deploy it to Heroku.

Heroku requires having a buildpack for each app, so binary-buildpack is used for Rust apps. There are community buildpacks for Rust, and since GitHub Actions are already being used to build the app, time can be saved by directly using the binary build on Heroku.

The GitHub Actions market has a very useful akhileshns/heroku-deploy that deploys the Heroku app using GitHub Actions. In combination with binary-buildpack, it becomes a powerful tool to deploy code.

    - uses: akhileshns/[email protected]
      with:
      # key from repository secrets
        heroku_api_key: ${{secrets.HEROKU_API_KEY}}
        # name of the Heroku app
        heroku_app_name: "axum-demo-try2"
        # email from which the app is uploaded
        heroku_email: "[email protected]"

        # app directory
        appdir: "./app"

        # start command
        procfile: "web: ./axum"
        # buildpack is like environment used to run the app
        buildpack: "https://github.com/ph3nx/heroku-binary-buildpack.git"

To use this Action, a Heroku API key is needed. The key can be generated using the Heroku console in your account settings.

This action will create the app and deploy it for you. It takes the directory of the app and starts the command for the app, and you can also specify the buildpack you’d like to use.

Some code changes are required before the Rust app can be deployed to Heroku. Currently, the app uses an 8080 port, but Heroku will provide a different port for the app to use, so the Rust app should read the environment variable PORT.

    // read the port from env or use the port default port(8080)
    let port = std::env::var("PORT").unwrap_or(String::from("8080"));
    // convert the port to a socket address
    let addr = SocketAddr::from_str(&format!("0.0.0.0:{}", port)).unwrap();
    // listen on the port
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();

Conclusion

axum is a very good web server framework with support for the wider tower-rs ecosystem. It allows the building of extensible and composable web services and offers performance benefits by offering a thin layer over hyper.

GitHub Actions are great for CI/CD and allow for performing various automated tasks, such as building and testing code and generating docs on various platforms. GitHub Actions also support caching cargo dependencies to speed up Actions.

Heroku comes with support to autoscale continuous deployment, as well as support for hosted resources like databases and storage, for example. GitHub Actions and Heroku are independent of the framework, meaning the same action can test and deploy a web server written in Rocket or Actix Web — so feel free to experiment with whatever suits you!

When all of these tools are used together, they become a killer combo for developing and hosting Rust web servers. I hope you enjoyed following along with this tutorial — leave a comment about your experience below.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Anshul Goyal I love to code and use new technologies.

Leave a Reply