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).
"""
2. Alpine Linux with SSH Key (Recommended)
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
When an image build fails, SmolVM automatically diagnoses the root cause and returns a specific error message. You don’t need to debug Docker issues manually — the ImageError you receive tells you exactly what went wrong and how to fix it.
Docker not installed
If Docker is not found on your system, you get a clear install prompt:
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)
Docker daemon not running
If Docker is installed but the daemon is not reachable, SmolVM detects this and tells you to start it:
Docker is installed, but SmolVM could not reach the Docker daemon.
Start Docker Desktop or the Docker service and try again.
Solution:
Open Docker Desktop from your Applications folder, or run: sudo systemctl start docker
Permission denied on Docker socket
If your user cannot access the Docker daemon socket, you get a targeted permission error:
Docker is installed, but this user cannot access the Docker daemon socket.
Make sure Docker Desktop is running or grant access to /var/run/docker.sock.
Solution:
# Add your user to the docker group
sudo usermod -aG docker $USER
# Log out and back in, or run:
newgrp docker
Checking Docker availability in code
You can check Docker availability before attempting a build:
from smolvm.build import ImageBuilder
builder = ImageBuilder()
if builder.check_docker():
kernel, rootfs = builder.build_alpine_ssh()
else :
# Get a detailed diagnostic error
error = builder.docker_requirement_error()
print (error) # Specific message based on what's wrong
General build failures
If the Docker build itself fails (after Docker is confirmed available):
Check disk space
Image builds require sufficient disk space:
Next Steps
Environment Variables Inject configuration into custom images
AI Agent Integration Build specialized images for AI agent sandboxes
VM Lifecycle Manage VMs built with custom images