Skip to main content

Overview

SmolVM’s ImageBuilder class provides tools to build custom VM images with SSH pre-configured. Images are built using Docker and cached for reuse.

Quick Start

from smolvm import SmolVM, VMConfig
from smolvm.build import ImageBuilder, SSH_BOOT_ARGS

builder = ImageBuilder()
kernel, rootfs = builder.build_alpine_ssh()

config = VMConfig(
    vm_id="my-custom-vm",
    vcpu_count=1,
    mem_size_mib=512,
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
)

with SmolVM(config) as vm:
    result = vm.run("cat /etc/os-release")
    print(result.output)

ImageBuilder Class

Initialization

from smolvm.build import ImageBuilder
from pathlib import Path

# Use default cache directory (~/.smolvm/images/)
builder = ImageBuilder()

# Use custom cache directory
builder = ImageBuilder(cache_dir=Path("/custom/cache/path"))

Check Docker Availability

if builder.check_docker():
    print("Docker is available")
else:
    print("Docker is required but not found")
ImageBuilder requires Docker to be installed and running. On macOS, use Docker Desktop. On Linux, install docker.io.

Build Methods

1. Alpine Linux with SSH Password

Build a minimal Alpine Linux image with password authentication:
kernel, rootfs = builder.build_alpine_ssh(
    name="my-alpine",           # Cache name (default: "alpine-ssh")
    ssh_password="mypassword",  # Root password (default: "smolvm")
    rootfs_size_mb=512,         # Disk size in MB (default: 512)
)
Features:
  • Alpine Linux 3.19 base
  • OpenSSH server pre-configured
  • Root password authentication enabled
  • Custom /init script for network setup
  • DNS resolution configured (8.8.8.8, 8.8.4.4)
Method Signature:
def build_alpine_ssh(
    self,
    name: str = "alpine-ssh",
    ssh_password: str = "smolvm",
    rootfs_size_mb: int = 512,
    kernel_url: str | None = None,
) -> tuple[Path, Path]:
    """Build Alpine Linux image with SSH server.

    Returns:
        Tuple of (kernel_path, rootfs_path).
    """
Build Alpine with public key authentication (more secure):
from smolvm.utils import ensure_ssh_key

# Generate or load SSH keys
private_key, public_key = ensure_ssh_key()

# Build image with key-only auth
kernel, rootfs = builder.build_alpine_ssh_key(
    ssh_public_key=public_key,
    name="alpine-key",
    rootfs_size_mb=512,
)

config = VMConfig(
    vm_id="key-vm",
    vcpu_count=1,
    mem_size_mib=512,
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
)

with SmolVM(config, ssh_key_path=str(private_key)) as vm:
    result = vm.run("whoami")
    print(result.output)  # "root"
Method Signature:
def build_alpine_ssh_key(
    self,
    ssh_public_key: str | Path,
    name: str = "alpine-ssh-key",
    rootfs_size_mb: int = 512,
    kernel_url: str | None = None,
) -> tuple[Path, Path]:
    """Build Alpine Linux image with key-only SSH access.

    Args:
        ssh_public_key: Public key content or path to a public key file.
        name: Image name for caching.
        rootfs_size_mb: Size of rootfs in MB.
        kernel_url: Optional kernel URL override.

    Returns:
        Tuple of (kernel_path, rootfs_path).
    """

3. Debian Linux with SSH Key

Build a Debian-based image (larger but more compatible):
from smolvm.utils import ensure_ssh_key

private_key, public_key = ensure_ssh_key()

kernel, rootfs = builder.build_debian_ssh_key(
    ssh_public_key=public_key,
    name="debian-key",
    rootfs_size_mb=2048,  # Debian needs more space
    base_image="debian:bookworm-slim",
)

config = VMConfig(
    vm_id="debian-vm",
    vcpu_count=2,
    mem_size_mib=1024,
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
)

with SmolVM(config, ssh_key_path=str(private_key)) as vm:
    result = vm.run("cat /etc/debian_version")
    print(result.output)
Method Signature:
def build_debian_ssh_key(
    self,
    ssh_public_key: str | Path,
    name: str = "debian-ssh-key",
    rootfs_size_mb: int = 2048,
    base_image: str = "debian:bookworm-slim",
    kernel_url: str | None = None,
) -> tuple[Path, Path]:
    """Build Debian Linux image with key-only SSH access."""

Boot Arguments

Use the SSH_BOOT_ARGS constant for SSH-capable images:
from smolvm.build import SSH_BOOT_ARGS

print(SSH_BOOT_ARGS)
# "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw init=/init"
The init=/init parameter is required for SmolVM’s SSH functionality. Without it, the custom init script won’t run and SSH won’t start.

Caching Behavior

ImageBuilder caches built images to avoid rebuilding:
builder = ImageBuilder()

# First build downloads and builds everything
kernel1, rootfs1 = builder.build_alpine_ssh(name="my-image")
# Building Alpine SSH image 'my-image'...

# Second call with same name returns cached paths instantly
kernel2, rootfs2 = builder.build_alpine_ssh(name="my-image")
# Image 'my-image' already exists at /home/user/.smolvm/images/my-image

assert kernel1 == kernel2
assert rootfs1 == rootfs2

Cache Invalidation

For key-based images, the cache is invalidated if the SSH key is newer than the cached image:
# First build
kernel, rootfs = builder.build_alpine_ssh_key(
    ssh_public_key=Path("/path/to/key.pub"),
    name="key-image",
)

# If you update the key file, next build will detect staleness
# and rebuild automatically

Custom Cache Directory

from pathlib import Path

custom_cache = Path("/mnt/fast-ssd/vm-images")
builder = ImageBuilder(cache_dir=custom_cache)

kernel, rootfs = builder.build_alpine_ssh()
print(kernel)  # /mnt/fast-ssd/vm-images/alpine-ssh/vmlinux.bin
print(rootfs)  # /mnt/fast-ssd/vm-images/alpine-ssh/rootfs.ext4

Advanced: Kernel URLs

Override the default kernel URL:
# Use a custom kernel
kernel, rootfs = builder.build_alpine_ssh(
    kernel_url="https://example.com/my-custom-kernel"
)
Default Kernel URLs:
from smolvm.build import FIRECRACKER_KERNEL_URLS, QEMU_KERNEL_URLS

print(FIRECRACKER_KERNEL_URLS)
# {
#     "x86_64": "https://s3.amazonaws.com/.../vmlinux-5.10.198",
#     "aarch64": "https://s3.amazonaws.com/.../vmlinux-5.10.198",
# }

print(QEMU_KERNEL_URLS)
# {
#     "x86_64": "https://cloud-images.ubuntu.com/.../vmlinuz-generic",
#     "aarch64": "https://cloud-images.ubuntu.com/.../vmlinuz-generic",
# }

Real-world Example: OpenClaw Installation

From examples/openclaw.py - building a 4GB Debian image for OpenClaw:
from smolvm import SmolVM, VMConfig, SSH_BOOT_ARGS
from smolvm.build import ImageBuilder
from smolvm.utils import ensure_ssh_key

private_key, public_key = ensure_ssh_key()

# Build large Debian image for OpenClaw
kernel, rootfs = ImageBuilder().build_debian_ssh_key(
    ssh_public_key=public_key,
    name="debian-ssh-key-openclaw-4g",
    rootfs_size_mb=4096,  # 4GB for Node.js + npm packages
)

config = VMConfig(
    vcpu_count=1,
    mem_size_mib=2048,  # OpenClaw npm install is memory-intensive
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
    env_vars={
        "OPENROUTER_API_KEY": os.getenv("OPENROUTER_API_KEY", ""),
    },
)

with SmolVM(config, ssh_key_path=str(private_key)) as vm:
    # Install Node.js, npm, and OpenClaw
    vm.run("apt-get update && apt-get install -y nodejs npm")
    vm.run("npm install -g openclaw")

The Init Script

All SSH-capable images include a custom /init script that runs as PID 1:
#!/bin/sh
# SmolVM custom init - runs as PID 1 inside Firecracker VM

# Mount essential filesystems
mount -t proc proc /proc
mount -t sysfs sys /sys
mount -t devtmpfs dev /dev
mount -t devpts devpts /dev/pts
mount -t tmpfs tmpfs /run
mount -t tmpfs tmpfs /tmp

# Remount root read-write
mount -o remount,rw /

# Configure networking from kernel command line
IP_CONFIG=$(cat /proc/cmdline | tr ' ' '\n' | grep '^ip=' | head -1)
GUEST_IP=$(echo "$IP_CONFIG" | cut -d= -f2 | cut -d: -f1)
GATEWAY=$(echo "$IP_CONFIG" | cut -d= -f2 | cut -d: -f3)

ip link set lo up
ip link set eth0 up
ip addr add "${GUEST_IP}/24" dev eth0
ip route add default via "${GATEWAY}" dev eth0

# Configure DNS
echo "nameserver 8.8.8.8" > /etc/resolv.conf

# Generate SSH host keys if missing
if ! ls /etc/ssh/ssh_host_*_key >/dev/null 2>&1; then
    ssh-keygen -A
fi

# Start SSH server
/usr/sbin/sshd -e

echo "SmolVM init complete: IP=${GUEST_IP}, SSH listening on port 22"

# Keep PID 1 alive
while true; do
    sleep 3600 &
    wait $!
done
This init script:
  • Mounts essential filesystems
  • Configures networking from kernel boot args
  • Sets up DNS resolution
  • Generates SSH host keys
  • Starts the SSH daemon
  • Stays alive as PID 1

Troubleshooting

Docker Not Found

from smolvm.build import ImageBuilder
from smolvm.exceptions import ImageError

try:
    builder = ImageBuilder()
    builder.build_alpine_ssh()
except ImageError as e:
    print(e)
    # "Docker is required to build images. Install Docker Desktop (macOS) or docker.io (Linux)."
Solution:
  • macOS: Install Docker Desktop
  • Linux: sudo apt-get install docker.io (Debian/Ubuntu)

Permission Errors (Linux)

If you see permission errors during image builds on Linux:
# Add your user to the docker group
sudo usermod -aG docker $USER

# Or run the SmolVM setup script
sudo ./scripts/system-setup.sh --configure-runtime

Image Build Fails

If Docker build fails:
1

Check Docker is running

docker ps
2

Clean Docker cache

docker system prune -a
3

Check disk space

Image builds require sufficient disk space:
df -h ~/.smolvm/images/

Next Steps

Last modified on March 3, 2026