# container2wasm: running container images inside WASM runtimes

> container2wasm converts an OCI container image into a WASM image that boots a small virtual machine, so the container runs on wasmtime, wazero or a browser tab. It is experimental, and the architecture you pick decides how slow it is.

**container2wasm/container2wasm** — Container to WASM converter

- Repository: https://github.com/container2wasm/container2wasm
- Website: https://ktock.github.io/container2wasm-demo/
- Stars: 2,792 · Forks: 154
- Language: Go
- License: Apache-2.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/container2wasm-container2wasm

## The gap container2wasm fills between an OCI image and a WASI runtime

A WASI runtime executes a single .wasm module. A container image is a filesystem plus a process tree plus a Linux kernel ABI. Nothing in the WASI specification gives you fork, /proc, or a syscall table, so a normal image cannot simply be recompiled into WASM. container2wasm takes the image, embeds it in a WASM module together with a small machine emulator, and boots a Linux kernel inside that emulator. The README states that it converts a container to WASM using Bochs for x86_64 containers, TinyEMU for riscv64 containers, and QEMU. The target audience is people who already have an image and want it to run somewhere a container runtime is not available: a WASI host, or a browser tab. The README labels the project experimental software, and that label is the honest starting point for any evaluation.

## How the conversion works: emulator, kernel, rootfs in one module

The output of c2w is not a recompiled binary. It is a WASM module containing a virtual machine, a Linux kernel, and the container's root filesystem. When the module starts under a WASI runtime, the emulator begins executing the kernel, the kernel mounts the embedded rootfs, and the container's entrypoint runs inside that guest. This is why the README example of wasmtime out.wasm uname -a prints a Linux banner with a 6.1.0 kernel: you are looking at a guest kernel, not the host. The Dockerfile in the repository shows the build-time inputs that make this work, including ARG VM_MEMORY_SIZE_MB=128, ARG VM_CORE_NUMS=1, and emulator sources pulled from ktock/Bochs, ktock/tinyemu-c2w and ktock/qemu-wasm. Those defaults are worth reading before you benchmark anything: a single core and 128 MB of guest memory is a small machine, and a JVM or a database will not be happy inside it. The repository also carries an OPTIMIZATION_MODE argument with values wizer and native, which tells you the project snapshots the pre-booted state to cut startup cost. The consequence of the design is that the guest is a full Linux, so almost any userspace program works, and almost any program pays emulation cost.

## Installing c2w and converting a first image

The README points at the GitHub releases page for downloads, and the Makefile shows the two binaries the project builds: c2w and c2w-net. If you build from source you need Go, since go.mod declares go 1.25.0 and the module path github.com/container2wasm/container2wasm. The Makefile target c2w runs CGO_ENABLED=0 go build -o $(PREFIX)/c2w ... ./cmd/c2w, and make install copies c2w and c2w-net into $(CMD_DESTDIR)/bin, which defaults to /usr/local. Once c2w is on your PATH, converting an image is one command. The README gives exactly this example:

```bash
c2w ubuntu:22.04 out.wasm
```

That produces out.wasm, a WASI image. The README notes that for a non-amd64 image you select the architecture with --target-arch, for example c2w --target-arch=riscv64 riscv64/ubuntu:22.04 out.wasm. Running the result requires a WASI runtime; the README names wasmtime, wamr, wasmer, wasmedge and wazero as supported hosts. The first real use is to boot it and look at the guest:

```bash
wasmtime out.wasm uname -a
```

You should see a Linux kernel banner and x86_64 machine fields, which confirm the guest booted rather than the host. Host directories can be exposed to the guest with the runtime's mapdir flag, as in the README example wasmtime --mapdir /mnt/share::/tmp/share out.wasm cat /mnt/share/from-host. If you want the browser path instead, c2w --to-js alpine:3.20 /tmp/out-js/htdocs/ generates a WASM image plus a JS file, and the examples/emscripten directory carries the page that loads it.

## Networking is opt-in, and the browser case is the awkward one

A converted image has no network by default. The README is explicit that localhost:8080 without a query parameter means the container runs without networking, and that enabling it requires either the browser-side stack c2w-net-proxy or the host-side c2w-net, both built on gvisor-tap-vsock. The browser variant forwards HTTP and HTTPS through the Fetch API, which means the set of reachable sites is restricted by the browser's configuration, CORS in particular. The README also states that the delegate mode, where the browser tunnels packets over a WebSocket to a c2w-net process on the host, was tested only on Linux, and that the browser networking demo was tested only on Chrome. Those are three separate limits on the same feature, and they are the kind of thing that turns a demo into a support burden. If your container's job is to talk to a database or an internal API, the browser path is the wrong tool; the WASI path with c2w-net on the host is closer, but it is still a user-space stack rather than the host's real network namespace.

## What it costs: emulation, image size and the architecture you pick

The README recommends x86_64, riscv64 or AArch64 containers and warns that other platforms should work but slow because of additional emulation. Read that as a cost model. An x86_64 image on an x86_64 host still runs through Bochs, so you are already paying for instruction emulation; a container built for another architecture pays twice. The same logic applies to the emscripten path, which the README says uses QEMU Wasm with JIT compilation enabled. Startup is the other cost. The repository's OPTIMIZATION_MODE argument and its wizer value exist because booting a kernel and a rootfs from scratch is slow enough to matter, and the Makefile's benchmark target, ./tests/bench.sh, is the project's own way of measuring it. There is a practical ceiling here too: with VM_MEMORY_SIZE_MB defaulting to 128 and VM_CORE_NUMS to 1, memory-hungry or multi-threaded workloads will hit the guest's limits long before they hit the host's. None of this makes the tool bad. It makes it a tool for small, self-contained Linux userspace, not for production services.

## Where container2wasm is the wrong choice, and what to use instead

If your goal is to run containers on a server, container2wasm is the wrong layer. Docker, containerd and Podman already run the image natively, with the host kernel, real networking and no emulator between the process and the CPU. The repository itself depends on containerd libraries, which is a reminder that it is consuming the container ecosystem, not replacing it. The closer comparison is a WASI runtime used directly: tools such as wasmtime or wazero run a .wasm module compiled from your source, with no guest kernel, no rootfs image and no emulation. That path is faster and smaller, and it is the right answer when you can recompile the workload. container2wasm exists for the case where you cannot: the artifact is a container image, and you need it to run in a WASI host or a browser. Choosing between them is a question about your artifact, not about which runtime is better.

## Licence, maintenance and the upgrade path

The project is Apache-2.0, and the README links a FOSSA licence badge. Apache-2.0 is permissive and includes an explicit patent grant, but the repository also embeds third-party emulators and build inputs, including Bochs, TinyEMU and QEMU forks pinned by commit in the Dockerfile, plus a WASI SDK and Emscripten toolchain. If you redistribute a generated .wasm, the licences that matter are the ones on those components and on the container image you converted, not only the one on container2wasm itself. That is a question for your own legal review, not something the README settles. On maintenance, the last push to the default branch was on 2026-09-14, and the most recent release listed is v0.8.4 from 2026-03-16. The repository is not archived. Upgrades are not a drop-in affair: the Dockerfile pins SOURCE_REPO_VERSION=v0.8.4, EMSDK_VERSION=3.1.40 with a TODO to support a recent version, and specific commits for the emulator forks, so moving between releases can mean rebuilding the toolchain rather than swapping a binary. The README does not document rollback or compatibility guarantees between generated images and runtime versions, so pin both sides in your own build.

## Conclusion

Adopt container2wasm when you want an existing container image to execute inside a WASI runtime or a browser tab and you can accept emulation overhead and a 128 MB default VM memory. Do not adopt it as a general replacement for Docker or containerd on a server: it is described as experimental, networking is off unless you wire up c2w-net or c2w-net-proxy, and non-x86_64/riscv64 images pay for a second layer of emulation. Before committing, convert one of your own images with c2w, run it under wasmtime, and check the --target-arch, --to-js and networking paths your workload actually needs.

## FAQ

### Is container2wasm the same as running Docker in the browser?

It converts a container image into a WASM module that boots a Linux kernel inside an emulator, and the README provides a browser demo that serves that module over HTTP. There is no Docker daemon in the browser; the module runs on a WASI runtime shim such as browser_wasi_shim.

### Does a container converted by container2wasm have network access?

Not by default. The README states that localhost:8080 without a query parameter runs the container without networking, and that networking requires either the browser-side c2w-net-proxy or the host-side c2w-net.

### Which container architectures does container2wasm recommend?

The README recommends x86_64, riscv64 or AArch64 containers, and notes that other platforms should work but slow because of additional emulation. The --target-arch flag selects the architecture, for example c2w --target-arch=riscv64 riscv64/ubuntu:22.04 out.wasm.

### What are the downsides of using wasm?

For this project the practical downside is emulation: the converted image runs a guest Linux kernel through Bochs, TinyEMU or QEMU rather than natively, and the build defaults set the guest to one core and 128 MB of memory. Networking is also opt-in and, in the browser case, restricted by CORS.

## Sources

- [container2wasm/container2wasm on GitHub](https://github.com/container2wasm/container2wasm)
- [License: Apache-2.0](https://github.com/container2wasm/container2wasm/blob/main/LICENSE)
- [Project website](https://ktock.github.io/container2wasm-demo/)
- [README](https://github.com/container2wasm/container2wasm/blob/main/README.md)
- [Releases](https://github.com/container2wasm/container2wasm/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/container2wasm-container2wasm
