Migrating Mollymawk to mkernel, mnet, and vif
2026-10-30Mollymawk is a web application — itself running as a unikernel and designed to simplify deploying, monitoring, and managing unikernels via Albatross and reproducible builds from builds.robur.coop.
In our previous articles, we wrote about building a test suite and FreeBSD CI for mollymawk and refactoring how mollymawk structures and persists its data. At the end of that work, we mentioned the next big step: moving Mollymawk from MirageOS and Lwt to our new OCaml 5 unikernel ecosystem (uniker.nl), built around Miou, mkernel, mnet, and vif.
In this article, we cover why we migrated, what changed across the application, and some bugs we uncovered along the way.
1. The Problem
While MirageOS helped us build the initial version of mollymawk, maintaining a growing web application on top of it came with a few recurring pain points:
- Complex build and configuration workflow: Building a MirageOS unikernel relies on a multi-stage build that generates build files and wires device functors together. Doing anything slightly outside what the tool expects - like reading a specific DHCP option from a network lease meant embedding raw OCaml source code strings inside the configuration.
- Functors and
Lwteverywhere: Every part of the codebase that touched the network or disk had to be parameterized over module functors and written in monadic style withLwt. This added a lot of ceremony to both the application code and our tests. - A monolithic web layer: Our main unikernel entrypoint had grown to nearly 4,000 lines of low-level HTTP request handling, manual form and JSON parsing, and business logic all mixed together. On the frontend, we also had over 1,100 lines of custom JavaScript just to handle form submissions and UI updates.
2. Migrating to the New Stack
Our new approach to building unikernels with OCaml 5 focuses on keeping the developer in control of the build workflow and writing direct-style code using effects rather than monadic promises or pervasive functors.
Simpler Builds with Dune, unic, and mfetch
Instead of generating build files with the mirage tool, mollymawk now builds directly with dune. Because the Solo5 OCaml compiler produces the same OCaml artifacts as the host compiler, we only need to cross-compile packages that contain C stubs.
We use unic to identify which dependencies need to be cross-compiled and mfetch to vendor them. For mollymawk, our dune workspace defines two build contexts:
default: compiles and runs our native test suite on the host.solo5: compiles the bootable unikernel binary.
Static assets (CSS, JavaScript, and images) are bundled at build time using mcrunch, which now also generates ETag checksums so browsers can cache static files instead of re-downloading them on every visit.
Direct-Style Networking and Storage with mkernel and mnet
With Miou and mkernel, we replaced all of the functorized Mirage libraries with direct-style equivalents:
- Network interfaces and DHCP: Mollymawk uses two network interfaces: a
serviceinterface for serving web traffic and talking to external services, and amanagementinterface for querying internal unikernels. Usingmnetandmnet-dhcp, the management interface acquires its IPv4 address, DNS servers, and domain name directly from the DHCP lease without any code-generation workarounds. - Albatross, DNS, HTTP, and Email: We ported our Albatross TLS client, DNS resolver, HTTP client, and SMTP email sender to
mnet-tls,mnet-dns,mhttp-client, andmsendmail. Background tasks such as our daily update emails also now use miou's structured concurrency for clean timeouts and cancellation. - Block storage: Our fault-safe block filesystem now talks directly to the
mkernelblock device without needing a functor wrapper.
Restructuring the Web Application with vif and HTMX
We replaced the nearly 4,000+ line monolithic entrypoint with vif and split the codebase into clear responsibilities:
- Declarative routes and models: Routing, request handlers, and data models are now cleanly separated, with JSON serialization handled declaratively using
jsont. Routes declare the form or JSON schema they expect, andvifdecodes requests before they reach the handler. - Stateless CSRF and persistent sessions: Instead of keeping CSRF tokens in memory, Mollymawk now signs stateless HMAC-SHA256 CSRF tokens bound to the user's session. We also re-enabled saving user session cookies to disk so users stay logged in across unikernel restarts while ensuring disk writes only happen when sessions are created or closed, never on ordinary page loads.
- Replacing JavaScript boilerplate with HTMX: By moving shared HTML components into a dedicated UI module and using HTMX for form submissions and partial page updates, we shrank our client-side JavaScript from over 1,100+ lines to just over 100 lines.
Overall, the migration removed roughly 6,700 net lines of code across the repository while keeping all 66 routes and our native test suite intact.
Managing Dependencies Directly in OPAM
Previously, our dependencies and version constraints were split between the Mirage configuration file (config.ml) and the mirage tool itself. Whenever an upstream library introduced a breaking API change, updating or constraining versions often meant waiting on changes in the mirage tool. With our new stack, all dependencies and version bounds live directly in a standard OPAM file, giving us more flexibility when upstream libraries release updates and making local development, CI, and reproducible builds much simpler.
3. Some bugs Found Along the Way
Migrating the stack and testing the unikernel end-to-end helped us catch several bugs, both in mollymawk and in our upstream libraries:
- Double multiplication of sector offsets in the filesystem:
When writing data that spanned multiple 512-byte disk sectors, the function calculating the next sector pointer multiplied the sector number by the sector size (
512) twice. Instead of writing chunks 512 bytes apart, each chunk was written512 * 512 = 262,144bytes (256 KB) apart. Small payloads under four sectors (~2 KB) fit inside our 1 MB disk image and appeared to work, but as soon as a user saved larger data (such as an Albatross configuration with TLS certificates), the fifth sector jumped past the 1 MB boundary and caused subsequent disk reads to fail. - Parsing multiple cookies in a single HTTP
Cookieheader (vif): When a browser holds multiple cookies for a site (for example as with mollymawk: both a session cookie and a flash message cookie), it sends them in a singleCookieheader separated by semicolons (Cookie: a=1; b=2). We found that our cookie parser was not splitting the header value correctly, causing session lookups to fail whenever a second cookie was present. - Handling relative redirect URLs in
mhttp-client: When Mollymawk queriesbuilds.robur.coopto inspect or download unikernel binaries, the server responds with an HTTP redirect whoseLocationheader is a relative path rather than a full URL.mhttp-clientonly handled full URLs with a scheme and hostname when following redirects, which broke binary downloads.
4. Conclusion
Our production version of Mollymawk now runs entirely on our new OCaml 5 unikernel stack (Miou, mkernel, mnet, and vif). You can check it out at mollymawk.robur.coop. Moving away from multi-stage code generation, functors, and Lwt has made the codebase significantly smaller, easier to read, and simpler to build and test while exercising our new libraries in a real-world application and helping us improve them upstream.
To learn more about this new stack and our approach to developing and deploying unikernels in OCaml 5, check out our announcement on OCaml Discuss and visit our new website at https://uniker.nl.
The new mollymawk is being developed on our branch: https://git.robur.coop/robur/mollymawk/src/branch/init_mkernel and being built on our reproducible builds platform: builds.robur.coop. A draft PR to track changes and prepare merging into the main branch is open at https://git.robur.coop/robur/mollymawk/pulls/50.
Some issues we opened while working on this:
- Add an optional name argument for Mnet_dns_cli.setup(): https://git.robur.coop/robur/mnet/issues/92
- Parse multiple cookies in the cookie header: https://git.robur.coop/robur/vif/issues/81
- Mhttp_client: resolving redirects doesn't work well: https://github.com/robur-coop/mhttp/issues/15 with a PR proposed at http://github.com/robur-coop/mhttp/pull/17