Docker Desktop and Spin for Serverless WebAssembly Apps
Matt Butcher
docker
docker desktop
rust
Docker Desktop can run Fermyon Spin applications. Using containerd’s extension architecture, Docker Desktop can pull specially built container images that have Spin WebAssembly applications, and it can execute those alongside regular containers.
In this article, we’ll walk through the process of enabling these features, and then building and deploying a Spin application in Docker Desktop.
We are about to create and run a WebAssembly (Wasm) application in Docker. Docker’s Wasm workload functionality is in Beta, hence this new functionality does have a troubleshooting section on Docker’s documentation website. Similarly, this article has a troubleshooting section at the very end (be sure to refer to these sections if you encounter any errors). Let’s get started by enabling Wasm support in Docker.
Enabling Wasm Support in Docker
You will need Docker Desktop 4.21 or newer to use this feature. Please upgrade if you haven’t already done so.
As we mentioned, Wasm support is still an in-development (Beta) feature of Docker Desktop. Wasm support is disabled by default. To turn it on, open your Docker Desktop settings menu (click the gear icon in the top right corner of the navigation bar). Click Extensions from the menu on the left and ensure that boxes relating to Docker Marketplace and Docker Extensions system containers are checked (as shown in the image below). Checking these boxes enables the Features in development extension.
Please ensure that you press Apply and restart to save any changes.
Click on Features in development from the menu on the left, and enable the following two options:
Use containerd for pulling and storing images
: This turns on containerd support, which is necessary for Wasm.
Enable Wasm
: This installs the Wasm subsystem, which includes containerd shims and Spin (among other things).
Make sure you press Apply and restart to save the changes.
Now that Docker Desktop is Wasm-ready, we can build a quick and trivial application to test it out.
Creating a Simple Spin Application
We don’t need to do much for testing. For our purposes, we’ll just create a simple Rust app. If Rust is not your thing, the Spin Quickstart covers other languages like JavaScript, TypeScript, Python, and Go. Language doesn’t matter in this tutorial.
I have already installed Rust as well as the wasm32-wasi
compiler target.
We can now go ahead and create our application:
$ spin new http-rust hello-docker --accept-defaults
$ cd hello-docker
Configuration the Application
Next, we open the Cargo.toml
file and modify it to meet the following criteria:
- That the
spin-sdk
dependency has its tag
key set to v1.3.0
.
- That the
wit-bindgen-rust
dependency is being used with the same git
and rev
values as below. (Add this line to the [dependencies]
section of your Cargo.toml
file, if it does not already exist).
[dependencies]
// --snip--
# The Spin SDK.
spin-sdk = { git = "https://github.com/fermyon/spin", tag = "v1.3.0" }
# Crate that generates Rust Wasm bindings from a WebAssembly interface.
wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba" }
// --snip--
Building the Application
With the above configuration in place, we can now go ahead and build our application:
$ spin build
Building component hello-docker with `cargo build --target wasm32-wasi --release`
// --snip--
Finished building all Spin components
The above command builds the Wasm binary. Different compiler toolchains put the Wasm modules in different places. With Rust, the Wasm binary will be here:
target/wasm32-wasi/release/hello_docker.wasm
That will be important when we create a Docker container.
Note that we didn’t edit any Rust source code. By default, spin new http-rust
creates a “Hello world” program, and that is good enough for us.
If you’d like to give your app a quick test locally (before proceeding) you can use the spin up
command:
$ spin up
Logging component stdio to ".spin/logs/"
Serving http://127.0.0.1:3000
Available Routes:
hello-docker: http://127.0.0.1:3000 (wildcard)
Sending a request to the application will return a response object (with a status 200
, header of foo:bar
and a body containing Hello, Fermyon
):
curl -i localhost:3000
HTTP/1.1 200 OK
foo: bar
Hello, Fermyon
You can also give your app a quick test in Fermyon Cloud by using the spin deploy
command.
Creating a Container
Docker Desktop works a little differently than other Wasm runtimes. While other runtimes (Spin included) package the Wasm binaries in OCI artifacts, Docker Desktop packages them in scratch images. This is not a big deal, but it does mean we need to create a Dockerfile
and then build a special image.
In this next step, we create a new file called Dockerfile
(which lives in the hello-docker
directory next to spin.toml
) and then populate our new Dockerfile
with the following content:
FROM scratch
COPY spin.toml /spin.toml
COPY target/wasm32-wasi/release/hello_docker.wasm /target/wasm32-wasi/release/hello_docker.wasm
ENTRYPOINT ["/spin.toml"]
The content in the new Dockerfile
does four things:
- It declares this image to be created from
scratch
instead of from a Linux or Windows image.
- It copies the
spin.toml
file into the empty image.
- It copies the
hello-docker.wasm
file into the empty image. Note that it copies it to a similarly named directory structure. The reason for this is the spin.toml
refers to the exact path of the Wasm file.
- Specifies that we are running the application as an executable.
Once we have the Dockerfile
written, we can build it. The command is a little lengthy and requires personalizing (replace technosophos
with your own dockerhub username) before running:
$ docker buildx build --platform wasi/wasm --provenance=false -t docker.io/technosophos/hello-docker:latest .
[+] Building 0.1s (6/6) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 200B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 437B 0.0s
=> CACHED [1/2] COPY spin.toml /spin.toml 0.0s
=> CACHED [2/2] COPY target/wasm32-wasi/release/hello_docker.wasm /target/ 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => exporting manifest sha256:f17a2f8f5c53b95b72d9cda85fd074c7125e35d563 0.0s
=> => exporting config sha256:df98b0b3e98904542236da48ae7b44af8d5f7c40913d 0.0s
=> => naming to docker.io/technosophos/hello-docker:latest 0.0s
=> => unpacking to docker.io/technosophos/hello-docker:latest 0.0s
In this example, we have built a container image named docker.io/technosophos/hello-docker:latest
and copied our Spin app into the container.
At this point, we have a Docker image that contains our Wasm module. We can check to see if Docker sees this image:
$ docker image ls | grep hello
technosophos/hello-docker latest 940456fb017f 2 minutes ago 2.67MB
Now we are ready to run it!
Running a Spin Wasm App in Docker Desktop
It is best to run Wasm images using the docker
CLI. While it may be possible to run them through the Docker Desktop UI, it appears that not all of the necessary options are yet supported there.
To run it on the CLI, use a command like this (making sure to substitute technosophos
for your username when referring to the image’s name):
$ docker run -i --runtime=io.containerd.spin.v1 --platform=wasi/wasm -p 3000:80 docker.io/technosophos/hello-docker:latest
This command should neither exit nor print any output yet. Because we used -i
, anything that spin
prints should log to the console.
The --runtime
option tells Docker to use the io.containerd.spin.v1
executor instead of the default executor (which runs containers instead of Wasm images).
The --platform
option tells Docker to expect an architecture of wasi/wasm
. Otherwise it will default to the architecture of your system (e.g. linux/arm64/v8
).
Finally, -p 3000:80
tells Docker to map Spin’s 80 port (inside the container) to the localhost:3000
port (outside the container) so that we can access it.
Here’s what it looks like with curl
:
$ curl 127.0.0.1:3000/
Hello, Fermyon
We’ve now created a new Wasm serverless app, packaged it into a scratch container, and run it using Docker Desktop. At this point, you may want to go back to the code and edit it a bit to try things out. Just remember, each time you are ready to give it a test run, you will need to:
- Run the
spin build
command to build the Wasm file.
- Run the
docker buildx
command to rebuild the container image.
- Restart the
docker run
command so it picks up the latest changes.
Conclusion
Docker Desktop is good for running simple Spin applications. You will need to enable Wasm support the first time you use Docker Desktop for Wasm. Then there are just a few extra steps to do with your Spin app. Along with compiling to Wasm, you will need to copy your spin.toml
and .wasm
files both into a scratch container (using the usual Dockerfile
format), but then you can run your app just like you would run any Docker container.
Ready to move beyond this simple example? Head over to the Spin Quickstart and start building your own app. Thanks for following along.
Troubleshooting (Optional)
Error: Unable to listen on 127.0.0.1:3000
The Unable to listen
error message (when trying to run your application using spin up
) is due to another process that is already listening on port 3000. Please ensure that no other applications are sharing this port before running spin up
.
error[E0463]: can't find crate for `core`
If you encounter an error with the above text, when performing the spin build
command, check out the Quickstart for tips on installing the wasm32-wasi
target.
docker: Error response from daemon: failed to create task for container: failed to start shim: failed to resolve runtime path: runtime "io.containerd.spin.v1" binary not installed "containerd-shim-spin-v1": file does not exist: unknown.
ERRO[0000] error waiting for container:
If you have an error similar to the above console output, please verify that your system is capable of running a container in Docker Desktop without the Wasm feature enabled. For example, can you run docker run -it redis:latest
successfully without Beta features enabled? This simple test ensures that the entire container initialization process of Docker Desktop is active and functioning as intended.
docker: Error response from daemon: Others("could not build spin trigger: Failed to instantiate component 'hello-docker'"): unknown.
ERRO[0000] error waiting for container:
If you encounter the above could not build spin trigger
error, please check that you have updated your Cargo.toml
file as recommended in this article and that your Dockerfile
content matches the example provided in this article.
error[E0433]: failed to resolve: use of undeclared crate or module `wit_bindgen_rust`
If you get the above undeclared crate or module
error when building the application with spin build
please ensure that your Cargo.toml
file has the wit-bindgen-rust
dependency configured (as recommended in this article).
curl: (52) Empty reply from server
The most common reason for the above error is not specifying the -p
values correctly. Please use -p 3000:80
when using the docker run
command (as shown in this article). This --publish
flag tells Docker to publish the container’s port of 80
, to the localhost:3000
port (outside the container) which makes the container accessible i.e. when using curl -i localhost:3000
.
If you encounter any other issues please reach out on Discord or GitHub.