Networking¶
The default network is NAT. It works well for the same workflow as docker run: start a VM, publish the ports you need, and keep the host network unchanged.
Bridge and direct modes are available when you want the VM to join a network more like a normal machine. Those modes depend on host networking, so their Docker options are different from the default NAT examples.
Default NAT¶
NAT uses libvirt user-mode networking. The VM gets an internal address, and selected ports are forwarded to the container.
Publish the default SSH port:
docker run --rm -it \
--device /dev/kvm \
-p 2222:2222 \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
Connect with:
ssh user@localhost -p 2222
The default login is user with password password.
Additional Ports¶
Use PORT_FWD to forward another container port to a VM port:
docker run --rm \
-e DISTRO=alpine-3.22-cloud-amd64 \
-e PORT_FWD=8080:80 \
ghcr.io/munenick/docker-vm-runner:latest --show-config
The format is container_port:guest_port. When you boot the VM, publish the same container port with Docker:
docker run --rm -it \
--device /dev/kvm \
-p 8080:8080 \
-e PORT_FWD=8080:80 \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
Use comma-separated values for multiple forwards:
-e PORT_FWD=8080:80,8443:443
PORT_FWD applies to NAT NICs only. If every NIC is bridge or direct, there is no user-mode NAT device to own the forwarded port.
Multiple NICs¶
The first NIC uses unnumbered variables. Additional NICs use NETWORK2_..., NETWORK3_..., and so on.
Keep NAT as the management path and add a second bridged NIC:
docker run --rm \
-e DISTRO=alpine-3.22-cloud-amd64 \
-e NETWORK_MODE=nat \
-e NETWORK2_MODE=bridge \
-e NETWORK2_BRIDGE=docker0 \
ghcr.io/munenick/docker-vm-runner:latest --show-config
This pattern keeps SSH_PORT and PORT_FWD available on the NAT NIC while the second NIC joins the bridge.
Bridge Mode¶
Bridge mode connects the VM NIC to an existing Linux bridge such as br0. Use it when the VM should appear on the bridged network instead of only behind NAT.
Example: use Bridge when your host already has br0 on the LAN and you want the VM to receive an address from that LAN.
Required variables:
-e NETWORK_MODE=bridge
-e NETWORK_BRIDGE=br0
Preview the configuration:
docker run --rm \
-e DISTRO=alpine-3.22-cloud-amd64 \
-e NETWORK_MODE=bridge \
-e NETWORK_BRIDGE=docker0 \
ghcr.io/munenick/docker-vm-runner:latest --show-config
To boot with bridge mode from Docker, the bridge must be visible to the container and libvirt must be able to create a tap device. One verified headless shape is:
docker run -d --name docker-vm-runner-bridge \
--network host \
--cap-add NET_ADMIN \
--device /dev/kvm \
--device /dev/net/tun \
--device /dev/vhost-net \
-e NETWORK_MODE=bridge \
-e NETWORK_BRIDGE=docker0 \
-e NO_CONSOLE=1 \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
Replace docker0 with the bridge you actually want to use. For a VM on your LAN, that is usually a host bridge such as br0, not Docker's default bridge.
When bridge mode is the only NIC, connect to the VM through the bridged network address. Docker -p, SSH_PORT, and PORT_FWD are not the access path for that NIC.
Direct Mode¶
Direct mode attaches the VM NIC to a host interface without creating a Linux bridge first. Use it when you need the VM to use a specific host NIC path and you understand the host network exposure.
Example: use Direct when the VM needs a real address on the physical network through eth0, but you do not want to create a host bridge.
Required variables:
-e NETWORK_MODE=direct
-e NETWORK_DIRECT_DEV=eth0
Use the real host interface name from ip -br link.
Preview the configuration:
docker run --rm \
-e DISTRO=alpine-3.22-cloud-amd64 \
-e NETWORK_MODE=direct \
-e NETWORK_DIRECT_DEV=eth0 \
ghcr.io/munenick/docker-vm-runner:latest --show-config
To boot with Direct mode from Docker, allow Docker to expose the dynamic macvtap devices created for the VM:
MACVTAP_MAJOR="$(awk '$2 == "macvtap" { print $1 }' /proc/devices)"
if [ -z "$MACVTAP_MAJOR" ]; then
echo "macvtap is not available on this host" >&2
exit 1
fi
docker run -d --name docker-vm-runner-direct \
--network host \
--cap-add NET_ADMIN \
--device /dev/kvm \
--device /dev/vhost-net \
--device-cgroup-rule "c ${MACVTAP_MAJOR}:* rwm" \
-v /dev:/dev:ro \
-e NETWORK_MODE=direct \
-e NETWORK_DIRECT_DEV=eth0 \
-e NO_CONSOLE=1 \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
/dev/vhost-net is used by the default virtio NIC. If you set NETWORK_MODEL=e1000, Direct mode can run without /dev/vhost-net, but the guest NIC model changes.
For day-to-day access, a practical pattern is NAT plus direct:
docker run --rm \
-e DISTRO=alpine-3.22-cloud-amd64 \
-e NETWORK_MODE=nat \
-e NETWORK2_MODE=direct \
-e NETWORK2_DIRECT_DEV=eth0 \
ghcr.io/munenick/docker-vm-runner:latest --show-config
The NAT NIC keeps Docker-style port forwarding available; the direct NIC is available for network tests that need the host interface path.
NIC Details¶
Use these variables when the guest or network requires fixed interface properties:
| Variable | Purpose |
|---|---|
NETWORK_MAC |
Set a stable MAC address. |
NETWORK_MODEL |
Set the NIC model. The default is virtio. |
NETWORK_MTU |
Set the NIC MTU. |
NETWORK_GUEST_IP |
Set guest IPv4 addresses for NAT mode. |
NETWORK_GUEST_IP6 |
Set guest IPv6 addresses for NAT mode. |
NETWORK_BOOT |
Mark the NIC as bootable for network boot flows. |
See Reference for the complete variable list.