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 ./initrdOption 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.gzLinux 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 consolequiet- Reduce boot messagesloglevel=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
- Console - Raw serial port access
- Host-Guest Communication - Vsock for custom protocols
- VM Pools - Pre-warmed VMs for performance