While investigating the isolation boundary of Multikernel Linux containers, I kept returning to one comparison: why can a compromised virtual-machine guest kernel not simply map the host’s memory, while a compromised Multikernel child kernel can attempt to map memory outside its allocation?
My first Multikernel container networking experiment built a static /30 link for each child. The primary owned a TUN interface, routes, NAT, and firewall rules; mk-agent owned another TUN inside the child; and an authenticated packet pump transported IP frames between them.
The phrase “one kernel per container” sounds like a stronger isolation claim than “one process namespace per container.” In a Multikernel Linux runtime, each child really does have its own kernel, process table, boot ID, CPUs, memory description, and root filesystem. That still does not make it equivalent to a virtual machine.
The components in my Multikernel container runtime do not share one ordinary process hierarchy. The containerd shim and mkruntimed run in the primary Linux system, while mk-agent and the workload run under a child kernel. Calls that would normally be local runtime operations therefore become protocol messages.
In Turning Container Images into Multikernel Linux Roots, I described how an unpacked image becomes a private root for a child kernel. That gets the files into the right place, but files are not yet a running container. Something inside the child still has to interpret the OCI process configuration, establish its safety boundaries, start the executable, and translate later exec, signal, terminal, and wait requests.
My earlier walkthrough of a Multikernel container from the primary Linux system followed a successful task from containerd through a shim, mkruntimed, Kerf, and finally a child kernel. The normal path is only half of a runtime, though. The harder question is what to do when one of those steps succeeds but the caller disappears before learning the result.
My earlier posts looked at individual parts of the Multikernel container path: building image-derived roots, providing mediated networking, and implementing a containerd Runtime v2 shim. This post puts those pieces together from one particular viewpoint: what happens on the primary Linux system when containerd creates a container whose process will run under another kernel?
My first Multikernel Linux experiment on GCE started child kernels with roots that I built ahead of time. That was useful for proving that the kernels could boot, but it was not how a container runtime should consume an image. A runtime should accept the root filesystem that containerd has already prepared and should not become another registry client, layer downloader, or snapshotter.
Giving a Multikernel Linux child network access is not as simple as moving the host’s network interface into it. On Google Compute Engine (GCE), the virtual NIC is also the primary kernel’s route to SSH, metadata, and the guest agent. Assigning that device or its shared controller to a child would put management access at risk.
My first Multikernel Linux runtime used a custom command-line client to create a child and ask an agent inside it to run a process. That proved the control and process layers, but it did not make Multikernel usable by existing container clients.
My first Multikernel Linux experiment on Google Compute Engine (GCE) showed that one VM could run a primary kernel and multiple child kernels at the same time. The children could use different kernel binaries, but their root filesystems were either minimal initramfs images or DAXFS. That left an important question unanswered: could a child kernel have a persistent, read-write ext4 root filesystem?
The idea behind Multikernel Linux is unusual: one physical machine can run several independent Linux kernels at the same time, without placing a conventional hypervisor between those kernels and the hardware. A primary kernel owns the machine’s resources, moves selected CPUs, memory, and devices into a pool, and starts child kernels from that pool.
When Claude Code uses the Playwright MCP server, it opens a separate Chromium window. This window floats outside of Worklayer, disconnected from the workspace. I wanted Playwright-controlled pages to appear inside a web panel so the user can watch the AI work without Alt-Tabbing to a different window.
I initially thought UUIDv7 would be very important for avoiding expensive OFFSET pagination. UUIDv7 is time ordered, while UUIDv4 is random, so it seemed natural that changing the identifier would be the key to fast pagination.
PostgreSQL can let readers and writers work concurrently because of multi-version concurrency control, or MVCC. The trade-off is that an update usually creates a new tuple version instead of replacing the old one in place. A delete also makes a tuple obsolete without immediately removing its storage.
I wanted find-in-page search for web panels in Worklayer. Electron provides webview.findInPage() which highlights matches and handles navigation between them. Simple enough. Except after the first character is typed into the search input, all subsequent keystrokes disappear into the void.
I built a small Go CRUD service to learn where a PostgreSQL-backed application starts to slow down. The service manages companies, users, and inventory, while a separate load generator creates a repeatable mixture of reads and writes.
Agent Sandbox provides a Kubernetes API for stateful, singleton, Pod-backed workloads. GKE can add a stronger gVisor isolation boundary and Pod Snapshots that preserve process memory and root filesystem changes.
When using Claude Code inside Worklayer’s terminal panel, I wanted it to be able to interact with web pages displayed in adjacent web panels. The standard approach would be to use the Playwright MCP server, but that spawns a separate Chromium instance outside the app. The page Playwright controls and the page the user sees are two different browser sessions with no shared state.
The MCP Grafana server previously relied on static API keys or basic auth for authenticating requests to Grafana. This works fine for local development or single-user setups, but falls apart once you have multiple users who each need their own Grafana permissions. Passing around shared API keys is a security concern and means everyone operates with the same access level regardless of their actual role.