How to Install K3s on Ubuntu (Lightweight Kubernetes Getting Started)

Written by: Bagus Facsi Aginsa
Published at: 07 Jul 2026


Learning Kubernetes has a chicken-and-egg problem: you need a cluster to practice on, but building a cluster the traditional way (kubeadm, a container runtime, a CNI plugin, certificates) is itself an advanced exercise. And the managed cloud offerings hide exactly the parts you are trying to learn while charging you by the hour.

K3s solves this neatly. It is a certified, production-grade Kubernetes distribution packed into a single binary of well under 100 MB, and it installs on a plain Ubuntu server with one command. Sixty seconds later you have a working cluster with networking, DNS, storage, and an ingress controller already wired up. It is not a toy: plenty of companies run K3s in production, especially on edge devices and small on-premise setups.

In this tutorial you will install a single-node K3s cluster on Ubuntu, understand what the installer actually set up, deploy a real application, and expose it to the network. This guide is for developers and sysadmins who are new to Kubernetes or who want the fastest respectable path to a homelab cluster.


What K3s Is (and What It Trades Away)

K3s is upstream Kubernetes with the operational weight removed. The differences that matter:

  • One binary, one service. The API server, scheduler, controller manager, kubelet, and container runtime (containerd) all run inside a single process managed by systemd. There is no separate Docker installation and nothing to assemble.
  • SQLite instead of etcd by default. Cluster state goes to a local SQLite file, which is perfect for one node. You can swap in etcd or an external SQL database later without changing how you use the cluster.
  • Batteries included. K3s ships with flannel (pod networking), CoreDNS, a local-path storage provisioner, a service load balancer (Klipper), and the Traefik ingress controller. On full Kubernetes, each of those is homework.

The trade-offs are honest ones: flannel does not enforce network policies, SQLite means a single point of truth on one disk, and the bundled components are opinionated defaults. Every one of these can be replaced when you outgrow it, which is exactly what I did in How To Install K3s With Calico And External MySQL Database. Today, the defaults are what we want: the fastest path to a working cluster.


Prerequisites

  1. A server or VM with Ubuntu 22.04 or 24.04, minimum 1 CPU core and 1 GB RAM (2 CPU / 4 GB is comfortable once you deploy real workloads)
  2. A user with sudo privileges
  3. Basic Linux command line knowledge, no prior Kubernetes experience required
  4. If a firewall is active, ports 6443/tcp (API server) open to machines that will run kubectl

Step 1: Install K3s

One command:

curl -sfL https://get.k3s.io | sh -
[INFO]  Finding release for channel stable
[INFO]  Using v1.33.1+k3s1 as release
[INFO]  Downloading binary https://github.com/k3s-io/k3s/releases/download/v1.33.1+k3s1/k3s
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  systemd: Enabling k3s unit
[INFO]  systemd: Starting k3s

The script downloaded the binary, created a systemd service, generated certificates, and started the cluster. It also symlinked kubectl (plus crictl and ctr) to the K3s binary, so the standard Kubernetes CLI is already on your PATH.

Confirm the service is healthy:

sudo systemctl status k3s
● k3s.service - Lightweight Kubernetes
     Loaded: loaded (/etc/systemd/system/k3s.service; enabled)
     Active: active (running)

Step 2: Talk to Your Cluster

Try the classic first command:

sudo kubectl get nodes
NAME      STATUS   ROLES                  AGE   VERSION
ngoprek   Ready    control-plane,master   1m    v1.33.1+k3s1

One node, ready, acting as both control plane and worker. Notice the sudo: the kubeconfig file at /etc/rancher/k3s/k3s.yaml is root-only by default. Fix that for your daily user the clean way:

mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/config

Now kubectl works without sudo. Take a look at everything the installer started for you:

kubectl get pods -n kube-system
NAME                                      READY   STATUS      RESTARTS   AGE
coredns-697968c856-7gvxp                  1/1     Running     0          3m
local-path-provisioner-774c6665dc-tp4tb   1/1     Running     0          3m
metrics-server-6f4c6675d5-t8p2q           1/1     Running     0          3m
helm-install-traefik-crd-jt5cx            0/1     Completed   0          3m
helm-install-traefik-w9r6k                0/1     Completed   0          3m
svclb-traefik-8fb56f96-kkbmz              2/2     Running     0          2m
traefik-c98fdf6fb-df2rc                   1/1     Running     0          2m

That is DNS, storage, metrics, and a full ingress controller, running before you configured anything.


Step 3: Deploy Your First Application

Let’s deploy nginx with two replicas and watch Kubernetes do its job. Create a manifest:

nano web-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.29-alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web-app
spec:
  selector:
    app: web-app
  ports:
  - port: 80
    targetPort: 80

Apply it and watch the pods appear:

kubectl apply -f web-app.yaml
kubectl get pods -l app=web-app
NAME                       READY   STATUS    RESTARTS   AGE
web-app-59d4f6c7d8-4x2mn   1/1     Running   0          20s
web-app-59d4f6c7d8-p7ztl   1/1     Running   0          20s

The Service gives the two pods one stable internal address. Test it from inside the cluster:

kubectl run test --rm -it --image=busybox --restart=Never -- wget -qO- http://web-app
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

If the Deployment, Service, and label plumbing feel like magic words right now, my explainer Understanding Kubernetes Objects fills in the model behind them.


Step 4: Expose the App with the Bundled Traefik Ingress

Internal access is nice, but you want the app reachable from a browser. K3s already runs Traefik listening on ports 80 and 443 of the node. All it needs is an Ingress rule:

nano web-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app
spec:
  rules:
  - host: web.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-app
            port:
              number: 80
kubectl apply -f web-ingress.yaml

Point the hostname at your node for testing (on your workstation, replace 10.0.0.30 with the server’s IP):

echo "10.0.0.30 web.example.com" | sudo tee -a /etc/hosts
curl -s http://web.example.com | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Request flow: browser to Traefik on port 80, Traefik matches the Host header against your ingress rule, forwards to the web-app service, which balances across the two pods. That is the same pattern production clusters use, running on your single node.


Step 5: Know Your Cluster’s Housekeeping Commands

A few commands worth knowing from day one.

Check resource usage (the bundled metrics-server makes this work immediately):

kubectl top nodes
kubectl top pods

Restart K3s safely (workloads keep running, containers are not touched):

sudo systemctl restart k3s

And if you ever want to wipe the experiment completely, K3s installs its own uninstaller, which removes the service, binaries, and all cluster data:

/usr/local/bin/k3s-uninstall.sh

Common Mistakes and Troubleshooting

kubectl says permission denied reading the config. You skipped the kubeconfig copy in Step 2, or a K3s upgrade reset permissions on /etc/rancher/k3s/k3s.yaml. Repeat the copy commands.

The node is Ready but pods stay Pending. Describe the pod to see the scheduler’s reason: kubectl describe pod <name>. On tiny VMs it is usually insufficient memory; either shrink resource requests or give the VM more RAM.

Ingress returns 404 from Traefik. The host in your ingress rule does not match the hostname you are requesting. Traefik routes by Host header; curl http://10.0.0.30 without the header hits no rule and returns 404. Use the domain name.

Port 80 already in use / svclb pod crash-looping. Something else on the node (often an apt-installed nginx) occupies the port Traefik needs. Stop and disable it: sudo systemctl disable --now nginx.

Cluster misbehaves after the host IP changed. Certificates and internal state reference the old address. For a homelab, give the node a static IP; the fastest fix after a change is often reinstalling K3s (seconds, thanks to the uninstaller).

You installed on a machine with an existing Docker/Kubernetes setup and things conflict. K3s brings its own containerd and iptables rules. Start from a clean host, or at minimum remove old CNI configs in /etc/cni/net.d/.


Best Practices

  1. Give the node a static IP address before you invest work in the cluster. Kubernetes certificates and joins all reference it.
  2. Keep your manifests in files (and in git), as we did here, instead of kubectl run improvisation. The YAML file is the cluster’s documentation.
  3. Update K3s deliberately: re-running the install script upgrades in place, so pin a channel or version in real environments rather than riding stable blindly.
  4. Back up /var/lib/rancher/k3s/server (or the SQLite file specifically) if the cluster starts holding anything you care about.
  5. Learn the health-check primitives early. Liveness and readiness probes are what turn “it deployed” into “it stays up”: Kubernetes Health Checks: Liveness, Readiness, and Startup Probes.

Conclusion

You installed a complete Kubernetes cluster with one command, deployed a replicated application, and exposed it through a real ingress controller, all on a single Ubuntu box. More importantly, everything you practiced (Deployment, Service, Ingress, kubectl) transfers unchanged to any Kubernetes cluster anywhere.

When one node is not enough, grow it: How to Join a Node to an Existing K3s Cluster adds workers and servers, and K3s with Calico and External MySQL upgrades the internals for production duty. And once you deploy more than a couple of apps, Helm keeps the YAML manageable.