Playground
https://go-wasmcontainers.github.io/playground/ is a dashboard of WASI
containers, images and volumes with a terminal running wctr, in a web page.
There is no server: the engine runs in the page (the browser executor,
runtime/browser, new in engine v0.2.0). This is playground v0.7.0, on engine
v0.30.1.
What is new in v0.7.0
- Engine v0.30.1: piped standard input, output and error are no longer reported to a guest as a terminal, and a guest’s exit codes 0 to 255 are kept (see Commands).
[checked]or[mirrored]next to each image inwctr imagesand in completion, read fromcatalog.json:[checked]when the build ran the image under the engine and under wasmtime and nothing differed. The live catalogue has 52 images, 43 of them pkgx bottles, 41 of those checked against wasmtime 50.0.1.- A stale
yqexpectation is removed (it now matches wasmtime byte for byte);gnosticstays a named difference.
What was new in v0.6.0
- A file server in the tab: the
fileshareimage, then explore :8080 in the ports bar (see A file server in the tab). - The
-proute relays WebDAV’s methods (PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK; RFC 4918) as well as HTTP’s. - GPU surface input: click a surface’s pane under Surfaces to give it the
focus, and its pointer presses and keys reach the guest (gpu v0.6.0, dashboard
v0.8.0).
wctr run --rm --gpu triangle [frames [input]]withinputprints what the guest receives, such aspointer-down 80,80andkey-down key-a "A" shift=true; the end-to-end testTestGPUSurfaceInputchecks the coordinates and Shift+A.
What was new in v0.5.0
-pin the browser:wctr run -d -p 8080:80 net serve, then open :8080 in the bar above the terminal. The container’s pages open in a tab of their own, on the usercontent origin (see Published ports).- Every pkgx image comes from the signed catalogue, and Pages rebuilds daily, so new pkgx bottles appear without a release.
What was new in v0.4.1
- Engine v0.30.0:
run -i/-t,attachandstart -aiin the page. A line whose ownwctrflags say-i(run,start,exec; also-dit) gets what you type; the end-to-end test runswctr run --rm -it shellandexec -it c shand checks the exit status. - The usercontent shell forgets requests the playground never answers
(
shell.jsandsw.js): a pending-request leak fixed.
What was new in v0.4.0
- GPU surfaces and compute: the
computeandtriangleimages (copies of gpu v0.5.1’s examples) run with--gpu; a surface is a pane under the dashboard’s Surfaces view. A container without--gpu, or a browser without WebGPU, gets no adapter: these images printno GPU adapterand exit 2. - WASIX: programs started by a shell run as WASIX processes in the page.
- The
shellimage,ghcr.io/go-wasmcontainers/shell:0.5.0, mirrored verbatim and pinned by its digest (sha256:d122bdb7…df433ed):wctr run --rm shell sh -c 'echo a | cat'andwctr exec -it CONTAINER shwork in the page, andshcompletes with Tab (ecTab givesecho). - Typing into
exec -i: a line whose ownwctrflags say-igets what you type, Ctrl-C and Ctrl-D included. wctr buildandwctr composeare wired into the page’swctr, as incmd/wctr.- The shell of a second origin for containers’ pages
(https://go-wasmcontainers-usercontent.github.io/) and the route policy: the
first half of
-pin the browser. The second half came in v0.5.0.
What was new in v0.3.0
- The dashboard is the main UI: the dashboard
(Containers, Images, Volumes, Networks, Terminal) runs on the persistent engine
worker. The terminal alone, on xterm.js, stays at
?ui=terminal. - Networking in the browser: containers talk over TCP and UDP, by name (see
Networking). The
netimage hasserve,get,bench,udp-echo,udp,lookupanddial. - Join class: with a teacher’s relay, the page joins
the class network (the dashboard’s Networks view, Join class…). The relay
must run with
--cors-origin https://go-wasmcontainers.github.io(relay v0.3.0 or later). The join code is typed in a password field, posted once to the engine worker, and never stored. - wasi-nn in the browser: the
mnistimage reads a handwritten digit (see GPU). - It still works offline and keeps its images and containers across a reload.
Published ports (-p)
$ wctr run -d -p 8080:80 net serve
The bar above the terminal then shows open :8080, which opens the container’s web pages in a tab of their own. They are served on another site, https://go-wasmcontainers-usercontent.github.io/, never on the playground’s origin: a container’s page there could otherwise read the playground’s storage and script it.
- The engine publishes the port in memory, and gives each opened port a
session: a random id that names its pages (
/s/<session>/8080/), bound to that one port. - The view tab relays each request of the container’s page to the playground tab, which runs the engine, and the answer back. Nothing leaves the browser.
- Cookies work, but not as you might expect. The engine keeps one cookie jar
per session. The container’s
Set-Cookienever reaches the browser, anddocument.cookiein its page does not see them. - Closing the view tab ends the session. The playground tab must stay open, as it runs the engine.
Measured in Chromium: 8 MiB from a container to its page, through every hop, in 32 ms (about 265 MB/s).
A file server in the tab: the fileshare image
$ wctr run -d -p 8080:80 --name files fileshare
The image is go-fileshare/fileshare
v0.32.0, built for wasip1 and serving WebDAV only (6.5 MB gzip):
- one share,
files, with a README in it; - one person: user
alice, passwordplayground. The password is a demo, written into the image on purpose, since nothing the share holds belongs to anybody; - plaintext WebDAV, because TLS ends at the browser’s origin and the hop to the container is the engine’s, in memory.
open :8080 shows the server’s page. The route relays WebDAV’s methods
(PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK; RFC 4918) as
well as HTTP’s, so a WebDAV client in the page works. WebDAV through
open :8080 works on the live playground: the usercontent origin
(https://go-wasmcontainers-usercontent.github.io/) is rendered from playground
v0.6.0 and relays these methods. The end-to-end test TestFileshare checks, from inside the container’s page:
- without credentials: 401 with a
Basicchallenge; - with a wrong password: 401;
- with alice’s: a PROPFIND of the share (207) lists
README.txt, which reads; - a PUT lands (201) and reads back.
explore :8080 opens the go-fileshare explorer
(v0.4.0, built into the site as explorer.wasm, fetched only when used) as a
browser client of the server:
- It asks for a user and a password first. They are sent as HTTP Basic with each request, and they stay in the page’s memory. They are dropped when the explorer closes; nothing is stored.
- The explorer runs in a frame of the playground, in its bridge mode. Its requests go straight to the engine, on a relay session of its own, with no view tab and no usercontent origin.
- The explorer is the site’s own code, not the container’s, and it draws on a canvas. Nothing the container answers becomes HTML in the playground.
explorer.htmlis the only page that may be framed, and only by the playground (frame-ancestors 'self'; everything else keeps'none').- The session’s credentials replace any
Authorizationheader the frame sends. - A download is read whole, up to the route’s 256 MiB bound, and saved under a sanitised name.
TestFileshareExplorer checks that a wrong password gives 401 and the explorer
says the server refused the login, that alice’s lists the share, that a PUT
through the bridge (201) reads back, that a frame-supplied Authorization is
replaced, and that after close the bridge refuses.
Completion and editing
The terminal completes with Tab, as a shell does: subcommands, flags, -m
sizes, local images (for run and rmi), the catalogue of images to pull,
container names and IDs (logs, rm, inspect, and only running containers
for kill) and image arguments. Editing is bash-style: Up and Down for the
history, Ctrl-R to search it, Ctrl-A, Ctrl-E, Ctrl-B and Ctrl-F to move,
Ctrl-U, Ctrl-K and Ctrl-W to cut, Ctrl-D, and Ctrl-L to clear. The history is
kept per tab (in sessionStorage). From a keypress to the line on screen took
7 to 14 ms in Chromium and 23 ms in Firefox, measured in CI.
The line editor and its completion are the package cli/lineedit of the
engine: pure Go, usable by anyone who embeds the engine’s CLI. Since engine
v0.31.0, cli/lineedit is a thin adapter over lineedit, a module of its own
(github.com/go-wasmcontainers/engine/lineedit, tag lineedit/v0.1.0, standard
library only) that the guest shell uses too. The playground (v0.7.0) is built on
engine v0.30.1.
What you can type
wctr pull, run, ps, logs, rm, rmi, images, inspect and kill,
start, stop, restart, diff, stats, events, volume, network and
system df|prune, plus help and clear. run takes --rm, -d, --name,
-e, -w, -m, --entrypoint, -v NAME:/path[:ro], --network NAME and
-p HOST:CONTAINER (see Published ports). Exit
codes are the native engine’s: the container’s own, or 125, 126, 127, 134 and 137;
see Commands.
$ wctr run --rm hello
$ wctr run --rm -e NAME=you greet
Images
| Image | What it is |
|---|---|
hello, greet | the small demonstration modules |
lab | a module with subcommands: echo, env, pwd, ls, cat, write, count, sleep, spin, hog, bench, fs, exit, esc |
net | a networking probe: serve, get, bench, udp-echo, udp, lookup, dial |
fileshare | a WebDAV file server (go-fileshare v0.32.0, 6.5 MB gzip): share files, user alice, password playground (a demo) |
mnist | classifies a handwritten digit with wasi-nn |
every pkgx wasip1/wasm bottle: 43 images from 41 bottles today (yq, gojq, jj, shfmt, actionlint, age, lua, luac, sqlite3, zstd, simdjson-minify and more; wctr images lists them all) | derived from pkgx’s signed catalogue at build time, see below |
pkgx images are derived, not listed
The site build (cmd/site build --pkgx) reads pkgx’s signed wasip1/wasm
catalogue (go-pkgx.dev/catalog; the signature is verified) and mirrors the
newest version of every project that has one published. No list is typed in the
playground: a bottle published tomorrow is an image after the next build, and
wctr images and the dashboard’s Images view show whatever catalog.json
holds, each entry with its project, version and layer size.
- Version: the highest by numeric components, then rebuild counter, then raw string; the catalogue’s order is not trusted.
- Names: one image per
bin/<name>.wasmin the bottle (luagivesluaandluac), at most 8 per bottle; all of a bottle’s images share its layer. The catalogue’sprovidesis only a hint (on wasip1/wasm it is copied from the host recipe and wrong both ways): a name without a module and a module it does not list are reported as findings. The image’s entrypoint is the bare binary name, so argv[0] islua. A name taken by two projects goes to the one whose last path segment equals it, else the smallest project name; the other is published as<project-with-dashes>-<name>. - Dependencies are ignored: a
wasip1module is statically linked. - Limits: 400 projects, 900 images, 8 images per bottle, 100 MiB per
compressed layer, 512 MiB and 20 000 entries unpacked, 3 minutes for the
catalogue and 30 in all. An unverifiable or empty catalogue stops the build;
there is no built-in list. A bottle over a limit, or whose tar has an absolute
path, a
..or a link leaving the root, is refused and listed infindings.json. - Checked: each
catalog.jsonentry haschecked:truewhen the build ran the image under the engine and under wasmtime (probes with real input foryq,gojq,jj,tomljsonand others) and nothing failed or differed (41 of 43 pkgx images on 2026-11-01, against wasmtime 50.0.1),falsewhen it is only mirrored. - Site size: GitHub Pages refuses a site over 1 GB. With 43 images the mirror is 112 MB (about 2.6 MB per image, each image stored separately) and the whole site 167 MB. The build stops if the layers exceed 700 MiB; the plan beyond that is to drop the largest, then to shard across a second Pages site.
- Page cost: one
catalog.jsonof about 330 bytes per image, read once; an image’s blobs are fetched only when a command uses it. - Smoke test: CI runs every derived image with
--versionand--help(or a named probe such aslua -v) under the engine and under wasmtime and compares exit status, stdout and stderr byte for byte; a failure or difference that is not named, with its reason, fails the job.
Lua’s errors, which use WebAssembly exception handling, work in Chromium,
Firefox and natively. The first lua run took 69–108 ms in Chromium and 170 ms
in Firefox, measured in CI.
Images and pulls survive a reload: they are kept in the browser’s IndexedDB.
So do the containers (engine v0.4.0): state, exit code, logs and written files.
A container that was running comes back exited with code 137, and a --rm one
is removed.
Offline and updates
The playground works offline (dashboard.wasm is in the service-worker cache). A
new version installs in the background and applies when you choose “Reload to
update”. Only one tab runs the engine at a time; another tab can take over with
“Use it here”. The engine was ready in 130–190 ms on a first visit in Chromium,
80–115 ms after a reload, and 75 ms offline.
Limits per container
256 MiB of memory, a 5-minute run, 64 MiB of writes, 4 MiB of logs, and 64 containers at once.
Not yet
One wctr command per line (pipes need the shell image’s sh); vertex buffers and
depth-stencil are not served to GPU guests; published ports are HTTP and WebDAV only, shown
in a tab, with no UDP and no raw TCP from outside the page. Networks stay inside the page: there is no route to the Internet. Ctrl-C
stops waiting for a container but leaves it running. See
Limitations and the roadmap.
Speed
Measured in one CI run, the page is ready in 0.46 s in Chromium and 1.0 s in
Firefox, and the first hello runs in 81 ms and 291 ms. Cold start is slower
than wasmtime (23 ms natively). The table is on
Performance.
Created on by tannevaled
Modified on by tannevaled