Build a 3-Node Vagrant on VirtualBox Lab (with Static Bridged IPs)

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/bash across 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/24 range (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.

  1. Open VirtualBox → Tools → Network → Host-only Networks (VirtualBox 7.x)
    (VirtualBox 6.x: File → Host Network Manager…)
  2. Click Create.
  3. 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)
  4. 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_network in 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/sh defaults to dash (fast, POSIX-y).
  • Some scripts expect Bash features under #!/bin/sh—this flips the system symlink the clean way (via dpkg-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.
  • 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.
  • Provision error on dpkg-reconfigure dash
    • Ensure the VM has internet to reach package metadata, or preseed with the debconf-set-selections line (already included) and rerun vagrant provision.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.