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 sudo privileges on every server.
  • Private or public network connectivity between the nodes. This guide uses your_server_ip for 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
...

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-mode keeps the admin kubeconfig readable only by root.
  • tls-san adds your server IP (or a DNS name) to the API server certificate so you can use kubectl remotely.
  • node-label is 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

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.

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

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.