A clean, reproducible VirtualBox lab is the fastest way to learn Kubernetes the right way—by breaking things safely. In this tutorial, we’ll create a 3-node k3s cluster lab using Vagrant on VirtualBox, with:
- Static bridged IPs on your real LAN (e.g.,
192.168.0.101–103) - Host-only network for a quiet management backchannel (
192.168.230.0/24) - An optional provision step to switch
/bin/sh→/bin/bashacross the nodes
I’ll show you the full Vagrantfile and explain every important line, plus a short VirtualBox GUI guide to create the host-only subnet.
Why this setup?
- Bridged (static) IPs let your laptop and other devices reach the VMs directly—great for testing Ingress, DNS, and real-world networking.
- Host-only IPs give you a separate management plane, even if the bridged interface loses connectivity.
- generic/ubuntu2204 is a lightweight, reliable base box widely used in homelabs.
Prerequisites
- VirtualBox 6.x/7.x installed
- Vagrant 2.4+
- A LAN in the
192.168.0.0/24range (adjust the addresses below if yours differs) - (Windows) The bridged adapter name you’ll use (e.g.,
Intel(R) Ethernet Connection (17) I219-LM)
Step 1 — Create the host-only subnet in VirtualBox
We’ll use 192.168.230.0/24 for host-only. This keeps cluster control traffic off your real LAN.
- Open VirtualBox → Tools → Network → Host-only Networks (VirtualBox 7.x)
(VirtualBox 6.x: File → Host Network Manager…) - Click Create.
- Edit the new network:
- IPv4 Address:
192.168.230.1 - IPv4 Mask:
255.255.255.0 - Disable DHCP (we’ll set static IPs in Vagrant)
- IPv4 Address:
- Note the adapter’s exact name, e.g. VirtualBox Host-Only Ethernet Adapter #2.
Step 2 — Full Vagrantfile (copy/paste)
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
# Base box: Ubuntu 22.04 (generic/ubuntu2204 is small & stable)
config.vm.box = "generic/ubuntu2204"
# Global provider tweaks shared by all VMs
config.vm.provider "virtualbox" do |vb|
vb.gui = false
end
# --- Cluster definition ---
nodes = [
{
name: "k3s-master", hostname: "k3s-master",
public_ip: "192.168.0.101", # LAN (bridged)
private_ip: "192.168.230.10", # host-only
mem: 3072, cpus: 2
},
{
name: "k3s-worker-1", hostname: "k3s-worker-1",
public_ip: "192.168.0.102",
private_ip: "192.168.230.11",
mem: 2048, cpus: 2
},
{
name: "k3s-worker-2", hostname: "k3s-worker-2",
public_ip: "192.168.0.103",
private_ip: "192.168.230.12",
mem: 2048, cpus: 2
},
]
nodes.each do |n|
config.vm.define n[:name] do |node|
node.vm.hostname = n[:hostname]
# ---- BRIDGED (public) interface with static IP on your real LAN ----
# Adjust 'bridge:' to match your host NIC name shown by VirtualBox.
node.vm.network "public_network",
bridge: "Intel(R) Ethernet Connection (17) I219-LM",
ip: n[:public_ip],
netmask: "255.255.255.0",
gateway: "192.168.0.1"
# ---- HOST-ONLY interface with static IP (quiet backchannel) ----
node.vm.network "private_network",
type: "static",
ip: n[:private_ip],
name: "VirtualBox Host-Only Ethernet Adapter #2"
# ---- VirtualBox machine-level settings ----
node.vm.provider "virtualbox" do |vb|
vb.name = n[:name]
vb.memory = n[:mem]
vb.cpus = n[:cpus]
vb.customize ["modifyvm", :id, "--paravirtprovider", "kvm"]
vb.customize ["modifyvm", :id, "--ioapic", "on"]
end
# Optional but handy in some environments:
# node.vm.synced_folder ".", "/vagrant", disabled: true
# ---- OPTIONAL: switch /bin/sh to /bin/bash (non-interactive) ----
node.vm.provision "shell", inline: <<-'SHELL'
set -e
echo "dash dash/sh boolean false" | sudo debconf-set-selections
sudo DEBIAN_FRONTEND=noninteractive dpkg-reconfigure dash
ls -l /bin/sh
SHELL
end
end
end
Step 3 — Understand the Vagrantfile (line-by-line)
Box & provider
config.vm.box = "generic/ubuntu2204"
Uses a lean Ubuntu 22.04 base. Swap it for another if you like (e.g., bento/ubuntu-22.04).
config.vm.provider "virtualbox" do |vb|
vb.gui = false
end
Disables the VM window. You’ll SSH via vagrant ssh.
Node inventory
nodes = [ { ... }, { ... }, { ... } ]
A Ruby array with per-node settings so we can loop and avoid repeat code.
Each node has:
name/hostname: VM name in VirtualBox vs. OS hostname inside the guest.public_ip: static bridged IP on your LAN (reachable by other devices).private_ip: static host-only IP for a quiet backchannel.mem/cpus: adjust per node based on your host’s resources.
Bridged interface (public_network)
node.vm.network "public_network",
bridge: "Intel(R) Ethernet Connection (17) I219-LM",
ip: n[:public_ip],
netmask: "255.255.255.0",
gateway: "192.168.0.1"
public_networkin bridged mode attaches the VM NIC directly to your host’s physical adapter.bridge:must match the adapter name VirtualBox shows (copy the exact string).- Static IP: we assign the address so your DHCP server doesn’t have to.
- Gateway: usually your router; change if yours isn’t
192.168.0.1.
Tip: Make sure the IPs (.101–.103) are outside your DHCP pool to avoid conflicts.
Host-only interface (private_network)
node.vm.network "private_network",
type: "static",
ip: n[:private_ip],
name: "VirtualBox Host-Only Ethernet Adapter #2"
- Adds a second NIC on a host-only switch.
name:has to match the exact VirtualBox host-only adapter you created.- No internet here—perfect for internal cluster traffic, rsync, etc.
VirtualBox tuning
vb.customize ["modifyvm", :id, "--paravirtprovider", "kvm"]
vb.customize ["modifyvm", :id, "--ioapic", "on"]
KVM paravirtualization + IOAPIC generally yield smoother Kubernetes workloads.
Optional: switch /bin/sh to Bash
node.vm.provision "shell", inline: <<-'SHELL'
set -e
echo "dash dash/sh boolean false" | sudo debconf-set-selections
sudo DEBIAN_FRONTEND=noninteractive dpkg-reconfigure dash
ls -l /bin/sh
SHELL
- Ubuntu’s
/bin/shdefaults to dash (fast, POSIX-y). - Some scripts expect Bash features under
#!/bin/sh—this flips the system symlink the clean way (viadpkg-reconfigure). - Caution: If you run third-party scripts written strictly for POSIX
sh, they might behave differently under Bash.
Step 4 — Bring the cluster up
vagrant up
Wait for the three VMs to boot and provision. To access a node:
vagrant ssh k3s-master
# or:
vagrant ssh k3s-worker-1
vagrant ssh k3s-worker-2
Firewall notes: If you can’t ping the bridged IPs from another device on your LAN, check your host firewall and any VLAN/isolation rules on your router/switch.
Step 5 — (Optional) Next steps: prep for k3s
Before installing k3s, it’s smart to:
- Disable swap
- Ensure br_netfilter & iptables settings are correct
- Align containerd cgroups
You can add a shell provisioner that does those, or use Ansible for idempotent setup.
Troubleshooting
- Bridged IP won’t come up
- Confirm the
bridge:adapter string matches VirtualBox’s UI exactly. - Make sure your static IPs are outside your DHCP pool.
- Some Wi-Fi drivers don’t support bridged mode well—try a wired NIC or different adapter.
- Confirm the
- Host-only adapter name mismatch
- Open VirtualBox Host-only Networks and copy the exact name into
name:. - If “#2” already exists with a different subnet, create another (“#3”) or edit it.
- Open VirtualBox Host-only Networks and copy the exact name into
- Provision error on
dpkg-reconfigure dash- Ensure the VM has internet to reach package metadata, or preseed with the
debconf-set-selectionsline (already included) and rerunvagrant provision.
- Ensure the VM has internet to reach package metadata, or preseed with the
Conclusion
You now have a repeatable 3-node VirtualBox lab with static bridged IPs (realistic networking) and a host-only backchannel (stable management). This mirrors on-prem environments closely and is an ideal base for installing k3s, Ingress controllers, registries, and CI/CD.
One comment