← Learn··Updated 31 May 2026·8 min read

Set up a k3s cluster

Hands-on: install a k3s server with the get.k3s.io script, join agent nodes with a token, pull the kubeconfig onto your laptop, and run kubectl get nodes against a real cluster.

Kubernetes
Kubernetes from scratch with k3sPart 3 of 9
#kubernetes
#k3s
#installation
#ai-assisted

level:Intermediate verified:Jun 2026

ℹ️ Hands-on — this part builds the cluster the rest of the series uses. You'll run these commands on a real (or virtual) Linux machine.

The last part named every component of a cluster — the control plane, the worker nodes, the way requests flow through the API server. This part makes one. By the end you will have a running k3s cluster, your laptop talking to it, and kubectl get nodes returning machines in Ready state. The genuinely surprising thing is how short the path is: the famous "Kubernetes in one command" reputation is real, and it comes from k3s collapsing the whole control plane into a single binary you install with one script.

What you need first

You need at least one Linux machine — a VM, a mini PC, an old laptop. One is enough to learn on; the multi-node steps below are optional. The machine should have a static IP or a stable hostname on your network, because agents and your laptop will both address the server by it. A fresh Debian or Ubuntu install of the kind from installing a Debian server is an ideal starting point.

ℹ️ Note A single node is a complete, production-capable cluster — not just a learning toy. A one-machine k3s install runs the full control plane and the worker components on the same box, and every concept in this series — pods, deployments, services, ingress, storage — works identically on one node. You add agents later only when you want more capacity or high availability, never to make the cluster "work". Plenty of real homelabs and small production setups are single-node for life.

Installing the k3s server

The control-plane machine is the server in k3s terms. The project ships an install script at https://get.k3s.io that downloads the binary, registers it as a systemd service, and starts it:

# On the control-plane machine
curl -sfL https://get.k3s.io | sh -

That one line gives you a complete, conformant Kubernetes cluster. The script installs k3s, the kubectl, crictl, and other utilities, and writes a kubeconfig to /etc/rancher/k3s/k3s.yaml. Because no other datastore is configured, k3s uses its default embedded SQLite datastore — no etcd cluster to stand up. Check it started:

sudo systemctl status k3s
sudo k3s kubectl get nodes

You should see one node in Ready state. That is a working cluster.

It is worth being clear about what just happened, because "one command" hides real machinery. The script detected your init system, dropped the k3s binary at /usr/local/bin/k3s, wrote a systemd unit, and started the process. That single process is now running the entire control plane — API server, scheduler, controller-manager — plus the worker components, all in one. The kubelet inside it registered the machine as a node against its own API server, which is why get nodes returns immediately. From Kubernetes' point of view this is a genuine cluster; it simply happens to live in one process on one box.

The +k3s1 suffix you will see on the version string is a reminder that this is upstream Kubernetes with k3s's packaging, not a fork or a cut-down reimplementation. The API your kubectl talks to is the real Kubernetes API, which is the whole point — everything you learn here transfers directly to a full-size cluster.

💡 Tip A default k3s server is not bare. It bundles the containerd runtime, the Flannel CNI, a Traefik ingress controller, the Klipper service load-balancer (ServiceLB), and local-path storage. So out of the box you already have working networking, an ingress controller (covered in ingress with Traefik), a way to expose services, and a default storage class — things a from-scratch Kubernetes install makes you assemble yourself.
flowchart TD
  A["curl get.k3s.io | sh -"] -->|"on machine 1"| B["k3s server<br/><i>control plane + SQLite</i>"]
  B -->|"generates"| C["node-token<br/><i>/var/lib/rancher/...</i>"]
  C -->|"K3S_TOKEN"| D["k3s agent install<br/><i>on machine 2, 3</i>"]
  D -->|"register :6443"| B
  B -->|"k3s.yaml"| E["kubectl<br/><i>your laptop</i>"]

The install flow end to end: stand up the server, read its token, use the token to join agents, then copy the kubeconfig to your laptop. Agents register to the server over its API on port 6443.

Joining agent nodes

If you have more than one machine, the others join as agents — worker nodes that run containers but hold no control-plane components. Two things are needed: the server's address and its join token.

The token is generated during the server install. The value to use for K3S_TOKEN is stored at /var/lib/rancher/k3s/server/node-token on the server node:

# On the server — read the join token
sudo cat /var/lib/rancher/k3s/server/node-token

You should see a single long token string (something like K10...::server:...) — copy the whole line.

Then run the same install script on each additional machine, but this time pass the server URL and token as environment variables. Setting K3S_URL causes the installer to configure k3s as an agent rather than a server, pointing at the server's API on port 6443:

# On each agent machine
curl -sfL https://get.k3s.io | \
  K3S_URL=https://<server-ip>:6443 \
  K3S_TOKEN=<token-from-above> sh -

The presence of K3S_URL is the whole switch: the same binary becomes a server or an agent purely based on whether you hand it a server to join. Under the hood the agent opens a websocket connection back to the server's API on port 6443, authenticates with the token, and from then on the kubelet on that machine takes its instructions from the control plane on the server. Back on the server, sudo k3s kubectl get nodes will now show the agents registering and turning Ready — usually within a few seconds.

If an agent never shows up, the cause is almost always one of three things: the agent cannot reach the server on 6443 (a firewall between them), the token was copied wrong, or the server IP in K3S_URL is unreachable from the agent. Each agent install logs to its own systemd unit, so sudo journalctl -u k3s-agent -f on the agent is where you look first — agents run as the k3s-agent service, servers as k3s.

flowchart LR
  subgraph S["Single-node — learning"]
    A["k3s server<br/><i>control plane + workloads</i>"]
  end
  subgraph M["Multi-node — capacity / HA"]
    B["k3s server"] --> C["agent 1"]
    B --> D["agent 2"]
  end
  S -->|"add machines later"| M

Start with one node that does everything. Add agents when you need more room or want workloads spread across machines — the cluster you built does not change, it just grows.

Getting kubectl onto your laptop

Running k3s kubectl over SSH works, but you want kubectl on your own machine. First, install kubectl itself — it is a single binary available for Linux, macOS, and Windows.

Then you need the cluster's credentials. k3s writes a kubeconfig to /etc/rancher/k3s/k3s.yaml on the server. That file holds the API endpoint, the cluster CA, and a client certificate. Copy it to your laptop and point kubectl at it:

# On your laptop — copy the kubeconfig from the server
scp user@<server-ip>:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s.yaml

# The file points at 127.0.0.1 by default — edit the server line
# to your server's real IP, then:
export KUBECONFIG=~/.kube/k3s.yaml
kubectl get nodes

The one gotcha: the kubeconfig k3s generates lists the server as 127.0.0.1, which only works on the server itself. Edit the server: line to https://<server-ip>:6443 before using it from your laptop. After that, kubectl get nodes from your own machine returns the same cluster — you are now driving it remotely.

$ kubectl get nodes
NAME       STATUS   ROLES                  AGE   VERSION
server-1   Ready    control-plane,master   4m    v1.x.x+k3s1
agent-1    Ready    <none>                 2m    v1.x.x+k3s1
agent-2    Ready    <none>                 2m    v1.x.x+k3s1

That output is the payoff: the control plane and worker roles from the previous part, now real machines you can kubectl apply against.

⚠️ Warning The Kubernetes API on port 6443 is the keys to the entire cluster — that kubeconfig is an admin credential. Do not expose 6443 to the public internet. Keep the API reachable only on your LAN or over a VPN such as Tailscale or WireGuard, and treat k3s.yaml like a private SSH key: never commit it, never paste it into a chat. Anyone with that file controls every workload you run.
Danger Piping a remote script straight into a shell with curl ... | sh runs arbitrary code as root before you have read a line of it. The k3s installer is reputable and widely used, but the habit is dangerous in general. On a host that holds real data, read the script first or pin to a checksum-verified release.

Useful flags at install time

You pass installer flags after sh -s -. The most common is disabling bundled components you intend to replace — for example bringing your own ingress controller instead of the bundled Traefik:

# Install the server without Traefik
curl -sfL https://get.k3s.io | sh -s - --disable traefik

For most of this series you want the defaults: Traefik, ServiceLB, and local-path are exactly the pieces the later parts build on. Reach for --disable only when you have a concrete reason.

A first look around the cluster

Now that kubectl talks to the cluster from your laptop, it is worth spending two minutes confirming the parts from the previous part are really there. A fresh k3s cluster is not empty — the bundled components run as actual workloads inside it, in a namespace called kube-system:

# Everything k3s started for you lives here
kubectl get pods -n kube-system

You will see Traefik, the local-path provisioner, CoreDNS (cluster DNS), and the metrics server, all running as pods. This is a useful early lesson: in Kubernetes, even the cluster's own infrastructure runs as Kubernetes workloads. There is no special hidden layer — Traefik is just a pod, scheduled and supervised the same way your applications will be.

A couple more orientation commands are worth knowing from the start:

# What can this cluster do? Every resource type it knows about
kubectl api-resources | head

# Detail on a node — capacity, conditions, what's scheduled on it
kubectl describe node <node-name>

You should see a long table of resource types (pods, services, deployments, configmaps, and dozens more) from api-resources, and a multi-section report for the named node from describe node. The describe node output is where the abstract pieces become concrete: you can see the node's allocatable CPU and memory, its Ready condition, and the list of pods the scheduler has placed on it. When you start deploying your own workloads in the coming parts, this is the view that tells you where they actually landed.

💡 Tip To take a node back out, k3s installs uninstall scripts: /usr/local/bin/k3s-uninstall.sh on a server and /usr/local/bin/k3s-agent-uninstall.sh on an agent. Because the install is one binary and a systemd unit, teardown is genuinely clean — handy while you are experimenting and want to start fresh.

A short close

A real Kubernetes cluster is now one curl away: the get.k3s.io script stands up a server with an embedded datastore and a working set of defaults, the server's node-token plus K3S_URL joins as many agents as you like, and a copied-and-edited k3s.yaml puts the whole thing under your laptop's kubectl. One node is a complete cluster — plenty to learn on and plenty to run real workloads on; more nodes add capacity and availability, not basic function.

You have a cluster and a prompt that answers. The next part puts the first workload on it — pods, the smallest thing Kubernetes will run. If you want to re-read what the server and agents are doing under the hood, the control plane and nodes is the part before this, and the series hub has the full path. This site runs on k3s in production, so this is the same install I actually operate.