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

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:
open -a 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):
1

Check disk space

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

Clean Docker cache

docker system prune -a
3

Check Docker logs

docker info

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
Last modified on March 20, 2026