Run a real Kubernetes control plane entirely inside a browser tab — with no server behind it.
This is not a web terminal connected to a remote Kubernetes cluster. There is no backend API, no remote node, and nothing your
kubectlcommands talk to over the network. The only thing served is static files (GitHub Pages on the live demo). The whole cluster runs client-side in an emulated x86 VM in your tab — reload the page and it's gone.
▶ Try it live: https://imaustink.github.io/browser-k8s/
browser-k8s is a training tool: a self-contained, zero-setup sandbox for
learning Kubernetes and practicing kubectl. A user types kubectl into an
in-page terminal, and those commands run inside an x86 Linux VM emulated by
v86, talking to a real kube-apiserver that
runs in the tab — not a remote server — backed by SQLite (via
kine) instead of etcd. A real kubelet +
containerd run inside the VM, so pods actually execute containers on a genuine
node. Nothing is provisioned server-side and no request ever leaves the browser,
so learners get a throwaway cluster they can break and reset freely.
There is no server-side infrastructure of any kind. Hosting the app is just static file serving — the live demo is plain GitHub Pages, and locally it's a tiny static server that only adds the COOP/COEP headers v86 needs. The whole cluster — control plane and a real container-running node — boots unattended inside a single browser tab. See plan.md for the original design and docs/how-it-works.md for the architecture.
| Path | What it is |
|---|---|
index.html, src/ |
The web frontend: v86 emulator + xterm.js terminal |
server/ |
Zero-dep static server that sends the required COOP/COEP headers |
scripts/ |
setup-assets.mjs copies v86 runtime files into public/v86/ |
build/ |
Pipeline that builds the Alpine + Kubernetes guest image |
public/images/ |
Where guest disk images / snapshots are served from |
npm install # also copies v86 assets into public/v86/
cp .env.example .env.local # then point VITE_IMAGE_URL at a guest image
npm run dev # http://localhost:5173npm run dev and npm start both serve with the cross-origin isolation
headers v86 needs for SharedArrayBuffer:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
You need a guest image in public/images/. To verify the emulator + terminal
plumbing, drop in any small v86-compatible Linux ISO as linux.iso (see
public/images/README.md). For the actual
Kubernetes node image, run the build pipeline.
npm run build # typecheck + bundle into dist/
npm start # serves dist/ on :8080 with COOP/COEP headersThe full pipeline (binaries → Alpine rootfs → bootable image) lives in build/:
cd build && ./build-image.shThen capture a boot snapshot via the toolbar's Save snapshot button so the page loads in seconds instead of cold-booting. Details and the two boot modes (9p filesystem vs. raw disk) are documented in build/README.md.
- Save snapshot — capture the running VM state as
preboot.bin(used for fast boot; serve it and setVITE_INITIAL_STATE=/preboot.bin). - Restore — load a previously saved snapshot.
- Reset — reboot the VM.
Once the node is Ready, run web-demo in the VM's terminal for a one-command
smoke test. It applies a baked-in nginx Deployment (the web-deployment.yaml
manifest at /etc/kubernetes/demo/), waits for the Pod, and prints where it
landed:
web-demoEquivalent to applying docker.io/browser-k8s/nginx:386 (imagePullPolicy: Never,
so it runs fully offline), then kubectl get pods -l app=web -o wide. Hit the
Pod with wget -qO- http://<pod-ip>/.
Works: kubectl apply/get/describe/delete/explain, namespaces, RBAC,
ConfigMaps/Secrets/Services (stored), Deployments/ReplicaSets/Jobs (real Pods
run on the node), scale, rollout, and kubectl logs/exec against the real
kubelet.
Doesn't: kubectl port-forward. Resource limits are stored and visible; CPU CFS
quota is disabled (the emulated kernel lacks CONFIG_CFS_BANDWIDTH).
The main constraint is memory. The whole cluster runs in the RAM v86 allocates to the emulated machine, which lives inside a single browser tab. The control plane, kubelet, containerd, and every Pod all share that budget, so only a handful of small workloads fit before the VM runs out of memory. This makes browser-k8s great for learning and experimentation, but not for running large or memory-hungry workloads.
It also only supports 32-bit 386 container images. The emulated CPU is i686,
so every binary and workload must be built for GOARCH=386/i386; standard amd64
images won't run on the node.
There is also no container registry. Nothing in the tab can reach the
network, so the node cannot pull images. Every image is built ahead of time,
packaged into the VM, and imported into containerd at boot. Pods must therefore
set imagePullPolicy: Never and reference an image that was baked in — anything
else fails with ErrImageNeverPull.
Images are built as linux/386 docker-archive tarballs, copied into the guest
image, and imported into containerd on boot. To add a new one:
-
Build and save the tarball. Add a step to build/v86-image/build-images.sh that builds (or pulls) your image for
linux/386and runsdocker save <ref> -o images/<name>-386.tar. Keep it small — it competes for the VM's limited RAM and disk. -
Copy it into the rootfs. Add a
COPY images/<name>-386.tar /var/lib/browser-k8s-images/<name>-386.tarline to build/v86-image/Dockerfile, and include<name>-386.tarin the tarball loop in build/v86-image/build-v86-image.sh. The boot script imports every*.tarunder/var/lib/browser-k8s-images/automatically. -
Normalise the ref (if needed). If containerd keeps a short image name, add a
ctr -n k8s.io images tag <short> docker.io/<ref> 2>/dev/null || trueline next to the others in build/guest-init.sh so Pods can reference the fully-qualifieddocker.io/...name. -
Rebuild the guest image:
cd build/v86-image && ./build-v86-image.sh, then reload the browser tab. -
Use it in a Pod with
imagePullPolicy: Never:image: docker.io/<ref> imagePullPolicy: Never
