VOOZH about

URL: https://blog.logrocket.com/deploy-rust-web-server-heroku-axum-tokio-github-actions/

โ‡ฑ Learn how to deploy a Rust web server to Heroku - LogRocket Blog


2022-10-31
1720
#rust
Anshul Goyal
139289
๐Ÿ‘ Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

๐Ÿ‘ Deploy a Rust web server to Heroku with Axum, Tokio, and GitHub actions

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:

๐Ÿš€ Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

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.


Over 200k developers use LogRocket to create better digital experiences

๐Ÿ‘ Image
Learn more โ†’

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/checkout@v3
 # 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 web frontends for Rust apps

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

LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings โ€” compatible with all frameworks.

LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.

๐Ÿ‘ LogRocket Dashboard Free Trial Banner

Modernize how you debug your Rust apps โ€” start monitoring for free.

๐Ÿ‘ Image
๐Ÿ‘ Image
๐Ÿ‘ Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

๐Ÿ‘ Image
Emmanuel John
Jun 17, 2026 โ‹… 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

๐Ÿ‘ Image
Chizaram Ken
Jun 16, 2026 โ‹… 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension โ€” no new framework required.

๐Ÿ‘ Image
Ikeh Akinyemi
Jun 12, 2026 โ‹… 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo โ€” with email/password login, Google OAuth, session persistence, and protected routes.

๐Ÿ‘ Image
Chinwike Maduabuchi
Jun 9, 2026 โ‹… 13 min read
View all posts

Hey there, want to help make our blog better?

Join LogRocketโ€™s Content Advisory Board. Youโ€™ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now