Rust — WebAssembly — Docker Integration

Paras Bansal
8 min readMar 11, 2023

I am a newbie to WebAssembly and with this story I’ll be sharing my learning on this topic. A lot of information is there on web, so I’ll cover whatever I could learn.

WebAssembly (a.k.a Wasm)

For a long time developers have been using Javascript for providing customized experience on web. There are other web scripting languages as well, but all of them have limitations. For enhancing the experience developers are using binary plugins for different web browsers. Still, it leaves a gap on executing an app with advanced features as a WebApp. This is where WebAssembly helps.

WebAssembly is a open standard that defines a portable binary format for executable programs and is supported by many modern web browsers. Now developers can develop code in languages like C/C++/Rust/Go and many more and compile with WebAssembly to create a native byte code which can be run in a web browser. Typically WebAssembly runtime is collocated with Javascript Engine, but it still is sandboxed. So for it to talk to resources like Javascript, HTML, CSS etc. or system resources, it needs bindings with Javascript. So for now, WebAssembly can not work on its own, it still needs Javascript engine.

WebAssembly can replace the slow computations done in Javascript and provide near native speed. The way it works is when developers write code in native languages, they can use WebAssembly as compilation target. WebAssembly then produces byte code which can be run in WebAssembly Runtime provided by browser. Some examples are games like Doom3 and web applications like Autocad and Figma which are now served as a WebApp via any browser. The biggest advantage here is that apps are now host independent i.e. same app works irrespective of OS and CPU architecture.

In addition to browsers, WebAssembly bytecode can also be containerized and executed as docker containers. This opens a whole new world of possibilities. Let’s see below.

A comparison between docker and WebAssembly

Docker provides a way to package an application and its dependencies (OS dependencies and libraries) into a image. Developers can run various instances of that image as containers. This is enabled by container runtime which can be Docker engine, runc, containerd etc. So basically what happens is that container runtime can start various instances of the application as different containers and it also enables communication with host OS resources.

In case of WebAssembly, WebAssembly Runtime enables the code execution. This runtime is provided by all modern browsers. The other examples of WebAssembly Runtime are Wasmedge, NodeJs etc. The communication with system resources is enabled by WebAssembly System Interface as shown in diagram above.

containerd — is a popular docker container runtime environment. Currently both Docker and Kubernetes support it. Folks at Docker and Wasmedge are working on enabling containerd for Wasmedge runtime environment. This will enable execution of WebAssembly apps as docker containers. The advantages of running a WebAssembly app as a docker container —

  • No base image needed, so containers size is very small, basically the size of compiled byte code itself
  • Near native speed for container start and execution

Let’s get into the demo app.

For this demo, we’ll build a Rust app that gives sum of 2 numbers. We’ll run the app as a WebAssembly app in browser and also package it as a docker container.

Let’s begin with installation first —

  1. Install Rust — https://www.rust-lang.org/learn/get-started
  2. Install Wasm pack — https://rustwasm.github.io/wasm-pack/installer/
  3. Optional install cargo-generate — to start template projects
  4. Optional install npm — to test/run javascript apps
  5. Install Docker Desktop — Enable containerd for pulling and storing images and also enable experimental feature (if not enabled)

WebApp

Let’s start with WebApp creation first. There is a small difference when you have to create the app as a Docker container, I’ll show you in the next section.

Create a new project as a lib module using cargo —

cargo init --lib

This will create the outline of project with Cargo.toml and lib.rs files.

Cargo.toml —

[package]
name = "rust-wasm-docker-hello-world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
wasm-bindgen = "0.2.63" ##ENABLE THIS FOR WEBAPP ONLY

[lib]
crate-type = ["cdylib"] ##ENABLE THIS FOR WEBAPP ONLY

lib.rs —

use wasm_bindgen::prelude::*; //ENABLE THIS FOR WEBAPP ONLY

#[wasm_bindgen] //ENABLE THIS FOR WEBAPP ONLY
pub fn add(left: usize, right: usize) -> usize {
left + right
}

Please see the comments on the cargo.toml and lib.rs ENABLE THIS FOR WEBAPP ONLY You need to enable those when you are creating a webapp only. When creating the docker, you need to comment these lines.

We are using wasm-bindgen here to bind functions with Javascript, so Javascript can make the function call.

As per documentation — The wasm-bindgen tool is sort of half polyfill for features like the host bindings proposal and half features for empowering high-level interactions between JS and wasm-compiled code (currently mostly from Rust). More specifically this project allows JS/wasm to communicate with strings, JS objects, classes, etc, as opposed to purely integers and floats. Using wasm-bindgen for example you can define a JS class in Rust or take a string from JS or return one.

#wasm-pack build --target web

[INFO]: Checking for the Wasm target...
[INFO]: Compiling to Wasm...
Compiling proc-macro2 v1.0.51
Compiling quote v1.0.23
Compiling unicode-ident v1.0.8
Compiling syn v1.0.109
Compiling log v0.4.17
Compiling wasm-bindgen-shared v0.2.84
Compiling cfg-if v1.0.0
Compiling bumpalo v3.12.0
Compiling once_cell v1.17.1
Compiling wasm-bindgen v0.2.84
Compiling wasm-bindgen-backend v0.2.84
Compiling wasm-bindgen-macro-support v0.2.84
Compiling wasm-bindgen-macro v0.2.84
Compiling rust-wasm-docker-hello-world v0.1.0 (C:\Users\email\OneDrive\Documents\Local\IntelliJ_Projects\rust-wasm-docker-hello-world)
Finished release [optimized] target(s) in 10.45s
[INFO]: Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: :-) Done in 10.94s
[INFO]: :-) Your wasm pkg is ready to publish at C:\Users\email\OneDrive\Documents\Local\IntelliJ_Projects\rust-wasm-docker-hello-world\pkg.

You’ll notice a pkg folder is created with Javascript and wasm file. Create a boilerplate html and configure the Wasm app using Javascript as below -

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HTML 5 Boilerplate</title>
</head>
<body>
<script type="module">
import init, { add } from '../pkg/rust_wasm_docker_hello_world.js'
await init()
console.log(add(1,3))
</script>
</body>
</html>

Notice here how we called the “add” function that we created in the Wasm app by importing using Javascript. As I mentioned above, wasm-bindgen here enables the communication between Javascript and Wasm bytecode. I used IntelliJ to open the file in a webbrowser which created a local server for me. You can use NodeJs or any other server to host your app and open it with webbrowser. As you see below, the console is printing the output.

Docker

Now to containerize this app, we need to build the image which can host our wasm file. Before that, we need a main.rs file.

Docker entry into the app is via main function only. Docker will use Wasmedge environment to execute the code present in the main function.

Before you proceed, you need to disable the lines in lib.rs and Cargo.toml which say — ENABLE THIS FOR WEBAPP ONLY

main.rs —

mod lib;
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let num1 = &args[1];
let num2 = &args[2];
let sum = lib::add(num1.parse::<usize>().unwrap(), num2.parse::<usize>().unwrap());
println!("Sum of 2 numbers is {}", sum);
}

Test your app —

#cargo run 1 2

Finished dev [unoptimized + debuginfo] target(s) in 1.47s
Running `target\debug\rust-wasm-docker-hello-world.exe 1 2`
Sum of 2 numbers is 3

Once done, compile/build the app for WASM —

#cargo build --target wasm32-wasi --release
Compiling rust-wasm-docker-hello-world v0.1.0 (C:\Users\email\OneDrive\Documents\Local\IntelliJ_Projects\rust-wasm-docker-hello-world)
Finished release [optimized] target(s) in 0.64s

Create the Dockerfile —

FROM scratch
COPY ./target/wasm32-wasi/release/rust-wasm-docker-hello-world.wasm /rust-wasm-docker-hello-world.wasm
ENTRYPOINT [ "rust-wasm-docker-hello-world.wasm" ]

We are using scratch, that means there is no base image provided. So the image contains application bytecode only.

Building the image —

#docker buildx build --platform wasi/wasm32 -t paras301/docker-wasm:latest .

[+] Building 0.0s (0/0)
[+] Building 0.9s (5/5) FINISHED

#docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
paras301/docker-wasm latest 260a6c09a8a9 2 hours ago 522kB

#docker push paras301/docker-wasm:latest
8e4811a477d2: Pushed
bf4fd0b11396: Pushed
260a6c09a8a9: Pushed
latest: digest: sha256:260a6c09a8a981b74ac3c0f467115befb32d478ccd12e8c799ed7e317abbcc5f, size: 526

#docker image inspect paras301/docker-wasm:latest | grep -A 3 "Architecture
"Architecture": "wasm32",
"Os": "wasi",
"Size": 522400,
"VirtualSize": 522400,

Notice the size of image is just 522Kb and image OS is WASI i.e. WebAssembly System Interface.

Running the docker image —

#docker run --rm --name=docker-wasm --runtime=io.containerd.wasmedge.v1 --platform=wasi/wasm32 paras301/docker-wasm:latest 1 2
Sum of 2 numbers is 3

While running the docker image, need to pass the runtime and the platform, so docker will understand to use the wasmedge as the runtime environment for this particular container.

Now you see the possibilities that Docker and Wasm integration brings. With image sizes in few MBs, developers can start creating containers that gives near native speeds for each requests and warmup time is near 0 as well. Moreover, resource requirements for the same load will be a lot less as well. The only downside as of today, the feature is still in beta and there are a lot of constraints when it comes to compiling high level languages like Java and Python to Wasm bytecode. I am sure the there is a great future for the Wasm Apps and Wasm Docker containers.

That’s it for this post. You can find the code here.

--

--