Overview
SmolVM provides expose_local() to forward TCP ports from the guest VM to localhost on your host machine. This allows you to access services running inside the VM (like web servers, databases, or APIs) from your host.
Basic Port Forwarding
Expose a guest port to localhost:
from smolvm import SmolVM
with SmolVM() as vm:
# Start a web server in the guest
vm.run("python3 -m http.server 8000 &")
# Expose guest port 8000 to host localhost:8000
host_port = vm.expose_local(guest_port=8000, host_port=8000)
print(f"Service available at http://127.0.0.1:{host_port}/")
# Access the service from your host
import requests
response = requests.get(f"http://127.0.0.1:{host_port}/")
print(response.text)
Automatic Port Allocation
Omit host_port to let SmolVM choose an available port:
with SmolVM() as vm:
vm.run("python3 -m http.server 8080 &")
# SmolVM picks an available port automatically
host_port = vm.expose_local(guest_port=8080)
print(f"Guest port 8080 exposed on localhost:{host_port}")
Automatic port allocation is useful when the desired port might already be in use, or when running multiple VMs simultaneously.
Method Signature
def expose_local(self, guest_port: int, host_port: int | None = None) -> int:
"""Expose a guest TCP port on localhost only.
Forwards 127.0.0.1:<host_port> on the host to
<guest_ip>:<guest_port> inside the VM.
Args:
guest_port: Guest TCP port to expose.
host_port: Host localhost port. If omitted, an available port is chosen.
Returns:
The host localhost port to connect to.
"""
Transport Methods
SmolVM automatically selects the best transport method:
nftables (Preferred)
SSH Tunnel (Fallback)
Uses Linux nftables for high-performance port forwarding:# SmolVM attempts nftables first (Linux only)
host_port = vm.expose_local(guest_port=3000)
# Forwarding configured via nftables NAT rules
Advantages:
- Native Linux networking
- Higher performance
- Lower overhead
Falls back to SSH tunneling if nftables is unavailable:# Automatically uses SSH tunnel if nftables fails
host_port = vm.expose_local(guest_port=5432)
# Background SSH process forwards the port
Advantages:
- Works without privileged network access
- Cross-platform compatible
- Automatic cleanup on VM stop
You don’t need to choose the transport method manually. SmolVM tries nftables first and falls back to SSH tunnels automatically.
Multiple Port Forwards
Expose multiple services from the same VM:
with SmolVM() as vm:
# Start multiple services
vm.run("python3 -m http.server 8000 &")
vm.run("python3 -m http.server 9000 &")
# Expose both ports
port1 = vm.expose_local(guest_port=8000, host_port=8000)
port2 = vm.expose_local(guest_port=9000, host_port=9000)
print(f"Service 1: http://127.0.0.1:{port1}/")
print(f"Service 2: http://127.0.0.1:{port2}/")
Removing Port Forwards
Manually remove a port forward before VM shutdown:
with SmolVM() as vm:
# Expose a port
host_port = vm.expose_local(guest_port=8080, host_port=8080)
print(f"Exposed on localhost:{host_port}")
# Do work...
# ...
# Remove the forward
vm.unexpose_local(host_port=8080, guest_port=8080)
print("Port forward removed")
Method Signature
def unexpose_local(self, host_port: int, guest_port: int) -> SmolVM:
"""Remove a previously configured localhost-only port forward."""
Port forwards are automatically cleaned up when the VM stops or the context manager exits. Manual cleanup is only needed for long-running VMs.
Real-world Example: OpenClaw Dashboard
From the OpenClaw integration example:
from smolvm import SmolVM, VMConfig
from smolvm.build import ImageBuilder, SSH_BOOT_ARGS
GUEST_DASHBOARD_PORT = 18789
HOST_DASHBOARD_PORT = 18789
builder = ImageBuilder()
kernel, rootfs = builder.build_debian_ssh_key(
ssh_public_key=public_key,
rootfs_size_mb=4096,
)
config = VMConfig(
vcpu_count=1,
mem_size_mib=2048,
kernel_path=kernel,
rootfs_path=rootfs,
boot_args=SSH_BOOT_ARGS,
)
with SmolVM(config) as vm:
# Install and start OpenClaw gateway inside VM
vm.run(f"openclaw gateway --port {GUEST_DASHBOARD_PORT} &")
# Expose the dashboard to host
host_port = vm.expose_local(
guest_port=GUEST_DASHBOARD_PORT,
host_port=HOST_DASHBOARD_PORT,
)
print(f"Dashboard available: http://127.0.0.1:{host_port}/")
input("Press Enter to stop...")
Security Considerations
Localhost-only Binding
expose_local() only binds to 127.0.0.1 (localhost), NOT to 0.0.0.0. Services are NOT exposed to your network.
# This is secure - only accessible from your machine
host_port = vm.expose_local(guest_port=8080)
# Binds to 127.0.0.1:8080, not 0.0.0.0:8080
If you need to expose to your network, you must set up additional forwarding outside of SmolVM.
Port Conflicts
expose_local() handles port conflicts gracefully:
# If port 8080 is already in use, SmolVM tries a fallback port
host_port = vm.expose_local(guest_port=8080, host_port=8080)
if host_port != 8080:
print(f"Port 8080 unavailable, using {host_port} instead")
Troubleshooting
Port Forward Not Reachable
If expose_local() succeeds but the port is not reachable:
Verify the service is running
# Check if the service is listening in the guest
result = vm.run("netstat -tuln | grep 8080")
print(result.output)
Check the service binds to the correct interface
Services must bind to 0.0.0.0 or the guest IP, NOT 127.0.0.1 only:# Good - binds to all interfaces
vm.run("python3 -m http.server 8000 --bind 0.0.0.0 &")
# Bad - only accessible within the guest
vm.run("python3 -m http.server 8000 --bind 127.0.0.1 &")
Test connectivity from inside the guest
result = vm.run("curl -v http://127.0.0.1:8000/")
print(result.output)
Permission Errors (nftables)
If you see permission errors related to nftables:
Failed to configure nftables: Permission denied
SmolVM will automatically fall back to SSH tunnels. To enable nftables:
# Run the SmolVM system setup script
sudo ./scripts/system-setup.sh --configure-runtime
Next Steps