https://wasmlabs.dev/articles/docker-without-containers/ Wasm Labs @ VMware OCTO * Articles * Projects * About * @vmwwasm * WebAssembly: Docker without containers! By [alexandrov] Asen Alexandrov At 2022 / 12 20 mins reading This is a companion article to a talk about Docker+WebAssembly that we gave at "Docker Community All Hands 7, Winter Edition" on Dec 15th, 2022. Introduction Recently Docker announced support for WebAssembly in cooperation with WasmEdge. This article will explain what is WebAssembly, why it is relevant to the Docker ecosystem and provide some hands-on examples to try on. We assume you are familiar with the Docker tooling. We will be using our work on the WebAssembly port of PHP to demonstrate how to build a PHP interpreter, package it as part of an OCI image and run it using Docker. Note that this article focuses on getting some hands-on experience rather than discussing technical details. You can either reproduce the examples below or just read through them till the end as we will also provide the output. WebAssembly - What? and Why? This is a very basic introduction. If you are already familiar with the technology you can skip to the next section. What is WebAssembly? WebAssembly (or Wasm) is an open standard that defines a binary instruction format, which allows the creation of portable binary executables from different source languages. Wasm is in all browsers These binaries can run in a variety of environments. It has its origins in the web and is supported by all major browsers. How does Wasm work in browsers? Browser engines integrate a Wasm virtual machine, usually called a Wasm runtime, which can run the Wasm binary instructions. There are compiler toolchains (like Emscripten) that can compile source code to the Wasm target. This allows for legacy applications to be ported to a browser and directly communicate with the JS code that runs in client-side Web applications. Wasm in a browser These technologies have allowed traditional desktop apps to run in a browser. And now they can run on any device on which you have a browser. Some notable examples are Google Earth and the Open CV library for computer vision. How does Wasm work on servers? There are Wasm runtimes that can run outside of the browser, including traditional operating systems such as Linux, Windows and macOS. Because they cannot rely on a JavaScript engine being available they communicate with the outside world using different interfaces, such as WASI, the WebAssembly System Interface. These runtimes allow Wasm applications to interact with their host system in a similar (but not quite the same) way as POSIX. Projects like WASI SDK and wasi-libc help people compile existing POSIX-compliant applications to WebAssembly. Wasm on the server You only need to compile an application into a Wasm module once, and then you can run the exact same binary everywhere. What's great about Wasm? Some of the features that make Wasm great in browsers also make it attractive for server-side development: Open - it is a standard widely adopted in the industry. In contrast to the browser wars of the past, major companies are collaborating for the standardization of WASI and WebAssembly applications. Fast - it can offer native-like speed via the JIT/AOT capabilities of most runtimes. No cold starts, unlike booting a VM or starting a container. Secure - the Wasm runtime is sandboxed by default and allows for safe access to memory. The capabilities-based model ensures that a Wasm application can access only what it is explicitly allowed to. There is better supply chain security. Portable - across the several major runtimes there is support for most CPUs (x86, ARM, RISC-V) and most OS-es including Linux, Windows, macOS, Android, ESXi, and even non-Posix ones. Efficient - Wasm applications can be made to run with minimal memory footprint and CPU requirements. [?] Polyglot - 40+ languages can be compiled to Wasm, with modern, constantly improving toolchains. The next step in server platform evolution? You may have seen this quote from Solomon Hykes (one of the co-founders of Docker): If WASM+WASI existed in 2008, we wouldn't have needed to create Docker. That's how important it is. WebAssembly on the server is the future of computing. Indeed, WASM+WASI does seem to be the next step in the evolution of server-side software infrastructure. * Back in the day, we had physical hardware to work on. We would meticulously install OS-es and applications on each box and maintain them all one by one. * Then with the adoption of VMs, pioneered by VMware, things became easier. People could copy, clone and move VMs across hardware boxes. But that still kept the need to install OS-es and applications in VMs. * Then came containers, popularized by Docker, which made it easier to run application configurations in a minimalistic wrapping context, without affecting any other applications on the host OS. However, that still kept the need to distribute applications bundled with their runtimes and necessary libraries. The security boundary was provided by the Linux kernel * We now have WebAssembly. Its technical features and portability make it possible to distribute the application, without requiring shipping OS-level dependencies and can run with strict security constraints. Given all this, it is common for developers to take a look at WebAssembly as the 'successor' to containers and the next logical step in infrastructure deployment. Wasm is the next step in the server platform evolution However, another way of looking at WebAssembly is as an alternative 'backend' for Docker tooling. You can use the same command line tools and workflows, but instead of using Linux containers, it is implemented using WebAssembly-based container equivalents. The rest of the article explores this concept and this is what we referred to with the "Docker without containers" title. How does Wasm work with Docker? Docker Desktop now includes support for WebAssembly. It is implemented with a containerd shim that can run Wasm applications using a Wasm runtime called WasmEdge. This means that instead of the typical Windows or Linux containers which would run a separate process from a binary in the container image, you can now run a Wasm application in the WasmEdge runtime, mimicking a container. As a result, the container image does not need to contain OS or runtime context for the running application - a single Wasm binary suffices. This is explained in detail in Docker's Wasm technical preview article. What is WasmEdge? WasmEdge is a High-Performance WebAssembly Runtime that: * Is Open Source, part of the CNCF. * Supports all major CPU architectures (x86, ARM, RISC-V). * Supports all major Operating Systems (Linux, Windows, macOS) as well as others such as seL4 RTOS, Android. * Is Optimized for cloud-native and Edge applications. * Is Extensible and supports standards and emerging technologies + AI Inference with Tensorflow, OpenVINO, PyTorch + Async networking with Tokio. Supports microservices, database clients, message queues, etc. + Integrates seamlessly with containers ecosystem, Docker and Kubernetes (as shown in this article!) What about interpreted languages? So far we have only mentioned compiled languages such as C and Rust can target WebAssembly. For interpreted languages such as Python, Ruby and PHP, the approach is different: their interpreters are written in C and can be compiled to WebAssembly. Then this interpreted compiled to Wasm can be used to execute the source code files, typically ending in .py, .rb, .php and so on. Once compiled to Wasm, any platform with a Wasm runtime will be able to run those interpreted languages even if the actual interpreter was never compiled for that platform natively. Wasm on the server for interpreted languages The hands-on examples Let's get started! In the hands-on examples, we will use the PHP interpreter compiled to Wasm. We will: * Build a Wasm container. * Compare Wasm and native binaries. * Compare traditional and Wasm containers. * Showcase Wasm's portability Prerequisites If you want to reproduce the examples locally you will need to prepare your environment with some or all of the following: * WASI SDK - to build WebAssembly applications from legacy C code * PHP - to run a native PHP binary for the sake of comparison * WasmEdge runtime - to run WebAssembly applications * Docker Desktop + Wasm (at the time of this writing, available as stable beta in version 4.15) to be able to run Wasm containers We are also leveraging the "Wasm Language Runtimes" repository, which provides ways to build the PHP interpreter as a WebAssembly application. You can start by checking out the demo branch like this: git clone --depth=1 -b php-wasmedge-demo \ https://github.com/vmware-labs/webassembly-language-runtimes.git wlr-demo cd wlr-demo Building a Wasm container As a first example, we will showcase how to build a C-based application like the PHP interpreter. The build uses WASI-SDK set of tools. It includes a clang compiler that can build to the wasm32-wasi target as well as wasi-libc which implements the basic POSIX system call interfaces on top of WASI. With WASI SDK we can build a Wasm module out of PHP's codebase, written in C. After that, it takes a very simple Dockerfile based on scratch for us to make an OCI image that can be run with Docker+Wasm. From C code to Wasm container Building a WASM binary Assuming you are in the wlr-demo folder which you checked out as part of the prerequisites section you could run the following to build a Wasm binary. export WASI_SDK_ROOT=/opt/wasi-sdk/ export WASMLABS_RUNTIME=wasmedge ./wl-make.sh php/php-7.4.32/ && tree build-output/php/php-7.4.32/bin/ ... ( a few minutes and hundreds of build log lines) build-output/php/php-7.4.32/bin/ +-- php-cgi-wasmedge +-- php-wasmedge PHP is built with autoconf and make. So if you take a look at the scripts/wl-build.sh script you will notice that we set up all relevant variables like CC, LD, CXX, etc. to use the compiler from WASI_SDK. export WASI_SYSROOT="${WASI_SDK_ROOT}/share/wasi-sysroot" export CC=${WASI_SDK_ROOT}/bin/clang export LD=${WASI_SDK_ROOT}/bin/wasm-ld export CXX=${WASI_SDK_ROOT}/bin/clang++ export NM=${WASI_SDK_ROOT}/bin/llvm-nm export AR=${WASI_SDK_ROOT}/bin/llvm-ar export RANLIB=${WASI_SDK_ROOT}/bin/llvm-ranlib Then, digging further into php/php-7.4.32/wl-build.sh you can see that we use the autoconf build process as usual. ./configure --host=wasm32-wasi host_alias=wasm32-musl-wasi \ --target=wasm32-wasi target_alias=wasm32-musl-wasi \ ${PHP_CONFIGURE} || exit 1 ... make -j ${MAKE_TARGETS} || exit 1 WASI is a work in progress and many of the POSIX calls still can not be implemented on top of it. So to build PHP we had to apply several patches on top of the original codebase. We saw above that the output binaries go to build-output/php/ php-7.4.32. In the following examples we will use the php-wasmedge binary that is specifically built for WasmEdge as it offers server-side socket support, which is not yet part of WASI. Optimizing the binary Wasm is a virtual instruction set so the default behavior of any runtime would be to interpret those instructions on the fly. Of course, this could make things slow in some cases. So to get the best of both worlds with WasmEdge you can create an AOT (ahead-of-time) optimized binary that runs natively on the current machine but can still be interpreted on other ones. To create that optimized binary run the following: wasmedgec --enable-all --optimize 3 \ build-output/php/php-7.4.32/bin/php-wasmedge \ build-output/php/php-7.4.32/bin/php-wasmedge-aot We will use this build-output/php/php-7.4.32/bin/php-wasmedge-aot binary in the following examples. To get to know more about the WasmEdge AOT optimized binaries take a look here. Building the OCI image Now that we have a binary, we can wrap it up in an OCI image. Let's take a look at images/php/Dockerfile.cli. All we need to do is just copy the Wasm binary and set it as ENTRYPOINT. FROM scratch ARG PHP_TAG=php-7.4.32 ARG PHP_BINARY=php COPY build-output/php/${PHP_TAG}/bin/${PHP_BINARY} /php.wasm ENTRYPOINT [ "php.wasm" ] We could also add more content to the image, which will be accessible to the Wasm binary when it is run by Docker. For example in images/ php/Dockerfile.server we also add some docroot content to be served by php.wasm when the container starts. FROM scratch ARG PHP_TAG=php-7.4.32 ARG PHP_BINARY=php COPY build-output/php/${PHP_TAG}/bin/${PHP_BINARY} /php.wasm COPY images/php/docroot /docroot ENTRYPOINT [ "php.wasm" , "-S", "0.0.0.0:8080", "-t", "/docroot"] Based on the above files we can easily build our php-wasm images locally. docker build --build-arg PHP_BINARY=php-wasmedge-aot -t ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot -f images/php/Dockerfile.cli . docker build --build-arg PHP_BINARY=php-wasmedge-aot -t ghcr.io/vmware-labs/php-wasm:7.4.32-server-aot -f images/php/Dockerfile.server . Native vs Wasm Now let's compare a native PHP binary with a Wasm binary. Both locally and in a Docker container. We will use the same index.php file and compare the results we get when running it with: * php, * php-wasmedge-aot, * php in a traditional container, * php-wasmedge-aot in a Wasm container. Running index.php We will use the same images/php/docroot/index.php file in all of the below examples so let's take a look. In a nutshell, this script will: * use phpversion and php_uname to show the interpreter version and the platform on which it is running * print the names of all environment variables that the script can access * print a hello message with the current time and date * list the contents of the root folder /