The Dell.
a second node, and the four things Fedora needs first.
Adding a second machine to a one-node cluster. The join itself is one command. Fedora needs four things done first, and skipping any of them fails in a way that does not look like the cause.
Why a second node changes anything
A one-node cluster is a real cluster in the same way a one-person team is a real team. Everything technically works, and none of the interesting problems have shown up yet.
The second node is where scheduling stops being theoretical. Pods start landing somewhere other than where you are typing. Networking has to cross a wire. Storage stops being a detail you can ignore, because a local volume on one machine is invisible to the other.
The machine going in is a Dell Latitude with 32 GB of RAM running Fedora, joining a server that has 16 GB. So the Dell is not just another node, it is the node with the memory, and things that were cramped on the server have somewhere to go.
Prep one: kill swap, including the one you did not know about
Kubernetes wants swap off. The kubelet makes memory decisions based on real memory, and swap makes those numbers lie.
On Fedora this is not the usual fstab edit. Fedora ships zram, a compressed block device in RAM, and it does not appear in fstab at all. You can comment out every swap line you find and still have swap running.
# what is actually providing swap swapon --show # -> /dev/zram0 partition 8G sudo dnf remove -y zram-generator-defaults sudo swapoff -a # confirm nothing comes back after a reboot sudo systemctl reboot swapon --show # -> no output
If swapon —show also lists a real swapfile, deal with that separately in fstab. A broken fstab can stop a machine from booting, so verify it parses before you reboot.
grep -i swap /etc/fstab # -> /var/swap/swapfile swap swap sw 0 0 sudo sed -i '/ swap / s/^/#/' /etc/fstab sudo findmnt --verify sudo systemctl daemon-reload
Prep two: firewalld and the two CIDRs
This is the one that fails in a way that does not look like the cause. Get it wrong and the node joins fine, shows Ready, and then pods on it cannot talk to pods anywhere else. It presents as a DNS problem. It is not a DNS problem.
Two networks need to be trusted, not two ports:
- 10.42.0.0/16 is the pod network. Every pod in the cluster gets an address here.
- 10.43.0.0/16 is the service network. ClusterIPs live here, which is what a Service name resolves to.
Then two ports on top: 10250/tcp for the kubelet, so the control plane can reach logs and exec, and 8472/udp for the flannel VXLAN tunnel that actually carries pod traffic between machines.
# pod and service CIDRs, trusted wholesale sudo firewall-cmd --permanent --zone=trusted --add-source=10.42.0.0/16 sudo firewall-cmd --permanent --zone=trusted --add-source=10.43.0.0/16 # kubelet and flannel vxlan sudo firewall-cmd --permanent --add-port=10250/tcp sudo firewall-cmd --permanent --add-port=8472/udp sudo firewall-cmd --reload sudo firewall-cmd --list-all
Miss 8472 specifically and you get the worst version of this: the node joins, pods schedule, everything looks healthy, and cross-node traffic silently goes nowhere.
Prep three: SELinux
Fedora runs SELinux in enforcing mode. k3s needs a policy that lets containerd do what it needs to do, and the install script handles this on its own.
You do not need to do anything here in advance. It is worth knowing it happened, because the installer will pull in k3s-selinux from the Rancher repo mid-install and ask to import a GPG key. That is expected, not a prompt to be suspicious of.
Do not turn SELinux off to make a problem go away. If something is denied, the denial is in the audit log and it will tell you what it wanted.
Prep four: stop the laptop sleeping
The node is a laptop. Laptops sleep when you close them, and a sleeping node is a node the cluster loses.
sudo mkdir -p /etc/systemd/logind.conf.d sudo vim /etc/systemd/logind.conf.d/99-lid.conf
Put this in it:
[Login] HandleLidSwitch=ignore HandleLidSwitchExternalPower=ignore HandleLidSwitchDocked=ignore
Then apply it and mask the sleep targets as a backstop:
sudo systemctl restart systemd-logind sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target # see who is holding the lid switch systemd-inhibit --list
Check that last command’s output. If a desktop environment appears in the list holding handle-lid-switch, it has taken the lid switch away from logind and your config file is being ignored. The desktop’s own power settings have to be changed too, and on a node the cleaner answer is to stop booting into a desktop at all.
The join, which is one command
Get the token and the version from the server:
# on the server sudo cat /var/lib/rancher/k3s/server/node-token # -> K10<long string> k3s --version # -> k3s version v1.36.2+k3s1
Before installing anything, confirm the Dell can actually reach the API:
# from the Dell, before installing anything
curl -k https://192.168.1.50:6443
# -> {"kind":"Status", ... "reason":"Unauthorized","code":401}A 401 Unauthorized is the good answer. It means the API server is there and talking. A timeout means the network path is wrong and nothing after this will work.
Then the join itself:
curl -sfL https://get.k3s.io | \ K3S_URL=https://192.168.1.50:6443 \ K3S_TOKEN=K10<your-token> sh -
Setting K3S_URL is what makes this an agent rather than a second server. Leave it out and you get a second control plane, which is a different post and a different set of problems.
sudo systemctl status k3s-agent --no-pager # -> Active: active (running) # -> k3s agent is up and running
kubectl does not work on the agent. Agents get no kubeconfig, so it tries localhost:8080 and gets connection refused. That is correct behaviour, not a broken install. Run kubectl on the server.# on the server, not the agent kubectl get nodes -o wide
The agent may install a slightly newer patch version than the server. That is fine in this direction. Agents can be newer than the control plane, never older.
Labels by capability, not hardware
A label is a tag on a node. A nodeSelector on a pod says “only put me somewhere with this tag.”
The temptation is to label by machine name. Label by what the machine can do instead. If the Dell gets replaced next year, you move one label and every manifest still works. Label it dell and you are editing YAML for an afternoon.
kubectl label node dell-node workload=heavy kubectl label node k3s-server workload=light kubectl get nodes -L workload
The ROLES column showing <none> for a worker is normal. That column reads a different label entirely and is purely cosmetic. Any node without a control-plane taint already runs workloads.
Then prove it actually works, because a selector that matches nothing leaves a pod Pending forever with no obvious complaint:
kubectl run placement-test --image=busybox --restart=Never \
--overrides='{"spec":{"nodeSelector":{"workload":"heavy"}}}' \
-- sleep 60
kubectl get pod placement-test -o wide
# -> NODE column should read dell-node
kubectl delete pod placement-testIf the pod lands on the Dell, scheduling is working, the label is right, and cross-node networking is up. That one test covers all three.
Final checklist: confirm everything works
If every check below passes, this post’s work is done and verified.
# 1. Swap is off and stays off across a reboot
swapon --show
# -> no output
# 2. The firewall trusts the pod and service networks
sudo firewall-cmd --list-all | grep -A2 sources
# 3. The agent is running on the worker
sudo systemctl is-active k3s-agent
# -> active
# 4. The server sees the node as Ready
kubectl get nodes
# -> dell-node Ready <none>
# 5. The label is applied
kubectl get nodes -L workload
# 6. A pod with the nodeSelector actually lands there
kubectl run placement-test --image=busybox --restart=Never \
--overrides='{"spec":{"nodeSelector":{"workload":"heavy"}}}' -- sleep 30
kubectl get pod placement-test -o wide
kubectl delete pod placement-test
# 7. Eyeball check: close the laptop lid, wait five minutes,
# confirm the node is still Ready from the serverTwo nodes, split by capability, with a test that proves placement rather than assuming it. The next thing that gets interesting is what happens when one of them goes away.