Deploying your Rust WASM Game to Web with Shuttle & Axum
Tags: rust, gamedev
Published: 2024-02-14 18:09:41

---
title: "Deploying your Rust WASM Game to Web with Shuttle & Axum"
published: "true"
description: "Deploying your Rust WASM Game to the Web 🎮 how you can use Shuttle to host your newly created 🦀 Rust game built with Axum web server."
tags: "rust, gamedev"
canonical_url: "https://rodneylab.com/deploying-your-rust-wasm-game/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eg12dq4hx84bmvyj4itp.png"
---

## 🦀 Deploying your Rust WASM Game

In this post, we see you how you can get going on deploying your Rust WASM game.  Often, WASM games will just need an HTML page to be embedded in, and a public server to make the page available across the Internet.  We saw in a <a href="https://rodneylab.com/rust-for-gaming/">recent post on Rust game engines</a>, that many engines currently offer the option to output your game in WASM, with (**Bevy**, **Fyrox** and **Macroquad** included).

Here, we will use an open-source Macroquad game written to demo Entity Component Systems, using **Shipyard**, a Rust ECS mentioned in a <a href="https://rodneylab.com/rust-entity-component-systems/">recent Rust ECS post</a>.

### Shuttle Rust App Hosting Service

That leaves us with a web server and deployment service to pick! **Axum** is currently a popular Rust Web framework choice, and to keep things completely Rust, we shall deploy using Shuttle.  Shuttle offer Rust app hosting and have a **free tier**, which should be just fine for our game.

Now we know the approach, why don&rsquo;t we get started?


## 🧱 What we're Building

![Deploying your Rust WASM Game to Web: screen capture shows a webpage with a Square Eater title. Below is the game window which reads Click to start.  Further down a title reads How to play.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/st6vxliipcqyczxzsht0.png)

We will build the Square Eater game, which was originally written to demo ECSs.  You should be able to follow along easily enough, with your own Rust game, though, even if it&rsquo;s not built using Macroquad.  Let me know if you run in difficulties with other game engines!

We will create three binaries from the project:

- one just to run the game locally, natively or build to WASM;
- another as a local Axum server for development and to test the webpage locally; and
- a copy of the Axum server binary configured to deploy and run on Shuttle.

Shuttle has excellent Axum support, and the code for running the Axum server locally and on Shuttle is almost identical, so we will put that shared code in a library and reference it from the two server output versions we create.

## ⚙️ Cargo.toml

To add the three binaries in a single project, we can use the <a href="https://doc.rust-lang.org/cargo/reference/cargo-targets.html">Cargo targets feature</a>.  The `Cargo.toml` file will look like this:

```toml
[lib]
path = "src/lib.rs"
name = "shared"

[[bin]]
path = "src/bin/server.rs"
name = "local-server"

[[bin]]
path = "src/bin/shuttle.rs"
name = "square-eater"

[[bin]]
path = "src/bin/main.rs"
name = "game"
```

Here we have the library and three binaries, mentioned earlier.  If you match the name of the Shuttle binary to your Shuttle project name, (line `16`), Shuttle will automatically build and deploy that binary.  The project name needs to be unique across Shuttle, as it forms the domain Shuttle uses to server the project from.

## 📂 Project Structure

Here, we are just using some open-source code for the game, and it can fit into a single source file.  We place that code in `src/bin/main.rs`, and can run the game locally using:

```shell
cargo run --bin game
```

With `game` matching the binary name used in `Cargo.toml`, above.  The code we use is provided as an <a href="https://github.com/leudz/shipyard/tree/master/square_eater">example of using the Shipyard Rust ECS</a>.  Paste the `main.rs` square_eater code from the repo into `src/bin/main.rs` in your project. 

For this to run, we need to add the following dependencies in `Cargo.toml`:

```toml
[package]
name = "square_eater"
version = "0.1.0"
edition = "2021"
 
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
path = "src/lib.rs"
name = "shared"
 
# TRUNCATED... 
 
[[bin]]
path = "src/bin/main.rs"
name = "game"
 
[build-dependencies]
askama = "0.12.1"
 
[dependencies]
macroquad = "0.4.4"
miniquad = "0.3.12"
sapp-wasm = "=0.1.26"
shipyard = { version = "0.6.2", default-features = false, features = ["proc", "std"] }
```

`miniquad` and `sapp-wasm` are not, strictly, needed to run the game locally, but used in the next section, when we build the game to WASM.

## 🎮 Building a Macroquad the Game as WASM

See the <a href="https://github.com/not-fl3/macroquad">macroquad docs for full details on building a WASM game</a>.

If this is the first time you are generating WASM with Rust on your machine, add the WASM output toolchain:

```shell
rustup target add wasm32-unknown-unknown
```

Then, you can run a WASM release build with:

```shell
 cargo build --bin game --release --target wasm32-unknown-unknown
```

This will output the WASM code to `target/wasm32-unknown-unknown/release/game.wasm`.  Copy that file to a new `public` folder in the project root directory;  we will use it when we create the web page next.

### Adding a Web Page

We will serve the WASM file and HTML page from the `public` directory.  Add an `index.html` file there with this content:

```html
<html lang="en-GB">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
    <link
      rel="alternate icon"
      href="/favicon.ico"
      type="image/png"
      sizes="16x16"
    />
    <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="192x192" />
    <link rel="mask-icon" href="/favicon.svg" />
    <title>Square Eater</title>
    <style>
      @font-face{font-display:swap;font-family:Josefin Sans;font-style:normal;font-weight:700;src:url(/fonts/josefin-sans-v32-latin-700.woff2)format("woff2")}@font-face{font-display:swap;font-family:Fira Mono;font-style:normal;font-weight:400;src:url(/fonts/fira-mono-v14-latin-regular.woff2)format("woff2")}@font-face{font-display:swap;font-family:Fira Mono;font-style:normal;font-weight:700;src:url(/fonts/fira-mono-v14-latin-700.woff2)format("woff2")}:root{--font-size-1:1.125rem;--font-size-2:1.406rem;--font-size-3:1.758rem}*,:after,:before{box-sizing:border-box}*{margin:0}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;scroll-behavior:smooth}body{background-color:oklch(.68,.03,261.69);color:oklch(.3,.04,279.69);font:var(--font-size-1)/1.75 Fira Mono;width:100%;max-width:100%;display:flex}main{width:100%;max-width:640px;margin-block-start:4rem;margin-inline:auto}h1,h2{margin-block:3rem 1.5rem}h1{font:var(--font-size-3)/1.3 Josefin Sans}h2{font:var(--font-size-2)/1.3 Josefin Sans}p{margin-block:0 1rem;margin-inline:0;padding:0}canvas{width:640px;max-width:100%;height:auto;margin-block:3rem 1.5rem;padding:0;overflow:hidden}
    </style>
  </head>
  <body>
    <main>
      <h1>Square Eater</h1>
      <canvas id="glcanvas" tabindex="1"></canvas>
      <h1>How to play</h1>
      <p>
        Eat as many squares as you can before the swarm gets you! Eat yellow
        squares to repel the swarm.
      </p>
      <script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
      <script>
        load("game.wasm");
      </script>
    </main>
  </body>
</html>
```

I have inlined the CSS here, and you can use Rust-based tooling like <a aria-label="lightning C S S to minify and bundle C S S" href="https://lightningcss.dev/">Lightning CSS to minify and bundle CSS</a> here.  You might also want to create a <a href="https://doc.rust-lang.org/cargo/reference/build-scripts.html">Rust build script</a> to generate the HTML from a template, using the <a href="https://crates.io/crates/askama">`askama` crate</a> (works a little like Jinja).

Remember to add any favicons referenced in the HTML rel tags to the public folder.

## 🚧 Local Development and Testing Server using Axum

We just need Axum to run as a static file server, and can set this up in a few source files.  Create `src/bin/server.rs` with this content:

```rust
use shared::app;
  
#[tokio::main]
async fn main() {
    let listen_address = "0.0.0.0:8000";
    let listener = tokio::net::TcpListener::bind(listen_address).await.unwrap();
    println!("Local    http://{listen_address}/");
    axum::serve(listener, app()).await.unwrap();
}
```

This references the shared library, mentioned before.  We can create it now, adding a `src/lib.rs` file:

```rust
use cfg_if::cfg_if;
 
cfg_if! {
    if #[cfg(feature = "game")] { } else {
        mod routes;
 
        use axum::{routing::get, Router};
        use routes::health_check;
        use tower_http::services::{ServeDir, ServeFile};
 
        pub fn app() -> Router {
            Router::new()
                .route("/health_check", get(health_check))
                .fallback_service(
                    ServeDir::new("public").not_found_service(ServeFile::new("public/index.html")),
                )
        }
    }
}
```

This will serve files in the `public` folder from the `/` route, and just serve the `index.html` file if the requested path is not found.  See Auxm docs for more sophisticated setup.

Finally, we can add the mentioned `health_check` route in `src/routes/health_check.rs`:

```rust
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde::Serialize;

#[derive(Serialize)]
struct Health {
    healthy: bool,
}

pub async fn health_check() -> impl IntoResponse {
    let health = Health { healthy: true };

    (StatusCode::OK, Json(health))
}
```

And, include that file in the source tree by adding `src/routes/mod.rs`:

```rust
pub mod health_check;
 
pub use health_check::health_check;
```

As a last step, here, we need to update `Cargo.toml` with Axum dependencies.  We only use those dependencies when building the local or Shuttle binaries, not the game.  For that reason, we make them optional, and will update the game build command to skip them.

Here is the final `Cargo.toml`:

```toml
[package]
name = "square_eater"
version = "0.1.0"
edition = "2021"
 
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
path = "src/lib.rs"
name = "shared"
 
[[bin]]
path = "src/bin/server.rs"
name = "local-server"
 
[[bin]]
path = "src/bin/shuttle.rs"
name = "square-eater"
 
[[bin]]
path = "src/bin/main.rs"
name = "game"
 
[dependencies]
axum = { version = "0.7.4", optional = true }
cfg-if="1.0.0"
macroquad = "0.4.4"
miniquad = "0.3.12"
sapp-wasm = "=0.1.26"
serde = { version = "1.0.196", features = ["derive"] }
shipyard = { version = "0.6.2", default-features = false, features = ["proc", "std"] }
shuttle-axum = { version = "0.38.0", optional = true }
shuttle-runtime = { version = "0.38.0", optional = true }
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"], optional = true }
tower-http = { version = "0.5.1", features = ["fs", "trace"], optional = true }
 
[features]
default = ["shuttle"]
game = []
local_server = [
  "dep:axum",
  "dep:tokio",
  "dep:tower-http",
]
shuttle = [
  "dep:axum",
  "dep:shuttle-axum",
  "dep:shuttle-runtime",
  "dep:tokio",
  "dep:tower-http",
]
```

I also added the shuttle dependencies, which we use in the next section.

To build just the game, now, we can run:

```shell
cargo build --bin game --release --target wasm32-unknown-unknown --features=game --no-default-features
```

And to test the local server:

```shell
cargo run --bin local-server --features=local_server
```

Try that command, and jump to `http://127.0.0.1:8000` in your browser, and you should see your game, in-browser.

## 🚀 Shuttle Configuration and Deploy

To get going with Shuttle, you will need the CLI app installed locally.  To install, run:

```shell
cargo install cargo-shuttle
```

This will probably take a few minutes to download and compile.

Next, create a `Shuttle.toml` file in the project root directory, with the following content:

```toml
name = "square-eater" # REPLACE with your own project name
```

Then, we need to add the source code for the shuttle binary file, which Shuttle will run when they deploy your app.  This file will is tiny, because we are using the shared Axum-based library, created earlier:

```rust
use shared::app;
 
#[shuttle_runtime::main]
async fn main() -> shuttle_axum::ShuttleAxum {
    Ok(app().into())
}
```

### Shuttle Deploy

To deploy, first create a project, from the Terminal (remember your project name has to be unique for this to work):

```shell
cargo shuttle project start
```

Then deploy:

```shell
cargo shuttle deploy
```

The binary will compile on Shuttle servers, and you will see output as it compiles. Your game site should now be accessible from the URL `https://YOUR-PROJECT-NAME.shuttleapp.rs`!

## 🙌🏽 Deploying your Rust WASM Game: Wrapping Up

In this post on deploying your Rust WASM game, we saw how you can deploy a Macroquad WASM game with Shuttle and Axum.  In particular, we saw:

- how to **Cargo targets to build multiple binaries**;
- how to serve a **static file server with Axum**; and
- the process for **deploying a Rust app to Shuttle**.

I hope you found this useful.  You can find the <a href="https://github.com/rodneylab/square_eater">full source code for the project in a Rodney Lab GitHub repo</a>.  Please share links for WASM games you create.  Also, let me know if there is anything I could improve in this content to make it easier to follow, or improve your experience.

## 🙏🏽 Deploying your Rust WASM Game: Feedback

If you have found this post useful, see links below for further related content on this site.  Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on X, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on X (previously Twitter) and also, join the <a href="https://matrix.to/#/%23rodney:matrix.org">#rodney</a> Element Matrix room. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact/">further ways to get in touch with Rodney Lab</a>. I post regularly on <a href="https://rodneylab.com/tags/gaming/">Game Dev</a> as well as <a href="https://rodneylab.com/tags/rust/">Rust</a> and <a href="https://rodneylab.com/tags/c++/">C++</a> (among other topics). Also, <a aria-label="Subscribe to the   Rodney Lab newsletter" href="https://newsletter.rodneylab.com/issue/latest-issue">subscribe to the newsletter to keep up-to-date</a> with our latest projects.