Skip to content

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, container, and direct modes are available when you want the VM to join a network more like a normal machine. Those modes need Docker options that allow the runner to create or attach low-level network devices.

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, container, 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.

Container Mode

Container mode connects the VM NIC to a Docker network that the docker-vm-runner container has already joined. Use it when a Compose service has more than one Docker network and one of those container-visible interfaces should become the VM's data-plane NIC.

Container mode does not take a Docker network name. Pass the interface name as seen inside the runner container, such as eth1.

Required variables:

-e NETWORK_MODE=container
-e NETWORK_CONTAINER_INTERFACE=eth1

The runner creates a Linux bridge inside the container, enslaves eth1 to that bridge, and connects the VM tap device to it. If the interface has IP addresses or routes, the runner moves them to the in-container bridge so the container can keep using that Docker network.

Preview the configuration:

docker run --rm \
  --network my-provisioning-network \
  -e DISTRO=alpine-3.22-cloud-amd64 \
  -e NETWORK_MODE=container \
  -e NETWORK_CONTAINER_INTERFACE=eth0 \
  ghcr.io/munenick/docker-vm-runner:latest --show-config

To boot with container mode, allow the runner to create a bridge and tap device inside the container:

docker run -d --name docker-vm-runner-container-net \
  --network my-provisioning-network \
  --cap-add NET_ADMIN \
  --device /dev/kvm \
  --device /dev/net/tun \
  --device /dev/vhost-net \
  -e NETWORK_MODE=container \
  -e NETWORK_CONTAINER_INTERFACE=eth0 \
  -e NO_CONSOLE=1 \
  -v docker-vm-runner-data:/data \
  ghcr.io/munenick/docker-vm-runner:latest

For Compose files with multiple Docker networks, set NETWORK_CONTAINER_INTERFACE to the interface that belongs to the network you want the VM NIC to join. Use docker exec <container> ip -br link to inspect the interface names.

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_CONTAINER_INTERFACE Select the container-visible interface for container mode.
NETWORK_CONTAINER_BRIDGE Override the generated in-container bridge name for container mode.
NETWORK_BOOT Mark the NIC as bootable for network boot flows.

See Reference for the complete variable list.