Running FreeBSD workloads in urunc🔗
While FreeBSD is not a unikernel framework, it can boot as a lightweight guest and run a single application. This guide walks through how to deploy FreeBSD workloads on top of urunc as a FreeBSD microVM on Firecracker or QEMU, configured (network, environment, user, command) by urunit as its init process.
Overall, we need to do the following:
- Build or reuse a FreeBSD kernel.
- (Optional but recommended) Include the urunit init process.
- Prepare the final image by appending the FreeBSD kernel (and
urunit) and set up theuruncannotations.
FreeBSD kernel🔗
The main requirement for running FreeBSD workloads on top of urunc is a FreeBSD kernel that boots directly (PVH) with virtio-mmio devices and ACPI. The upstream FIRECRACKER kernel configuration is a good start, with hint.acpi.0.disabled="0" (Firecracker >= 1.12 provides ACPI tables and no longer a valid MP table) and options EXT2FS to utilize devmapper as a snapshotter. The kernel must include virtio_mmio, virtio_blk and vtnet; for a 9p rootfs (see below) it must also include the p9fs/virtio_p9fs client.
Init process🔗
The FreeBSD kernel starts init without arguments and does not pass boot parameters as environment variables. Therefore, urunc passes the workload information into the microVM using a raw virtio-block device. This is done through the urunit configuration, which contains info about the network setup, the application to execute, the working directory, uid/gid, etc.
urunit performs the following actions:
- Opens
/dev/consoleand reads the configuration from/dev/vtbdN. - Configures
vtnet0with the IP, netmask and gateway of the container (a host route to the gateway is added first when it is outside the subnet). - Mounts block volumes (
/dev/vtbdNin attach order, or/dev/diskid/DISK-<serial>with QEMU) asext2fsorufs. - Sets the environment variables, uid/gid and working directory of the container and executes the container command (
CMD/docker runarguments). - Reaps children and reboots the VM when the application exits, which terminates the microVM.
If the image does not contain urunit, the kernel falls back to /sbin/init of the image and the configuration is ignored.
The configuration format is the one described in the Linux tutorial with two additions:
UCS
UID:<uid>
GID:<gid>
WD:<working directory>
ARC:<number of arguments>
ARV:<argument> (one line per argument)
UCE
...
UNS
IP:<ipv4 address>
GW:<gateway>
MSK:<netmask>
UNE
Preparing the image🔗
To differentiate traditional containers from unikernels, urunc uses specific annotations. Therefore, to run a container with a FreeBSD kernel on urunc, these annotations must be configured and the kernel (along with urunit) must be included in the container image.
Since we are booting a full FreeBSD microVM, a root filesystem must be provided. There are two ways to do this:
- Using a block image (UFS2) as the rootfs, on Firecracker or QEMU.
- Using the container's own rootfs directly, shared over 9p (QEMU only).
Using a block image🔗
Preparing the container image.🔗
First create a block image with the root filesystem (UFS2 created with makefs(8)) containing the application and urunit at /urunit.
To package everything as an OCI image we will use bunny. There are two file formats we can use:
a) bunnyfile:
#syntax=harbor.nbfc.io/nubificus/bunny:latest
version: v0.1
platforms:
framework: freebsd
monitor: firecracker #change to qemu for Qemu
architecture: x86
rootfs:
from: local
path: rootfs.ufs
type: block
kernel:
from: local
path: kernel
entrypoint: ["/urunit"]
cmd: ["/bin/sh"]
b) Dockerfile-like:
#syntax=harbor.nbfc.io/nubificus/bunny:latest
FROM scratch
COPY kernel /kernel
COPY rootfs.ufs /rootfs.ufs
LABEL "com.urunc.unikernel.binary"="/kernel"
LABEL "com.urunc.unikernel.block"="/rootfs.ufs"
LABEL "com.urunc.unikernel.blkMntPoint"="/"
LABEL "com.urunc.unikernel.unikernelType"="freebsd"
LABEL "com.urunc.unikernel.hypervisor"="firecracker" # change to qemu for Qemu
ENTRYPOINT ["/urunit"]
CMD ["/bin/sh"]
In both cases we can build the OCI image with the same command:
Running the container🔗
docker run --rm -e FOO=bar -w /tmp --runtime io.containerd.urunc.v2 freebsd/firecracker/block:latest
The guest gets the IP of the container, so services listening inside the microVM are reachable at the container address, e.g. with nc -l 8080 in the guest and curl http://<container-ip>:8080 on the host. docker run --user 1001:1001 runs both the monitor and the application as that user.
Using the container's rootfs directly🔗
Preparing the container image.🔗
There is also the option to use the container's own filesystem as the guest root, without a block image, either using 9pfs or devmapper as a snapshotter. In that case the image is a regular FreeBSD tree plus the kernel and urunit:
#syntax=harbor.nbfc.io/nubificus/bunny:latest
version: v0.1
platforms:
framework: freebsd
monitor: qemu #change to firecracker for Firecracker
architecture: x86
rootfs:
from: freebsd/freebsd-runtime:14.4
type: raw
include:
- urunit:/urunit
kernel:
from: local
path: kernel
entrypoint: ["/urunit"]
cmd: ["/bin/sh"]
#syntax=harbor.nbfc.io/nubificus/bunny:latest
FROM --platform=freebsd/amd64 freebsd/freebsd-runtime:14.4
COPY kernel /kernel
COPY urunit /urunit
LABEL com.urunc.unikernel.binary=/kernel
LABEL com.urunc.unikernel.unikernelType=freebsd
LABEL com.urunc.unikernel.hypervisor=qemu # Change to firecracker for Firecracker
LABEL com.urunc.unikernel.mountRootfs=true
ENTRYPOINT ["/urunit"]
CMD ["/bin/sh"]
In both cases we build the image with:
Running the container🔗
To run the image using 9pfs for the rootfs (QEMU only):
and with devmapper as a snapshotter for a block-based rootfs:
NOTE: Docker requires special configuration to use devmapper as a snapshotter.
Limitations🔗
/etc/resolv.confand/etc/hostsof the container are not visible in the guest when the rootfs is a block image inside the container image (they are with a 9p rootfs).