K3s is a lightweight, CNCF-certified Kubernetes distribution packaged as a single binary. It ships with containerd, the Flannel network plugin, CoreDNS, the Traefik ingress controller, a service load balancer and the local-path storage provisioner, so a working cluster takes a couple of minutes to set up. In this tutorial you will install a K3s server on Ubuntu 24.04, use kubectl as a regular user, join a worker node, and publish a test application through the built-in Traefik ingress.
Prerequisites
To follow this tutorial, you will need:
- One server running Ubuntu 24.04 LTS for the control plane, for example a CubePath VPS, with at least 2 vCPU and 2 GB of RAM. K3s itself runs in about 512 MB, but your workloads need room too.
- Optionally, one or more additional Ubuntu 24.04 servers to act as worker (agent) nodes, with at least 1 GB of RAM each.
- A non-root user with
sudoprivileges on every server. - Private or public network connectivity between the nodes. This guide uses
your_server_ipfor the IP of the server node.
Step 1 - Configuring the firewall
K3s nodes talk to each other on a few fixed ports, and pods need to reach the host. If UFW is active, open the Kubernetes API and allow traffic from the default pod (10.42.0.0/16) and service (10.43.0.0/16) networks before installing:
sudo ufw allow OpenSSH
sudo ufw allow 6443/tcp
sudo ufw allow from 10.42.0.0/16 to any
sudo ufw allow from 10.43.0.0/16 to any
If you plan to add worker nodes, also allow the Flannel VXLAN and kubelet ports from the other nodes only. Replace node_ip with the IP of each peer node and repeat for every one:
sudo ufw allow from node_ip to any port 8472 proto udp
sudo ufw allow from node_ip to any port 10250 proto tcp
To serve web traffic through Traefik later, open HTTP and HTTPS and enable the firewall:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Check the rules:
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
6443/tcp ALLOW Anywhere
Anywhere ALLOW 10.42.0.0/16
Anywhere ALLOW 10.43.0.0/16
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
...
TipIf only you need to reach the API, replace
sudo ufw allow 6443/tcpwithsudo ufw allow from your_admin_ip to any port 6443 proto tcp.
Step 2 - Writing the K3s configuration file
K3s reads its options from /etc/rancher/k3s/config.yaml. Creating the file before installation keeps the setup reproducible and avoids long command lines. Create the directory and open the file:
sudo mkdir -p /etc/rancher/k3s
sudo nano /etc/rancher/k3s/config.yaml
Add the following content:
write-kubeconfig-mode: "0600"
tls-san:
- your_server_ip
node-label:
- "environment=production"
write-kubeconfig-modekeeps the admin kubeconfig readable only by root.tls-sanadds your server IP (or a DNS name) to the API server certificate so you can usekubectlremotely.node-labelis an example label; remove it if you do not need it.
Any command-line flag of k3s server can be written here using the flag name without the leading dashes. For example, to skip the bundled Traefik you would add:
disable:
- traefik
Keep Traefik enabled for this tutorial, since Step 6 uses it.
Step 3 - Installing the K3s server
K3s is distributed through an install script that downloads the binary, creates the k3s systemd service and adds kubectl, crictl and ctr symlinks. Download the script first so you can review it:
curl -sfL https://get.k3s.io -o k3s-install.sh
less k3s-install.sh
Run it to install the latest stable release:
sudo sh k3s-install.sh
[INFO] Finding release for channel stable
[INFO] Using v1.xx.x+k3s1 as release
...
[INFO] systemd: Enabling k3s unit
[INFO] systemd: Starting k3s
NoteTo pin a specific Kubernetes minor version, set the channel when running the script, for example
sudo INSTALL_K3S_CHANNEL=v1.33 sh k3s-install.sh.
Verify that the service is running:
sudo systemctl status k3s
● k3s.service - Lightweight Kubernetes
Loaded: loaded (/etc/systemd/system/k3s.service; enabled; preset: enabled)
Active: active (running) since ...
Check that the node is Ready:
sudo k3s kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3s-01 Ready control-plane,master 45s v1.xx.x+k3s1
If the node stays NotReady, read the logs with sudo journalctl -u k3s -f.
Step 4 - Using kubectl as a regular user
K3s writes the admin kubeconfig to /etc/rancher/k3s/k3s.yaml, which only root can read. Copy it to your home directory and point kubectl to it:
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown "$USER":"$USER" ~/.kube/config
chmod 600 ~/.kube/config
echo 'export KUBECONFIG=$HOME/.kube/config' >> ~/.bashrc
source ~/.bashrc
Now run kubectl without sudo:
kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
kube-system helm-install-traefik-crd-xxxxx 0/1 Completed 0 2m
kube-system helm-install-traefik-xxxxx 0/1 Completed 1 2m
kube-system local-path-provisioner-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
kube-system metrics-server-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
kube-system svclb-traefik-xxxxxxxx-xxxxx 2/2 Running 0 90s
kube-system traefik-xxxxxxxxxx-xxxxx 1/1 Running 0 90s
All pods should be Running or Completed.
TipTo manage the cluster from your workstation, copy the same file there and replace
127.0.0.1in theserver:line withyour_server_ip. This works because you added the IP totls-sanin Step 2.
Step 5 - Joining worker nodes
Worker nodes run the K3s agent and authenticate to the server with a join token. On the server, print the token:
sudo cat /var/lib/rancher/k3s/server/node-token
K10a1b2c3...::server:4f5e6d...
On each worker node, download the install script and run it with the server URL and the token. When K3S_URL is set, the script installs the agent as the k3s-agent service instead of a server. Replace your_node_token with the value you just printed:
curl -sfL https://get.k3s.io -o k3s-install.sh
sudo K3S_URL=https://your_server_ip:6443 K3S_TOKEN=your_node_token sh k3s-install.sh
Check the agent on the worker:
sudo systemctl status k3s-agent
Then, on the server, confirm that the new node joined:
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP
k3s-01 Ready control-plane,master 10m v1.xx.x+k3s1 203.0.113.10
k3s-02 Ready <none> 40s v1.xx.x+k3s1 203.0.113.11
ImportantTreat the node token as a secret. Anyone who has it and can reach port 6443 can join a node to your cluster.
Step 6 - Deploying a test application with Traefik
To confirm networking, DNS and ingress work end to end, deploy Nginx, expose it as a Service and route HTTP traffic to it through Traefik.
kubectl create deployment hello --image=nginx:stable --replicas=2
kubectl expose deployment hello --port=80
Create an Ingress manifest:
nano hello-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello
spec:
ingressClassName: traefik
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello
port:
number: 80
Apply it and check the resources:
kubectl apply -f hello-ingress.yaml
kubectl get pods,svc,ingress -l app=hello
kubectl get ingress hello
NAME CLASS HOSTS ADDRESS PORTS AGE
hello traefik * 203.0.113.10 80 20s
Request the page from any machine:
curl -I http://your_server_ip
HTTP/1.1 200 OK
Content-Type: text/html
Server: nginx/1.xx.x
...
When you finish testing, remove the resources:
kubectl delete ingress hello
kubectl delete service hello
kubectl delete deployment hello
Step 7 - Using persistent storage
K3s includes the local-path provisioner, which creates volumes under /var/lib/rancher/k3s/storage on the node where the pod runs. It is the default StorageClass:
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 15m
Because it uses WaitForFirstConsumer, a PersistentVolumeClaim stays Pending until a pod uses it. That is expected. Local-path volumes live on a single node, so they do not follow a pod to another node; for replicated storage across nodes, install a solution such as Longhorn.
Troubleshooting
The service fails to start. Read the logs with sudo journalctl -u k3s -n 100 --no-pager. A common cause is another process already bound to port 6443; check with sudo ss -tlnp | grep 6443.
kubectl returns "permission denied" or connects to localhost:8080. KUBECONFIG is not set in the current shell or the copied file is not owned by your user. Repeat Step 4.
A worker does not appear in kubectl get nodes. On the worker, run sudo journalctl -u k3s-agent -n 100 --no-pager. Verify the token and that the worker can reach the server with curl -k https://your_server_ip:6443/ping, which should return pong.
Pods on different nodes cannot talk to each other. UDP port 8472 is blocked between the nodes. Check the UFW rules from Step 1 and any provider-level firewall.
Uninstalling. The installer creates /usr/local/bin/k3s-uninstall.sh on servers and /usr/local/bin/k3s-agent-uninstall.sh on agents. Running them removes K3s and all cluster data.
Conclusion
You now have a K3s cluster on Ubuntu 24.04 with kubectl access for a regular user, optional worker nodes and a verified ingress path through Traefik. From here you can add TLS certificates to your ingresses with cert-manager, install Helm to deploy packaged applications, or run three server nodes with cluster-init: true in the first server's configuration for an embedded etcd, high-availability control plane.
