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.