Skip to main content

Overview

SmolVM allows you to reconnect to existing VMs using their unique identifier. This enables persistent workflows where you can stop, restart, and reconnect to VMs across different Python sessions or processes.

Quick Start

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

# Session 1: Create a VM with a known ID
config = VMConfig(
    vm_id="my-persistent-vm",
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
)

vm = SmolVM(config)
vm.start()
vm.run("echo 'Hello' > /data/greeting.txt")
vm.stop()  # Don't delete
vm.close()

# Session 2: Reconnect to the same VM
vm = SmolVM.from_id("my-persistent-vm")
vm.start()
result = vm.run("cat /data/greeting.txt")
print(result.output)  # Hello
vm.stop()
vm.delete()
vm.close()

Reconnecting Methods

Using from_id() Class Method

The recommended way to reconnect:
from smolvm import SmolVM

# Reconnect to existing VM
vm = SmolVM.from_id(
    vm_id="my-vm",
    ssh_user="root",
    ssh_key_path="~/.smolvm/keys/id_ed25519"
)

vm.start()
print(f"Reconnected to {vm.vm_id}")
print(f"Status: {vm.status}")

Using Constructor with vm_id Parameter

Alternative syntax:
vm = SmolVM(
    vm_id="my-vm",
    ssh_user="root",
    ssh_key_path="~/.smolvm/keys/id_ed25519"
)
Both methods are equivalent. Use from_id() for clarity when reconnecting.

Ownership and Lifecycle

Created VMs vs Reconnected VMs

SmolVM tracks whether it “owns” a VM:
from smolvm import SmolVM

# Created by this instance
with SmolVM() as vm:
    # Automatically starts
    vm.run("hostname")
# Automatically stops AND deletes

Context Manager Behavior

ActionCreated VMReconnected VM
On enterAuto-startsNo change
On exit (normal)Stops & deletesStops only
On exit (exception)Stops & deletesStops only
# Reconnected VM is NOT deleted on exit
with SmolVM.from_id("my-vm") as vm:
    vm.start()
    vm.run("some-command")
# VM is stopped but still exists

# Explicitly delete if needed
vm.delete()

Persistent Workflow Patterns

Long-Running Development Environment

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

def get_or_create_dev_vm(vm_id: str) -> SmolVM:
    """Get existing dev VM or create a new one."""
    try:
        # Try to reconnect
        vm = SmolVM.from_id(vm_id)
        print(f"Reconnected to existing VM: {vm_id}")
        return vm
    except Exception:
        # Create new VM
        print(f"Creating new VM: {vm_id}")
        config = VMConfig(
            vm_id=vm_id,
            kernel_path=kernel,
            rootfs_path=rootfs,
            boot_args=SSH_BOOT_ARGS,
        )
        return SmolVM(config)

# Use in different sessions
vm = get_or_create_dev_vm("dev-env")
vm.start()

# Do work...
vm.run("git clone https://github.com/example/repo.git /workspace")

# Stop but keep VM
vm.stop()
vm.close()

# Later: reconnect and continue
vm = get_or_create_dev_vm("dev-env")
vm.start()
result = vm.run("ls /workspace")
print(result.output)  # repo is still there

Stateful Application Server

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

VMID = "app-server"

# Initial setup
config = VMConfig(
    vm_id=VMID,
    vcpu_count=2,
    mem_size_mib=2048,
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
)

vm = SmolVM(config)
vm.start()

# Install application
vm.run("apk add --no-cache python3 py3-pip")
vm.run("pip3 install flask redis")
vm.run("mkdir -p /app")
# ... deploy application files ...

# Start application
vm.run("nohup python3 /app/server.py > /tmp/app.log 2>&1 &")
port = vm.expose_local(guest_port=5000)

print(f"Application running at http://localhost:{port}")
print("VM can be stopped and restarted")

# Stop VM (application state persists in disk)
vm.stop()
vm.close()

print("\n--- Later: Restart the application ---\n")

# Reconnect and restart
vm = SmolVM.from_id(VMID)
vm.start()

# Application files are still there
result = vm.run("ls /app")
print(f"App files: {result.output}")

# Restart the service
vm.run("nohup python3 /app/server.py > /tmp/app.log 2>&1 &")
port = vm.expose_local(guest_port=5000)
print(f"Application restarted at http://localhost:{port}")

Multi-Process Coordination

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

def worker(vm_id: str, task_id: int):
    """Worker process connects to shared VM."""
    vm = SmolVM.from_id(vm_id)
    vm.start()
    
    result = vm.run(f"echo 'Task {task_id} completed' >> /shared/results.txt")
    print(f"Worker {task_id}: {result.ok}")
    
    vm.stop()
    vm.close()

# Main process: create VM
config = VMConfig(
    vm_id="shared-vm",
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
)

vm = SmolVM(config)
vm.start()
vm.run("mkdir -p /shared")
vm.run("touch /shared/results.txt")
vm.stop()
vm.close()

# Spawn workers that reconnect
processes = []
for i in range(5):
    p = multiprocessing.Process(target=worker, args=("shared-vm", i))
    p.start()
    processes.append(p)

for p in processes:
    p.join()

# Check results
vm = SmolVM.from_id("shared-vm")
vm.start()
result = vm.run("cat /shared/results.txt")
print(f"\nResults:\n{result.output}")
vm.delete()  # Clean up

Disk Persistence

Isolated Disk Mode (Default)

By default, VMs use isolated disks that persist between restarts:
config = VMConfig(
    vm_id="persistent-vm",
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
    disk_mode="isolated",  # Default
)

vm = SmolVM(config)
vm.start()

# Write data
vm.run("echo 'persistent data' > /data/file.txt")
vm.stop()
vm.close()

# Reconnect - data is still there
vm = SmolVM.from_id("persistent-vm")
vm.start()
result = vm.run("cat /data/file.txt")
print(result.output)  # persistent data

Retaining Disk on Delete

config = VMConfig(
    vm_id="recoverable-vm",
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
    retain_disk_on_delete=True,  # Keep disk after delete
)

vm = SmolVM(config)
vm.start()
vm.run("echo 'important data' > /important.txt")
vm.stop()
vm.delete()  # VM deleted, but disk retained
vm.close()

# Recreate VM with same ID
config2 = VMConfig(
    vm_id="recoverable-vm",  # Same ID
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
)

vm2 = SmolVM(config2)
vm2.start()
result = vm2.run("cat /important.txt")
print(result.output)  # important data
Retained disks remain in the data directory until manually removed or a new VM with the same ID is created without retain_disk_on_delete.

VM Discovery and Management

Listing VMs

from smolvm import SmolVMManager

# List all VMs
manager = SmolVMManager()
vms = manager.list_vms()

for vm_info in vms:
    print(f"VM: {vm_info.vm_id}")
    print(f"  Status: {vm_info.status}")
    print(f"  IP: {vm_info.network.guest_ip if vm_info.network else 'N/A'}")
    print()

Checking VM Existence

from smolvm import SmolVMManager
from smolvm.exceptions import VMNotFoundError

def vm_exists(vm_id: str) -> bool:
    """Check if a VM exists."""
    try:
        manager = SmolVMManager()
        manager.get(vm_id)
        return True
    except VMNotFoundError:
        return False

if vm_exists("my-vm"):
    vm = SmolVM.from_id("my-vm")
else:
    vm = SmolVM(VMConfig(vm_id="my-vm", ...))

Error Handling

VM Not Found

from smolvm import SmolVM
from smolvm.exceptions import VMNotFoundError

try:
    vm = SmolVM.from_id("nonexistent-vm")
except VMNotFoundError as e:
    print(f"VM not found: {e}")
    # Create instead of reconnecting

Stale VMs

from smolvm import SmolVM
from smolvm.exceptions import SmolVMError

try:
    vm = SmolVM.from_id("old-vm")
    vm.start()
except SmolVMError as e:
    print(f"Failed to start VM: {e}")
    # VM might be in error state
    vm.delete()  # Clean up and recreate

Advanced Patterns

VM Pool Management

from typing import Dict
from smolvm import SmolVM, VMConfig
from smolvm.build import SSH_BOOT_ARGS

class VMPool:
    """Manage a pool of reusable VMs."""
    
    def __init__(self, pool_size: int, kernel: str, rootfs: str):
        self.pool_size = pool_size
        self.kernel = kernel
        self.rootfs = rootfs
        self.available: Dict[str, SmolVM] = {}
        self.in_use: Dict[str, SmolVM] = {}
    
    def initialize(self):
        """Create initial pool of VMs."""
        for i in range(self.pool_size):
            vm_id = f"pool-vm-{i}"
            config = VMConfig(
                vm_id=vm_id,
                kernel_path=self.kernel,
                rootfs_path=self.rootfs,
                boot_args=SSH_BOOT_ARGS,
            )
            vm = SmolVM(config)
            vm.start()
            vm.stop()  # Prepared but stopped
            self.available[vm_id] = vm
    
    def acquire(self) -> SmolVM:
        """Get a VM from the pool."""
        if not self.available:
            raise RuntimeError("No VMs available in pool")
        
        vm_id, vm = self.available.popitem()
        vm.start()
        self.in_use[vm_id] = vm
        return vm
    
    def release(self, vm: SmolVM):
        """Return a VM to the pool."""
        vm.stop()
        vm_id = vm.vm_id
        self.in_use.pop(vm_id)
        self.available[vm_id] = vm
    
    def cleanup(self):
        """Delete all VMs in the pool."""
        for vm in list(self.available.values()):
            vm.delete()
        for vm in list(self.in_use.values()):
            vm.delete()

# Usage
pool = VMPool(pool_size=5, kernel=kernel, rootfs=rootfs)
pool.initialize()

vm1 = pool.acquire()
vm1.run("echo 'Task 1'")
pool.release(vm1)

vm2 = pool.acquire()  # Might get vm1 back
vm2.run("echo 'Task 2'")
pool.release(vm2)

pool.cleanup()

Session-Based Reconnection

import json
from pathlib import Path
from smolvm import SmolVM, VMConfig
from smolvm.build import SSH_BOOT_ARGS

SESSION_FILE = Path(".vm_session.json")

def save_session(vm: SmolVM):
    """Save VM session info."""
    session_data = {
        "vm_id": vm.vm_id,
        "status": vm.status.value,
    }
    SESSION_FILE.write_text(json.dumps(session_data))

def load_session() -> SmolVM | None:
    """Load previous VM session."""
    if not SESSION_FILE.exists():
        return None
    
    session_data = json.loads(SESSION_FILE.read_text())
    try:
        vm = SmolVM.from_id(session_data["vm_id"])
        print(f"Restored session: {vm.vm_id}")
        return vm
    except Exception:
        return None

# Try to restore previous session
vm = load_session()

if vm is None:
    # Create new session
    config = VMConfig(
        vm_id="session-vm",
        kernel_path=kernel,
        rootfs_path=rootfs,
        boot_args=SSH_BOOT_ARGS,
    )
    vm = SmolVM(config)
    print("Created new session")

vm.start()
save_session(vm)

# Do work...
vm.run("echo 'Work in progress' > /work/status.txt")

vm.stop()
vm.close()

Best Practices

1

Use meaningful VM IDs

Choose IDs that describe the VM’s purpose:
# Good
VMConfig(vm_id="api-server-prod")
VMConfig(vm_id="ci-runner-node-1")

# Avoid
VMConfig(vm_id="vm123")
VMConfig(vm_id="test")
2

Handle reconnection failures

Always handle cases where VM might not exist:
try:
    vm = SmolVM.from_id("my-vm")
except VMNotFoundError:
    # Create instead
    vm = SmolVM(VMConfig(vm_id="my-vm", ...))
3

Clean up when done

Explicitly delete VMs you won’t need again:
vm = SmolVM.from_id("temp-vm")
try:
    vm.start()
    # Do work
finally:
    vm.delete()  # Clean up
    vm.close()
4

Document persistence requirements

Be clear about which VMs need to persist:
# Ephemeral - will be deleted
with SmolVM() as vm:
    pass

# Persistent - manual lifecycle
vm = SmolVM(VMConfig(vm_id="persistent-server", ...))

Next Steps

Basic Usage

Learn VM lifecycle fundamentals

Environment Variables

Manage persistent configuration

AI Agent Integration

Build stateful agent environments

Build docs developers (and LLMs) love