Node minimums and control-plane sizing
The k3s requirements page gives a floor for k3s and its packaged components only; your pods come on top. It supports x86_64, armhf and arm64, expects most modern Linux distributions to work, and refuses two nodes with the same hostname.
| Role | Documented minimum | Notes from the same page |
|---|---|---|
| Server (control plane + datastore) | 2 cores, 2 GB RAM | SSD recommended: k3s speed follows the datastore |
| Agent (worker) | 1 core, 512 MB RAM | Size for the workload, not for k3s |
| Server sizing guide | 2 CPU / 4 GB: 0-350 agents | 4 CPU / 8 GB: 351-900 agents |
| Three HA servers | Roughly 50% more agents | Join agents in batches of 50 or fewer |
A cluster of three to ten machines sits at the bottom of that guide's first bracket. What the docs tie k3s speed to is the datastore's disk, and they warn that embedded etcd can struggle on slow disks, so put server nodes on SSD or NVMe. Workers are sized by what you schedule on them. If your images are built for arm64, the Ampere Altra servers are native arm64 hardware that k3s supports.
The network constraint: one public port, no private VLAN
Every one of our dedicated servers has one connected port with a public IPv4, and no private VLAN, second port or cross-connect is sold. Node-to-node traffic in a self-built cluster therefore crosses the public network. The k3s docs list what each node must accept, and from whom:
| Port | Purpose | Allow from |
|---|---|---|
| 6443/tcp | Kubernetes API and k3s supervisor | Cluster nodes, plus the address you run kubectl from |
| 10250/tcp | Kubelet metrics and API | Cluster nodes |
| 2379-2380/tcp | Embedded etcd | Server nodes only |
| 8472/udp | Flannel VXLAN (default backend) | Cluster nodes; never the internet |
| 51820/udp (51821 for IPv6) | Flannel wireguard-native | Cluster nodes |
The docs are blunt about VXLAN: an exposed 8472 opens the cluster network to anyone, so nodes belong behind a firewall that blocks it. The default vxlan backend does not encrypt, while --flannel-backend=wireguard-native encapsulates and encrypts pod traffic between nodes. On a network with no private segment, choose wireguard-native, and check that the WireGuard kernel module loads on every node first, as the docs require.
The k3s requirements page recommends turning ufw off, and lists the rules to keep if you leave it on. On a machine whose only interface is public, keep it on. The docs' ufw rules are 6443 plus the pod and service CIDRs; the rules below add the ports from the docs' inbound-rules table, each narrowed to your own node IPs.
Firewall first, on every node
Apply this before installing anything. Replace the example addresses with your nodes' public IPs and your workstation's address. The pod and service CIDRs are the k3s defaults; if you change them, change these rules too.
SERVERS="203.0.113.10 203.0.113.11 203.0.113.12"
NODES="$SERVERS 203.0.113.20"
ADMIN=198.51.100.7
ufw default deny incoming
ufw limit 22/tcp
for ip in $NODES; do
ufw allow from $ip to any port 6443 proto tcp
ufw allow from $ip to any port 10250 proto tcp
ufw allow from $ip to any port 51820 proto udp
done
for ip in $SERVERS; do
ufw allow from $ip to any port 2379:2380 proto tcp
done
ufw allow from $ADMIN to any port 6443 proto tcp
ufw allow from 10.42.0.0/16 to any # pods
ufw allow from 10.43.0.0/16 to any # services
ufw enable
ufw does not gate ServiceLB host ports or NodePorts: k3s's own iptables rules forward that traffic before ufw sees it. If Traefik should not answer the internet, install with --disable=traefik (or --disable=servicelb), or label only the nodes that should serve it with svccontroller.k3s.cattle.io/enablelb=true.
Then confirm from a machine outside the cluster that 6443, 10250, 2379-2380 and 51820 do not answer it, and that 80, 443 and the NodePort range (30000-32767 by default) answer only on the nodes you intend. Our rack servers and blades also have an out-of-band console, which is the way back in if a rule shuts out SSH.
Installing three servers and the agents
- Give every machine a unique hostname, and order them on a current Ubuntu or another modern distribution from the Linux server range.
- On server 1, write a random token to a root-only file, then run the installer with
--cluster-init. Passing the token in a file keeps it out of shell history, and unlike a--tokenflag, out of the process list.--tls-sanis optional: add a DNS name you will point at the servers. - Copy server 1's
/var/lib/rancher/k3s/server/tokento the other nodes as/root/k3s-token, mode 600. That file holds the secure-format token, which lets a joining node check the cluster's CA before it sends credentials. - Join servers 2 and 3 with
--serverpointing at server 1 and the same flannel backend: flannel options must match on every server. - Join each agent with
K3S_URL. The flannel backend is set on servers only, so agents need no--flannel-backendflag. - Run
kubectl get nodeson a server: three entries with the control-plane and etcd roles, plus your agents.
# server 1
(umask 077; openssl rand -hex 32 > /root/k3s-token)
curl -sfL https://get.k3s.io | K3S_TOKEN_FILE=/root/k3s-token sh -s - server \
--cluster-init --flannel-backend=wireguard-native --tls-san=k3s.example.com
# servers 2 and 3 (token copied from server 1, mode 600)
curl -sfL https://get.k3s.io | K3S_TOKEN_FILE=/root/k3s-token sh -s - server \
--server https://203.0.113.10:6443 \
--flannel-backend=wireguard-native --tls-san=k3s.example.com
# each agent
curl -sfL https://get.k3s.io | K3S_URL=https://203.0.113.10:6443 \
K3S_TOKEN_FILE=/root/k3s-token sh -
The admin kubeconfig lands at /etc/rancher/k3s/k3s.yaml, owned by root at mode 600. It carries system:masters credentials, so a copy on your workstation, with the server field changed to a server's address, is as sensitive as the token. k3s renews the certificates in that file when it starts; copies have to be refreshed by hand.
The token, quorum and snapshots
- Token. The k3s docs say anyone holding the server token effectively has full administrator access. It also encrypts the bootstrap data in the datastore, so a snapshot cannot be restored without it. Store it with your backups.
k3s token rotatereplaces it; every node that joined with the old one must then be restarted with the new one. - Quorum. Embedded etcd wants an odd server count; quorum is (n/2)+1. By that formula three servers keep working with one down, and a fourth adds no tolerance.
- Snapshots. The documented default schedule is
0 */12 * * *(every 12 hours), five kept, in a directory under the k3s data dir on each server. That is the same disk you are protecting, so copy them off the machine or configure the S3 target k3s supports.
Load balancers, hardening and upgrades
The k3s bare metal load balancer question has a built-in answer: ServiceLB, which serves a LoadBalancer Service from host ports. By default it puts the bundled Traefik on ports 80 and 443 of each node, so all node IPs answer web traffic. You can limit ServiceLB to chosen nodes, or disable it and deploy another LoadBalancer controller; extra IPv4 addresses for one come out of the server's own /24.
For hardening, the k3s CIS guide sets protect-kernel-defaults, secrets-encryption, Pod Security admission and audit logging in /etc/rancher/k3s/config.yaml, and k3s leaves host-level changes to you. The requirements page adds one more for flannel clusters: pods with NET_RAW can abuse the bridge network, so untrusted pods need a restricted profile that drops it.
To upgrade, re-run the installer with the same environment and flags, servers first and one at a time, then agents, without skipping a Kubernetes minor version. Drain a node first if a brief API outage would hurt.
Self-built k3s against managed Kubernetes
If the part you want is the workloads rather than the control plane, managed Kubernetes in New York splits the job: we operate an RKE2 control plane, and your dedicated servers are the workers.
| k3s you build | Our managed Kubernetes | |
|---|---|---|
| Control plane | Your servers, your etcd | Ours: Dev (1 node) or HA (3 nodes) |
| Datastore backups | Yours to schedule and copy off | etcd snapshot every 6 hours |
| Where | Any of our five cities | New York only |
| Workers | Any machine you order | 1 to 10 New York servers ready now, Ubuntu 24.04 or 26.04 |
| Node traffic | Your firewall; wireguard-native optional | Per-node firewall admits cluster nodes; not encrypted, Cilium NetworkPolicy |
| LoadBalancer IPs | ServiceLB host ports or your own controller | MetalLB on the extra IPv4 of your workers |
| Root on nodes | Yes | Yes: SSH, console and rescue mode |
Build it yourself when you need another city, encrypted node traffic, k3s itself or full control of the control plane; the dedicated server range lists every line a node can be. Take the managed cluster when New York suits and you would rather not own etcd. Racked machines are live about 30 minutes after an order is approved; builds to order take 4 to 24 hours.
Questions
Can a single k3s server run production workloads?
A single-server install is a complete cluster: datastore, control plane, kubelet and container runtime. It is also a single point of failure for the API. To keep the API up through the loss of one machine, the k3s docs call for three or more servers with embedded etcd, or an external database.
Does k3s need a private network between nodes?
No. It needs the documented ports reachable between nodes, which public addresses provide. Without a private segment, restrict every one of those ports to node IPs and use the wireguard-native flannel backend so pod traffic between machines is encrypted.
Is it safe to put the join token on the install command line?
It works, and the docs' examples do it, but the value then sits in shell history, and a --token flag also shows in the process list. The install script also accepts K3S_TOKEN_FILE, which points at a root-only file instead. Treat the token like a cluster-admin credential.
Should the Kubernetes API on 6443 be reachable from the internet?
Not openly. Agents and servers need it, and so does wherever you run kubectl. Allow those addresses and drop the rest; if your admin address changes often, reach 6443 over SSH or a VPN to a node instead.
Can I mix GPU and CPU machines in one k3s cluster?
k3s does not care what the hardware is, as long as each node meets its minimum and runs a supported architecture. Keep GPU jobs on GPU nodes with your own labels and taints. Our managed clusters work the same way: every worker starts with the label pool=general and you add the rest.