Join a node from a local QEMU VM
Spin up a throwaway Linux VM on your laptop with QEMU and attach it to your control plane as a Kubernetes worker — no spare hardware, no cloud bill.
Want to kick the tires without dedicated hardware or a cloud instance? Run a throwaway Linux VM on your own machine with QEMU and attach it as a worker. It’s the same outbound-only join as any other node — this guide gives you the exact VM to run it in.
This is the hands-on companion to Join a node, which covers the join itself in more depth. If you already have a Linux box, use that guide instead — you don’t need a VM.
You’ll need: a Mac (Apple Silicon) or a Linux workstation, QEMU, about 4 GB of free RAM for the VM, and a control plane you can mint a join token for. The VM needs only outbound internet — the same as any node.
The examples target Apple Silicon (arm64); notes call out where an Intel/Linux host differs. A node’s architecture is independent of your control plane — an arm64 worker joins an amd64-managed cluster fine.
1. Install QEMU
brew install qemu # macOS
# sudo apt install qemu-system-arm ovmf # Debian/Ubuntu host
2. Get an Ubuntu cloud image
Cloud images ship with cloud-init, which we use to drop in an SSH key on first
boot. Grab the current Ubuntu LTS for your VM’s architecture:
mkdir -p ~/flotilla-vm && cd ~/flotilla-vm
curl -fSLO https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-arm64.img
Make a resized overlay so the base image stays pristine and the VM has room for container images:
qemu-img create -f qcow2 -F qcow2 -b noble-server-cloudimg-arm64.img node.qcow2 20G
3. Seed cloud-init and boot
Generate a key and a tiny cloud-init seed that authorizes it for the default
ubuntu user:
ssh-keygen -t ed25519 -N '' -f id_ed25519
mkdir seed
cat > seed/meta-data <<'EOF'
instance-id: flotilla-node-1
local-hostname: flotilla-node-1
EOF
cat > seed/user-data <<EOF
#cloud-config
users:
- name: ubuntu
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- $(cat id_ed25519.pub)
EOF
# The seed must be an ISO labelled CIDATA (macOS: hdiutil; Linux: cloud-localds).
hdiutil makehybrid -iso -joliet -default-volume-name CIDATA -o seed.iso seed
# Linux: cloud-localds seed.iso seed/user-data seed/meta-data
Boot it, daemonized, forwarding host port 2222 to the VM’s SSH:
cp /opt/homebrew/share/qemu/edk2-arm-vars.fd efi-vars.fd
qemu-system-aarch64 \
-machine virt,accel=hvf,gic-version=3 -cpu host -smp 2 -m 6144 \
-drive if=pflash,format=raw,readonly=on,file=/opt/homebrew/share/qemu/edk2-aarch64-code.fd \
-drive if=pflash,format=raw,file=efi-vars.fd \
-drive if=virtio,format=qcow2,file=node.qcow2 \
-drive if=virtio,format=raw,file=seed.iso \
-netdev user,id=net0,hostfwd=tcp::2222-:22 -device virtio-net-pci,netdev=net0 \
-display none -serial file:serial.log -pidfile qemu.pid -daemonize
Intel Mac / Linux host: swap
qemu-system-aarch64forqemu-system-x86_64, useaccel=kvmon Linux (accel=hvfon Intel macOS), dropgic-version=3, and point the firmware drives at your host’s OVMF (/usr/share/OVMF/OVMF_CODE.fd). Use theamd64cloud image to match.
Give cloud-init a minute, then SSH in:
ssh -i id_ed25519 -p 2222 ubuntu@127.0.0.1
4. Get the join command
From your workstation (not the VM), mint a join and print the one-liner:
lbr cp node attach test
Or copy it from the Console — the cluster’s Attach a node card shows the same command. Either gives you:
curl -fsSL https://api.eu-hel.cp.lngbrdg.com/install.sh \
| sudo bash -s -- --token <token> --k8s-version 1.36
5. Join from inside the VM
Paste that one-liner into the VM’s SSH session and run it as root. The installer pulls the prerequisites (containerd, kubeadm/kubelet matched to your cluster’s Kubernetes version, and the outbound fabric tunnel), brings the tunnel up, and joins:
# inside the VM
curl -fsSL https://api.eu-hel.cp.lngbrdg.com/install.sh \
| sudo bash -s -- --token <token> --k8s-version 1.36
It ends with node 'flotilla-node-1' is joining. The control plane installs the
CNI for you and approves the node.
6. Verify
From your workstation:
lbr cp node list test # or: kubectl get nodes
flotilla-node-1 shows up and goes Ready once the CNI settles. Schedule
something to prove it out:
kubectl run hello --image nginx --restart=Never
kubectl get pod hello -o wide # lands on flotilla-node-1
Tear down
kill "$(cat ~/flotilla-vm/qemu.pid)" # stop the VM
lbr cp node remove test flotilla-node-1 # drop it from the cluster
Because the VM is a throwaway overlay, deleting node.qcow2 and re-running step 3
gives you a clean node every time.
Notes
- Outbound-only. The VM’s user-mode NIC gives it internet with no inbound ports; the node reaches the apiserver by dialing out to the shard’s fabric gateway. No host port-forwarding beyond SSH is needed for Kubernetes.
- Re-joining. A node that already joined won’t re-join in place — the
installer refuses if
/etc/kubernetes/kubelet.confexists. Reset the overlay (step 3) for a fresh node. - Resources. 2 vCPU / 4–6 GB is comfortable for a worker running a few pods. Drop to 2 GB only for the smallest tests.