Capsa is experimental software. APIs may change without notice.
Skip to content

Custom Kernels

For advanced use cases, you can bypass sandbox mode and use raw VMs with your own kernel and initrd.

When to Use Raw VMs

Use raw VMs instead of sandboxes when you need:

  • Custom kernel configuration
  • Specific kernel modules
  • Non-Linux operating systems
  • Complete control over the boot process
  • UEFI boot

Getting a Kernel and Initrd

Option 1: Extract from Linux Distribution

Most Linux distributions include a kernel and initrd:

bash
# Debian/Ubuntu
cp /boot/vmlinuz-$(uname -r) ./kernel
cp /boot/initrd.img-$(uname -r) ./initrd

# Fedora/RHEL
cp /boot/vmlinuz-$(uname -r) ./kernel
cp /boot/initramfs-$(uname -r).img ./initrd

Option 2: Build Custom (Nix)

Capsa's test VMs are built with Nix:

bash
# Build test VMs
nix-build nix -A vms.x86_64 -o result-vms
ls result-vms/
# default/  minimal/

Option 3: Minimal Linux

For testing, create a minimal initrd with BusyBox:

bash
# Create minimal initrd
mkdir -p initrd-root/{bin,dev,proc,sys}
cp /path/to/busybox-static initrd-root/bin/busybox

# Create init script
cat > initrd-root/init << 'EOF'
#!/bin/busybox sh
mount -t proc proc /proc
mount -t sysfs sys /sys
exec /bin/busybox sh
EOF
chmod +x initrd-root/init

# Pack initrd
cd initrd-root && find . | cpio -o -H newc | gzip > ../initrd.gz

Linux Direct Boot

Boot a kernel directly without a bootloader:

rust,no_run
let boot = LinuxDirectBoot::new("./kernel", "./initrd");

let vm = capsa::vm(boot)
    .cpus(2)
    .memory_mb(1024)
    .cmdline_arg("console", "ttyS0")
    .cmdline_flag("quiet")
    .console_enabled()
    .build()
    .await?;

let console = vm.console().await?;
console.wait_for("# ", Duration::from_secs(30)).await?;

Kernel Command Line

Customize kernel boot parameters:

rust,no_run
let boot = LinuxDirectBoot::new("./kernel", "./initrd");

let vm = capsa::vm(boot)
    .cmdline_arg("console", "ttyS0")
    .cmdline_arg("loglevel", "3")
    .cmdline_arg("root", "/dev/vda")
    .cmdline_flag("rw")
    .build()
    .await?;

Common parameters:

  • console=ttyS0 - Output to serial console
  • quiet - Reduce boot messages
  • loglevel=N - Set kernel log level (0-7)
  • root=/dev/vda - Root device (when using a disk)
  • rw - Mount root read-write

With Root Disk

Add a root filesystem disk:

rust,no_run
let boot = LinuxDirectBoot::new("./kernel", "./initrd")
    .with_root_disk("./rootfs.raw");

let vm = capsa::vm(boot)
    .cmdline_arg("console", "ttyS0")
    .cmdline_arg("root", "/dev/vda")
    .cmdline_flag("rw")
    .build()
    .await?;

UEFI Boot (macOS)

Boot from a disk with UEFI bootloader:

rust,no_run
let boot = UefiBoot::new("./disk.img");

let vm = capsa::vm(boot)
    .cpus(2)
    .memory_mb(2048)
    .build()
    .await?;

EFI Variable Store

Persist EFI variables across reboots:

rust,no_run
let boot = UefiBoot::new("./disk.img")
    .with_efi_variable_store("./nvram.efivarstore");

Adding Disks

Additional Disks

Add data disks beyond the root:

rust,no_run
let boot = LinuxDirectBoot::new("./kernel", "./initrd")
    .with_root_disk("./rootfs.raw");

let vm = capsa::vm(boot)
    .disk(DiskImage::new("./data.raw"))
    .build()
    .await?;

Read-Only Disks

Attach a disk as read-only:

rust,no_run
let vm = capsa::vm(boot)
    .disk(DiskImage::new("./readonly.raw").read_only())
    .build()
    .await?;

Shared Directories (Manual Mount)

With raw VMs, shared directories are not auto-mounted. You must mount them inside the guest:

rust,no_run
let share = VirtioFsShare::new("./workspace", "workspace")?;

let boot = LinuxDirectBoot::new("./kernel", "./initrd");

let vm = capsa::vm(boot)
    .virtio_fs(share)
    .console_enabled()
    .build()
    .await?;

let console = vm.console().await?;
console.wait_for("# ", Duration::from_secs(30)).await?;

// Mount manually inside the guest
console.write_line("mkdir -p /mnt/workspace").await?;
console.write_line("mount -t virtiofs workspace /mnt/workspace").await?;

Complete Example

rust,no_run
use capsa::boot::LinuxDirectBoot;
use capsa::{VirtioFsShare, VirtualNetwork};
use std::time::Duration;

async fn run_custom_vm() -> capsa::Result<()> {
    let boot = LinuxDirectBoot::new("./my-kernel", "./my-initrd")
        .with_root_disk("./my-rootfs.raw");

    let share = VirtioFsShare::new("./data", "hostdata")?;
    let network = VirtualNetwork::new();

    let vm = capsa::vm(boot)
        .cpus(4)
        .memory_mb(4096)
        .cmdline_arg("console", "ttyS0")
        .cmdline_flag("quiet")
        .console_enabled()
        .network(&network)?
        .virtio_fs(share)
        .vsock_listen(1024)
        .build()
        .await?;

    let console = vm.console().await?;

    // Wait for boot
    console.wait_for("login:", Duration::from_secs(60)).await?;
    console.write_line("root").await?;
    console.wait_for("# ", Duration::from_secs(5)).await?;

    // Mount shared directory
    console.write_line("mount -t virtiofs hostdata /mnt").await?;

    // Do work...

    vm.shutdown().await?;
    Ok(())
}

Next Steps

Released under the MIT License.