Skip to main content

Docker + WASM

The Docker Desktop distributes with the WasmEdge Runtime embedded. That allows developers to build, share and run very lightweight containers (i.e., a scratch empty container with only the .wasm file without any Linux OS libraries or files) through Docker tools. Those "WASM containers" are fully OCI-compliant and hence can be managed by Docker Hub. They are cross-platform and can run on any OS / CPU Docker supports (the OS and CPU platform is wasi/wasm). But most importantly, they are 1/10 of the size of a comparable Linux container and start up in 1/10 of the time, as the WASM containers do not need to bundle and start Linux libraries and services.

Together with Docker's capability to containerize developer and deployment environments, you can create and deploy complex applications without installing any dependencies. For example, you could setup a complete Rust and WasmEdge development environment without installing either tool on your local dev machine. You can also deploy a complex WasmEdge app that needs to connect to a MySQL database without having to install MySQL locally.

In this guide, we will cover how to:

Prerequisite

Install Docker Desktop and turn on the containerd image store feature in your Docker Desktop settings.

Docker config

Create and run a Rust program

With Docker + WASM, you can use the entire Rust toolchain in a Docker container to build the WASM bytecode application, and then publish and run the WASM application. The example Rust source code and build instructions are available here.

Build the rust example

In the project directory, run the following command to build the Rust source code into WASM and then package the WASM file into an empty container image. Notice that you do not need to install the Rust compiler toolchain here.

docker buildx build --platform wasi/wasm -t secondstate/rust-example-hello .

The Dockerfile shows how it is done. The Dockerfile has three parts. The first part sets up a Docker container for the Rust build environment.

FROM --platform=$BUILDPLATFORM rust:1.64 AS buildbase
WORKDIR /src
RUN <<EOT bash
set -ex
apt-get update
apt-get install -y \
git \
clang
rustup target add wasm32-wasi
EOT

The second part uses the Rust build environment to compile the Rust source code and generate the WASM file.

FROM buildbase AS build
COPY Cargo.toml .
COPY src ./src
# Build the WASM binary
RUN cargo build --target wasm32-wasi --release

The third part is the essential. It copies the WASM file into an empty scratch container and then set the WASM file as the ENTRYPOINT of the container. It is the container image rust-example-hello built by the command in this section.

FROM scratch
ENTRYPOINT [ "hello.wasm" ]
COPY --link --from=build /src/target/wasm32-wasi/release/hello.wasm /hello.wasm

The WASM container image is only 0.5MB. It is much smaller than a natively compiled Rust program in a minimal Linux container.

Publish the rust example

To publish the WASM container image to Docker Hub, do the following.

docker push secondstate/rust-example-hello

Run the rust example

You can use the regular Docker run command to run the WASM container application. Notice that you do need to specify the runtime and platform flags to tell Docker that this is a non-Linux container and requires WasmEdge to run it.

$ docker run --rm --runtime=io.containerd.wasmedge.v1 --platform=wasi/wasm secondstate/rust-example-hello:latest
Hello WasmEdge!

That's it.

Further reading for the rust example

To see more Dockerized Rust example apps for WasmEdge, check out the following.

Create and run a node.js server

WasmEdge provides a node.js compatible JavaScript runtime. You can create lightweight WASM container images that runs node.js apps. Compared with standard node.js Linux container images, the WASM images are 1/100 of the size, completely portable, and starts up in 1/10 of the time.

In this guide, the example app is an HTTP web server written in node.js. Its source code and build instructions are available here.

Build the node.js example

In the project directory, run the following command to package the WasmEdge JavaScript runtime and the JS HTTP server program into an empty container image.

docker buildx build --platform wasi/wasm -t secondstate/node-example-hello .

The Dockerfile shows how it is done. The Dockerfile has three parts. The first part sets up a Docker container for the wget and unzip utilities.

FROM --platform=$BUILDPLATFORM rust:1.64 AS buildbase
WORKDIR /src
RUN <<EOT bash
set -ex
apt-get update
apt-get install -y \
wget unzip
EOT

The second part uses wget and unzip to download and extract the WasmEdge JavaScript runtime files and the JS application files into a build container.

FROM buildbase AS build
COPY server.js .
RUN wget https://github.com/second-state/wasmedge-quickjs/releases/download/v0.5.0-alpha/wasmedge_quickjs.wasm
RUN wget https://github.com/second-state/wasmedge-quickjs/releases/download/v0.5.0-alpha/modules.zip
RUN unzip modules.zip

The third part is the essential. It copies the WasmEdge JavaScript runtime files and the JS application files into an empty scratch container and then set the ENTRYPOINT. It is the container image node-example-hello built by the command in this section.

FROM scratch
ENTRYPOINT [ "wasmedge_quickjs.wasm", "server.js" ]
COPY --link --from=build /src/wasmedge_quickjs.wasm /wasmedge_quickjs.wasm
COPY --link --from=build /src/server.js /server.js
COPY --link --from=build /src/modules /modules

The WASM container image for the entire node.js app is only 1MB. It is much smaller than a standard node.js image, which is 300+MB.

Publish the node.js example

To publish the WASM container image to Docker Hub, do the following.

docker push secondstate/node-example-hello

Run and test the node.js example

You can use the regular Docker run command to run the WASM container application. Notice that you do need to specify the runtime and platform flags to tell Docker that this is a non-Linux container and requires WasmEdge to run it. Since this is an HTTP server app, you also need to map the container port 8080 to host so that you can access the server from the host.

$ docker run -dp 8080:8080 --rm --runtime=io.containerd.wasmedge.v1 --platform=wasi/wasm secondstate/node-example-server:latest
listen 8080 ...

From another terminal, test the server application.

$ curl http://localhost:8080/echo -X POST -d "Hello WasmEdge"
Hello WasmEdge

That's it.

Further reading for the node.js example

Create and deploy a database driven microservice in Rust

Docker + wasm allows us to build and run WASM containers. However, in most complex applications, the WASM container is only part of the application. It needs to work together with other Linux containers in the system. The Docker compose tool is widely used to compose and manage multi-container deployments. It is installed with Docker Desktop.

In our example microservice application, there is an Nginx web server and a MySQL database. The WASM container is only for the Rust application that accesses the database and processes the HTTP requests (i.e., the application server).

note

For more Docker compose examples, including Linux containers + WASM containers mixed deployments, check out the awesome-compose repo.

Build the microservice example

In the project directory, run the following command to build all three containers: client, server and db.

docker compose up

There is a docker-compose.yml file. It defines the 3 containers needed in this application.

services:
client:
image: nginx:alpine
ports:
- 8090:80
volumes:
- ./client:/usr/share/nginx/html
server:
image: demo-microservice
platform: wasi/wasm
build:
context: .
ports:
- 8080:8080
environment:
DATABASE_URL: mysql://root:whalehello@db:3306/mysql
RUST_BACKTRACE: full
restart: unless-stopped
runtime: io.containerd.wasmedge.v1
db:
image: mariadb:10.9
environment:
MYSQL_ROOT_PASSWORD: whalehello
  • The client container is an Nginx web server
    • Linux container with mapped HTTP port and volume for the static HTML/JS files
  • The server container is a Rust container for the business logic
    • The WASM container is built from Rust source code using this Dockerfile
    • WASM container with mapped web service port and an environment variable for the database connection string
  • The db container is a MySQL database
    • Linux container with a pre-set database password

Deploy the microservice example

Start and run all three containers in the correct order with one command.

docker compose up

Go back to Docker Desktop Dash board, you will see there're three containers running.

Docker

CRUD tests

Open another terminal, and you can use the curl command to interact with the web service.

When the microservice receives a GET request to the /init endpoint, it would initialize the database with the orders table.

curl http://localhost:8080/init

When the microservice receives a POST request to the /create_order endpoint, it would extract the JSON data from the POST body and insert an Order record into the database table. For multiple records, use the /create_orders endpoint and POST a JSON array of Order objects.

curl http://localhost:8080/create_orders -X POST -d @orders.json

When the microservice receives a GET request to the /orders endpoint, it would get all rows from the orders table and return the result set in a JSON array in the HTTP response.

curl http://localhost:8080/orders

When the microservice receives a POST request to the /update_order endpoint, it would extract the JSON data from the POST body and update the Order record in the database table that matches the order_id in the input data.

curl http://localhost:8080/update_order -X POST -d @update_order.json

When the microservice receives a GET request to the /delete_order endpoint, it would delete the row in the orders table that matches the id GET parameter.

curl http://localhost:8080/delete_order?id=2

That's it. Feel free to fork this project and use it as a template for your own lightweight microservices!

Further reading for the microservice example

To learn how Docker + WASM works under the hood, visit the containerd chapter for more details.