Overview
SmolVM’s ImageBuilder creates custom VM images with SSH pre-configured, allowing you to build tailored environments for your workloads. It supports multiple Linux distributions, custom packages, and secure SSH key authentication.
Quick Start
from smolvm import ImageBuilder, SmolVM, VMConfig
from smolvm.build import SSH_BOOT_ARGS
# Build an Alpine Linux image with SSH
builder = ImageBuilder()
kernel, rootfs = builder.build_alpine_ssh(
name = "my-image" ,
ssh_password = "smolvm" ,
rootfs_size_mb = 512
)
# Use the image
config = VMConfig(
vm_id = "my-vm" ,
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 Basics
Initialization
from smolvm 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" ))
Image Caching
ImageBuilder caches built images to avoid rebuilding:
builder = ImageBuilder()
# First call: builds the image
kernel1, rootfs1 = builder.build_alpine_ssh( name = "cached-image" )
print ( "Built image" )
# Second call: returns cached image
kernel2, rootfs2 = builder.build_alpine_ssh( name = "cached-image" )
print ( "Used cached image" )
assert kernel1 == kernel2
assert rootfs1 == rootfs2
Images are cached by name. Use unique names for different configurations.
Alpine Linux Images
Password Authentication
Build Alpine Linux with SSH password authentication:
from smolvm import ImageBuilder
builder = ImageBuilder()
kernel, rootfs = builder.build_alpine_ssh(
name = "alpine-password" ,
ssh_password = "secure-password" ,
rootfs_size_mb = 512
)
print ( f "Kernel: { kernel } " )
print ( f "Rootfs: { rootfs } " )
# SSH with: ssh root@<guest-ip> (password: secure-password)
Key-Based Authentication
More secure option using SSH keys:
from smolvm import ImageBuilder
from smolvm.utils import ensure_ssh_key
builder = ImageBuilder()
# Generate or use existing SmolVM SSH key
private_key, public_key = ensure_ssh_key()
# Build image with key authentication
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = public_key,
name = "alpine-key" ,
rootfs_size_mb = 512
)
print ( f "Public key: { public_key } " )
print ( f "Private key: { private_key } " )
Using Custom SSH Keys
from pathlib import Path
# Read your own public key
my_pubkey = Path( "~/.ssh/id_rsa.pub" ).expanduser().read_text()
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = my_pubkey,
name = "alpine-custom-key" ,
rootfs_size_mb = 1024
)
Debian Linux Images
Build Debian-based images for applications needing apt packages:
from smolvm import ImageBuilder
from smolvm.utils import ensure_ssh_key
builder = ImageBuilder()
private_key, public_key = ensure_ssh_key()
kernel, rootfs = builder.build_debian_ssh_key(
ssh_public_key = public_key,
name = "debian-dev" ,
rootfs_size_mb = 2048 , # Debian needs more space
base_image = "debian:bookworm-slim"
)
Debian images require more disk space (minimum 2048 MB recommended) due to larger package footprint.
Custom Base Images
# Use different Debian versions
kernel, rootfs = builder.build_debian_ssh_key(
ssh_public_key = public_key,
name = "debian-bullseye" ,
rootfs_size_mb = 2048 ,
base_image = "debian:bullseye-slim"
)
# Or Ubuntu
kernel, rootfs = builder.build_debian_ssh_key(
ssh_public_key = public_key,
name = "ubuntu-22.04" ,
rootfs_size_mb = 3072 ,
base_image = "ubuntu:22.04"
)
Rootfs Sizing
Choosing the Right Size
builder = ImageBuilder()
# Minimal Alpine (base system only)
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = public_key,
name = "alpine-minimal" ,
rootfs_size_mb = 256 # Minimum recommended
)
# Development environment
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = public_key,
name = "alpine-dev" ,
rootfs_size_mb = 1024
)
# Application server
kernel, rootfs = builder.build_debian_ssh_key(
ssh_public_key = public_key,
name = "debian-app" ,
rootfs_size_mb = 4096 # Large application
)
Recommended sizes by use case
Minimal Alpine : 256-512 MB
Alpine with packages : 512-1024 MB
Debian base : 2048 MB minimum
Development environment : 2048-4096 MB
Application server : 4096+ MB
Boot Arguments
SSH_BOOT_ARGS
Always use SSH_BOOT_ARGS for SSH-capable images:
from smolvm import VMConfig
from smolvm.build import SSH_BOOT_ARGS
config = VMConfig(
vm_id = "ssh-vm" ,
kernel_path = kernel,
rootfs_path = rootfs,
boot_args = SSH_BOOT_ARGS , # Essential for SSH
)
The SSH_BOOT_ARGS constant:
# SSH_BOOT_ARGS = "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw init=/init"
Key components:
init=/init: Starts SmolVM’s custom init script
root=/dev/vda: Sets root filesystem device
rw: Mounts root as read-write
Without init=/init, SSH will not start automatically and vm.run() will fail.
Real-World Examples
Python Development Environment
from smolvm import ImageBuilder, SmolVM, VMConfig
from smolvm.build import SSH_BOOT_ARGS
from smolvm.utils import ensure_ssh_key
builder = ImageBuilder()
private_key, public_key = ensure_ssh_key()
# Build Debian image with enough space for Python packages
kernel, rootfs = builder.build_debian_ssh_key(
ssh_public_key = public_key,
name = "python-dev" ,
rootfs_size_mb = 3072
)
config = VMConfig(
vm_id = "python-dev" ,
vcpu_count = 2 ,
mem_size_mib = 2048 ,
kernel_path = kernel,
rootfs_path = rootfs,
boot_args = SSH_BOOT_ARGS ,
)
with SmolVM(config, ssh_key_path = str (private_key)) as vm:
# Install Python development tools
vm.run(
"apt-get update && apt-get install -y "
"python3 python3-pip python3-venv git" ,
timeout = 180
)
# Install common packages
vm.run( "pip3 install requests flask pytest black" )
result = vm.run( "python3 --version" )
print ( f "Python installed: { result.output } " )
Node.js Application Server
from smolvm import ImageBuilder, SmolVM, VMConfig
from smolvm.build import SSH_BOOT_ARGS
from smolvm.utils import ensure_ssh_key
builder = ImageBuilder()
private_key, public_key = ensure_ssh_key()
# Large rootfs for Node.js and npm packages
kernel, rootfs = builder.build_debian_ssh_key(
ssh_public_key = public_key,
name = "nodejs-server" ,
rootfs_size_mb = 4096
)
config = VMConfig(
vm_id = "node-app" ,
vcpu_count = 2 ,
mem_size_mib = 2048 ,
kernel_path = kernel,
rootfs_path = rootfs,
boot_args = SSH_BOOT_ARGS ,
)
with SmolVM(config, ssh_key_path = str (private_key)) as vm:
# Install Node.js 20.x
vm.run( "apt-get update && apt-get install -y curl gnupg" , timeout = 120 )
vm.run(
"curl -fsSL https://deb.nodesource.com/setup_20.x | bash -" ,
timeout = 120
)
vm.run( "apt-get install -y nodejs" , timeout = 180 )
# Verify installation
node_version = vm.run( "node --version" ).output
npm_version = vm.run( "npm --version" ).output
print ( f "Node.js: { node_version } " )
print ( f "npm: { npm_version } " )
Lightweight CI Runner
from smolvm import ImageBuilder, SmolVM, VMConfig
from smolvm.build import SSH_BOOT_ARGS
from smolvm.utils import ensure_ssh_key
builder = ImageBuilder()
private_key, public_key = ensure_ssh_key()
# Alpine for minimal resource usage
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = public_key,
name = "ci-runner" ,
rootfs_size_mb = 1024
)
config = VMConfig(
vm_id = "ci-runner" ,
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:
# Install CI tools
vm.run( "apk add --no-cache git python3 py3-pip bash make" )
# Clone and test a project
vm.run( "git clone https://github.com/example/repo.git /workspace" )
vm.run( "cd /workspace && pip3 install -r requirements.txt" )
result = vm.run( "cd /workspace && python3 -m pytest" , timeout = 300 )
if result.ok:
print ( "Tests passed!" )
else :
print ( f "Tests failed: \n { result.stderr } " )
Multi-Architecture Support
import platform
from smolvm import ImageBuilder
builder = ImageBuilder()
# Automatically builds for host architecture
arch = platform.machine()
print ( f "Building for { arch } " )
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = public_key,
name = f "alpine- { arch } " , # Include arch in name
rootfs_size_mb = 512
)
# Works on both x86_64 and aarch64 (ARM)
Docker Requirement
ImageBuilder requires Docker to build images:
from smolvm import ImageBuilder
from smolvm.exceptions import ImageError
builder = ImageBuilder()
# Check Docker availability
if not builder.check_docker():
print ( "Docker is required to build images" )
print ( "Install: https://docs.docker.com/get-docker/" )
else :
kernel, rootfs = builder.build_alpine_ssh()
Install Docker
macOS : Install Docker Desktop
Linux : sudo apt-get install docker.io
Windows : Install Docker Desktop with WSL2
Build images
builder = ImageBuilder()
kernel, rootfs = builder.build_alpine_ssh_key(public_key)
Image Storage
Default Location
Images are cached in ~/.smolvm/images/:
~/.smolvm/images/
├── alpine-ssh/
│ ├── vmlinux.bin
│ └── rootfs.ext4
├── alpine-key/
│ ├── vmlinux.bin
│ └── rootfs.ext4
└── debian-dev/
├── vmlinux.bin
└── rootfs.ext4
Custom Cache Directory
from pathlib import Path
builder = ImageBuilder( cache_dir = Path( "/data/vm-images" ))
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = public_key,
name = "custom-location"
)
print (kernel) # /data/vm-images/custom-location/vmlinux.bin
print (rootfs) # /data/vm-images/custom-location/rootfs.ext4
Automatic Key Staleness Detection
ImageBuilder rebuilds if the SSH key is newer than the cached image:
from pathlib import Path
pubkey_file = Path( "~/.ssh/id_rsa.pub" ).expanduser()
# First build
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = pubkey_file,
name = "key-aware"
)
print ( "Built new image" )
# Modify public key
pubkey_file.touch() # Update modification time
# Rebuild automatically detected
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = pubkey_file,
name = "key-aware"
)
print ( "Rebuilt due to key change" )
Error Handling
from smolvm import ImageBuilder
from smolvm.exceptions import ImageError
builder = ImageBuilder()
try :
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = "invalid-key-format" ,
name = "test"
)
except ImageError as e:
print ( f "Build failed: { e } " )
# Invalid SSH public key format
try :
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = public_key,
rootfs_size_mb = 32 # Too small
)
except ValueError as e:
print ( f "Configuration error: { e } " )
Advanced: Custom Kernel URLs
Override the default kernel:
# Use custom kernel URL
kernel, rootfs = builder.build_alpine_ssh_key(
ssh_public_key = public_key,
name = "custom-kernel" ,
kernel_url = "https://example.com/custom-vmlinux.bin"
)
Best Practices
Use descriptive image names
Include purpose, distro, and version in names: builder.build_alpine_ssh_key(public_key, name = "api-server-alpine-3.19" )
Size appropriately
Don’t over-allocate disk space: # Minimal: 512 MB
# Development: 1-2 GB
# Production: 2-4 GB
Use key authentication
Prefer SSH keys over passwords for security: builder.build_alpine_ssh_key(public_key) # Preferred
# vs
builder.build_alpine_ssh( ssh_password = "password" ) # Less secure
Cache strategically
Reuse images across VMs with the same requirements: # Build once
kernel, rootfs = builder.build_alpine_ssh_key(public_key, name = "shared" )
# Use many times
for i in range ( 10 ):
config = VMConfig( vm_id = f "vm- { i } " , kernel_path = kernel, rootfs_path = rootfs, ... )
Next Steps
Basic Usage Use custom images with SmolVM
Environment Variables Configure images with environment variables
AI Agent Integration Build specialized images for AI agents