# leaningtech/webvm: a Debian Linux VM that runs in the browser tab

> WebVM is a client-side Linux virtual machine built on the CheerpX engine and shipped as a Svelte app. It is easy to try and easy to fork, but the disk image and the networking model decide what it can actually do.

**leaningtech/webvm** — Virtual Machine for the Web

- Repository: https://github.com/leaningtech/webvm
- Website: https://webvm.io
- Stars: 17,407 · Forks: 3,325
- Language: JavaScript
- License: Apache-2.0
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/leaningtech-webvm

## The problem WebVM targets: a Linux environment without a server

Most ways of giving someone a Linux shell involve a machine somewhere. A container on a host, an SSH box, a cloud instance with a terminal in the browser. WebVM removes that machine. The README describes it as a "server-less virtual environment running fully client-side in HTML5/WebAssembly", and the target is an unmodified Debian distribution with native development toolchains inside it.

The audience follows from that. Developers who want to demonstrate something Linux-shaped without asking the visitor to install or sign in. People on locked-down hardware where the browser is the only execution surface available. Educators who want a shell that appears from a URL. The repository even ships example directories for C, Lua, Node.js, Python 3 and Ruby, which is a fair summary of the intended use: small, self-contained language sessions rather than long-lived production workloads.

The Alpine variant at webvm.io/alpine.html goes further and adds Xorg and i3, so the same idea covers a graphical desktop. That is the more ambitious claim, and the one that depends most heavily on how the disk image was built.

## CheerpX: JIT, virtual block file system, syscall emulation

WebVM is not itself the virtualisation layer. The README states the project is powered by CheerpX, and lists four parts of that engine: an x86-to-WebAssembly JIT compiler, a virtual block-based file system, a Linux syscall emulator, and sandboxed client-side execution. Everything interesting about WebVM's behaviour comes from that split.

The JIT is why an x86 Debian userland can run at all on a machine whose native instruction set is not x86. Guest instructions are translated into WebAssembly, and the syscall emulator answers the kernel calls that translated code makes. The file system is block-based and virtual, which means the guest sees a disk rather than a host directory. That is the detail that matters most for anyone deploying this: the operating system you get is defined by an .ext2 image, not by the JavaScript in the repository.

So the architecture has two halves that version independently. The front end is a SvelteKit and Vite application (package.json lists @sveltejs/kit, vite, xterm packages and @leaningtech/cheerpx), and the guest is a disk image. Updating the shell does nothing to the guest. Rebuilding the image changes everything the user sees inside the terminal.

## Running WebVM locally with nginx and a custom .ext2 image

The README points at webvm.io for immediate use with no setup, and then gives a local path for anyone who wants their own image. Clone the repository and change into it:

```bash
git clone https://github.com/leaningtech/webvm.git
cd webvm
```

The repository contains a persistent custom-disk-images/ directory for local .ext2 files. The README says to download the official Debian mini image from Releases, named debian_mini_20230519_5022088024.ext2, or to copy in an image you built yourself. Then point the app at it by editing config_public_terminal.js so that the exported values match your file:

```js
export const diskImageUrl =
  "/custom-disk-images/debian_mini_20230519_5022088024.ext2";
export const diskImageType = "bytes";
```

Install the dependencies and build, then serve the result with the nginx configuration that ships in the repository:

```bash
npm install
npm run build
nginx -p . -c nginx.conf
```

The README says to open http://127.0.0.1:8081 afterwards. If the terminal appears and the shell starts, the image path and type are correct; a blank terminal usually means the URL in diskImageUrl does not resolve from the server root. For the full Alpine desktop, the README redirects to leaningtech/alpine-image rather than describing it here.

## Deploying a fork to GitHub Pages, and the image size ceiling

The GitHub Pages route is the one the README calls beginner-friendly: fork the repository, set Settings, then Pages to use "GitHub Actions" as the source, and run the Deploy workflow from the Actions tab. When it finishes, the URL is shown under the deploy_to_github_pages job.

The same workflow also builds .ext2 images from a Dockerfile. You can point it at dockerfiles/debian_mini or another Dockerfile and either publish the result as a release asset or deploy the Pages build from your fork. This is the part worth understanding before you fork: the workflow is both a site deployer and an image builder, and the image is what your users actually run.

There is a hard ceiling. The README states plainly that dockerfiles/debian_large is too large an image for GitHub Pages. That is not a tuning problem, it is a hosting limit, and it is the single most likely reason a fork fails after the workflow reports success. A related tip covers REPL-style images: the workflow takes the CMD from the Dockerfile into account, so changing CMD [ "/bin/bash" ] to CMD [ "/usr/bin/python3" ] produces a Python 3 REPL instead of a shell. That is a one-line change with a large effect on what the deployed page is.

## Networking through Tailscale, and what ICMP rules out

WebVM has no ordinary network stack exposed to the user. Networking arrives through Tailscale, opened from the sidebar's Networking panel and connected with an interactive login, after which the browser VM can reach machines in your tailnet. To reach the public internet, the README says to advertise an exit node on another device in the same network, and WebVM picks it up automatically once advertised. Connection state is shown as a coloured dot: orange for local network, green for global, with the Tailscale IP in the button text once connected.

For unattended or scripted access, an auth key can go in the URL fragment instead of an interactive login:

```
https://webvm.io/#authKey=<your-ephemeral-key>
```

The README calls this equivalent to Tailscale's --login-server option, and notes that a custom control server can be added in the same fragment with controlUrl, separated by &. That is the hook for headscale, the self-hosted control server. Headscale does not add CORS headers by default, so the README requires a proxy in front of it and gives an nginx snippet adding Access-Control-Allow-Origin and Access-Control-Allow-Credentials for a specific origin inside the location / block. The resulting URL uses #controlUrl=<your-headscale-url>.

The limitation is explicit and worth repeating: low-level networking operations, especially the ICMP used by ping, are not available. The README tells you to check connectivity with curl or wget instead. Anyone whose first instinct is to run ping inside the VM will conclude the network is broken when it is not.

## Where WebVM is the wrong tool

Two constraints stand out. First, sudo is not installed. The README notes that users have root privileges by default and that sudo can be added to the Dockerfile if needed, which is a reasonable trade but means any tutorial, script or Dockerfile that assumes sudo will fail until you rebuild the image. Second, the guest is only as capable as the .ext2 you serve, and the convenient Debian image is deliberately mini. If your workload needs a package that is not in it, the answer is a custom image build, not a shell command inside the VM.

Beyond that, the execution model rules out whole categories of work. A browser tab is a bad place for anything long-running or stateful: close it and the session is gone, and the README documents no persistence or rollback mechanism for the virtual file system. WebVM is also not a security boundary you should reason about like a container host. CheerpX is described as sandboxed client-side execution, which is a statement about isolation from the page, not a claim that the guest is a hardened multi-tenant environment. And because the image is served to the browser, anything inside it is public to whoever loads the page. Do not put secrets in a disk image you deploy to GitHub Pages.

## How WebVM differs from Docker and from a hosted VM

The natural comparison is Docker. Both give you a Linux userland from a declarative build file, and WebVM's workflow even starts from a Dockerfile. The difference is where the kernel lives. Docker containers share the host kernel, so a container is a process tree on a machine you control, with the full syscall surface available and real networking. WebVM has no host kernel to share: CheerpX emulates the syscalls in JavaScript and WebAssembly, inside the browser, with no server involved. That is why it can run on a laptop with nothing installed, and also why ping is missing and why the disk image size is bounded by a static hosting limit.

Against a hosted VM or a cloud shell, the trade is the same shape but sharper. A cloud shell gives you a real kernel, persistence and a network, at the cost of an account, a server and a bill. WebVM gives you a URL. For a demo, a teaching page or a throwaway toolchain, the URL is the better artifact. For anything that must survive the tab closing, it is not.

Within the project's own family, the Alpine and Xorg and i3 build is the more interesting comparison: same engine, same image-driven model, but a graphical desktop instead of a terminal. If your goal is a shell, the Debian mini path is lighter. If your goal is to show a desktop, the README sends you to leaningtech/alpine-image rather than duplicating the instructions.

## Licence and the cost of staying current

The repository is Apache-2.0, with the licence text in LICENSE.txt at the top level. That covers the WebVM source in this repository. It does not automatically describe the CheerpX engine, which is a separate dependency (@leaningtech/cheerpx, pinned as "latest" in package.json) and which the README treats as a distinct product with its own documentation site. If you fork and deploy, check the terms that apply to the engine and to any disk image you redistribute, since the Debian image is distributed as a release asset rather than as source in this repository. This is a description of what the files say, not legal advice.

The maintenance picture is straightforward. The repository is not archived, and the last push was on 2026-09-17. The only release listed is the ext2_image release from 2023-05-16, which is the Debian mini image itself. Practically, that means the disk image you download is old, and the way to get a newer guest is to build one from dockerfiles/ through the Deploy workflow rather than to wait for a release. Upgrading the front end is a dependency bump; upgrading the guest is an image rebuild, and those two clocks do not move together.

## Conclusion

Adopt WebVM when you need a Linux shell or a small toolchain inside a browser tab, on a machine where you cannot install anything, and when a Debian mini image is enough. Do not adopt it as a general-purpose VM host: it has no sudo package, ICMP is unavailable, and the large Debian image is documented as too big for GitHub Pages. Before committing, verify three things: that the .ext2 image you intend to serve fits your hosting limit, that the Tailscale path you need (interactive login, auth key, or a self-hosted headscale control server behind a CORS proxy) actually connects from your network, and that the build you ship is the one produced by the Deploy workflow, because the disk image is the part that changes behaviour, not the HTML shell around it.

## FAQ

### What is WebVM used for?

It provides a Linux virtual machine that runs entirely client-side in the browser, aimed at running an unmodified Debian distribution with native development toolchains. The repository also ships example directories for C, Lua, Node.js, Python 3 and Ruby.

### How much does WebVM cost?

The repository is Apache-2.0 and the README says you can get started at webvm.io with no setup required, so nothing in the documentation describes a price for WebVM itself. The one external account the README does mention is Tailscale, for which it suggests creating a free account if you need networking.

### What is the difference between WebVM and Docker?

Docker containers share the host kernel, while WebVM runs through the CheerpX engine, which the README describes as an x86-to-WebAssembly JIT compiler, a virtual block-based file system and a Linux syscall emulator with sandboxed client-side execution. WebVM's own workflow still starts from a Dockerfile to build the .ext2 disk image, so the build input is similar even though the runtime is not.

### Is WebVM faster than a virtual machine?

The README makes no performance claim either way, and it does not compare WebVM's speed to a conventional virtual machine. What it does document is that some low-level networking operations, especially the ICMP used by ping, are not currently available, and that on slower connections there may be a short delay before Tailscale initialisation.

## Sources

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

---

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