Skip to main content

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:
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
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:
1

Verify the service is running

# Check if the service is listening in the guest
result = vm.run("netstat -tuln | grep 8080")
print(result.output)
2

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 &")
3

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

Last modified on March 3, 2026