# Kubernetes in your browser
> A real Kubernetes control plane, compiled to WebAssembly: kube-apiserver, kube-controller-manager, kube-scheduler and kubectl run in your browser. Join a real node with Docker and WebRTC.

Canonical URL: https://jkroepke.de/kubernetes/
Last modified: 2026-11-06


This is a real Kubernetes v1.37 control plane. Not a simulator. kube-apiserver, kube-controller-manager, kube-scheduler and kubectl are the upstream Go code, compiled to WebAssembly into one binary. The only replaced part is etcd: a small in-memory implementation of the etcd v3 API. Everything runs in your browser tab. Reload the page and the cluster is gone.

A control plane without nodes is boring. Type `join` and start the printed `docker run` command. The container runs a kubelet and containerd and connects to your browser with WebRTC. Then `kubectl get pods -A` shows flannel, kube-proxy and CoreDNS on a real node.

<link rel="stylesheet" href="/kwasm/xterm.css">
<style>
  .kwasm-wrap { position: relative; }
  .kwasm-terminal { height: 70vh; min-height: 420px; padding: 8px; border-radius: 6px; background: #0d1117; overflow: hidden; box-sizing: border-box; }
  .kwasm-start { display: block; margin: 1rem auto; padding: .6rem 1.2rem; font-size: 1rem; cursor: pointer; }
  .kwasm-fullscreen { position: absolute; top: 8px; right: 16px; z-index: 5; padding: .2rem .5rem; font-size: .8rem; color: #c9d1d9; background: #21262d; border: 1px solid #30363d; border-radius: 4px; cursor: pointer; opacity: .7; }
  .kwasm-fullscreen:hover { opacity: 1; }
  .kwasm-wrap:fullscreen, .kwasm-wrap.kwasm-fs { position: fixed; inset: 0; z-index: 9999; background: #0d1117; }
  .kwasm-wrap:fullscreen .kwasm-terminal, .kwasm-wrap.kwasm-fs .kwasm-terminal { height: 100%; min-height: 0; border-radius: 0; }
</style>
<div class="kwasm-wrap" id="kwasm-wrap" hidden>
  <div class="kwasm-terminal" id="kwasm" data-wasm="/kwasm/kwasm.wasm.zst"></div>
  <button class="kwasm-fullscreen" id="kwasm-fullscreen" type="button" title="Full screen (Esc to leave)">Full screen</button>
</div>
<button class="kwasm-start" id="kwasm-start" type="button">Boot the cluster (downloads ~28 MB)</button>
<script>
  (() => {
    const wrap = document.getElementById("kwasm-wrap");
    const btn = document.getElementById("kwasm-fullscreen");
    
    const isFull = () => document.fullscreenElement === wrap || wrap.classList.contains("kwasm-fs");
    const label = () => { btn.textContent = isFull() ? "Exit full screen" : "Full screen"; };
    btn.addEventListener("click", () => {
      if (document.fullscreenElement === wrap) document.exitFullscreen();
      else if (wrap.classList.contains("kwasm-fs")) wrap.classList.remove("kwasm-fs");
      else if (wrap.requestFullscreen) {
        const overlay = () => { if (document.fullscreenElement !== wrap) { wrap.classList.add("kwasm-fs"); label(); } };
        wrap.requestFullscreen().catch(overlay);
        
        setTimeout(overlay, 500);
      }
      else wrap.classList.add("kwasm-fs");
      label();
      wrap.querySelector(".xterm-helper-textarea")?.focus();
    });
    document.addEventListener("fullscreenchange", () => {
      if (document.fullscreenElement === wrap) wrap.classList.remove("kwasm-fs");
      label();
    });
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape" && wrap.classList.contains("kwasm-fs")) { wrap.classList.remove("kwasm-fs"); label(); }
    });
    document.getElementById("kwasm-start").addEventListener("click", (e) => {
      e.target.remove();
      wrap.hidden = false;
      const s = document.createElement("script");
      s.type = "module";
      s.src = "\/kwasm\/kwasm.js";
      document.body.appendChild(s);
    });
  })();
</script>


The boot takes around 15 seconds. WebAssembly in the browser is single threaded, so the whole control plane shares one CPU core. The source is in the [blog repository](https://github.com/jkroepke/blog/tree/main/kwasm).

