How to Install Kubernetes (K3s) on Ubuntu 24.04 — Lightweight Cluster
Quick Summary
K3s is a certified, production-grade Kubernetes distribution from Rancher
(now SUSE) that strips the standard upstream distribution down to a single
60 MB binary. It runs happily on a 1 vCPU / 2 GB RAM VPS yet speaks the
exact same API as the full "big" Kubernetes you see at AWS, GCP or Azure.
This guide walks you through a single-node install, then extends it to a
multi-node HA cluster, adds Helm, Traefik ingress, cert-manager for
automatic SSL, persistent storage, Prometheus monitoring and backups.
**Estimated time: 20 minutes for single-node, 45 minutes for the full
multi-node production setup.**>
Skip the manual setup? Deploy a pre-configured K3s cluster with our
1-click Kubernetes stack.
Table of Contents
- What is K3s?
- K3s vs Upstream Kubernetes (K8s)
- Prerequisites
- Step 1: Prepare the Ubuntu Host
- Step 2: Install K3s (Single-Node)
- Step 3: Verify the Cluster With kubectl
- Step 4: Add Agent Nodes (Multi-Node Cluster)
- Step 5: Production Cluster Hardening
- Step 6: Install Helm
- Step 7: Deploy a Sample NGINX App
- Step 8: Expose Services With Traefik Ingress
- Step 9: Automatic SSL With cert-manager
- Step 10: Persistent Volumes (local-path-provisioner)
- Step 11: Monitoring With Prometheus and Grafana
- Step 12: Back Up etcd and Cluster State
- Step 13: Upgrading K3s
- Troubleshooting
- FAQ
- Next Steps
What is K3s?
K3s is a fully conformant, CNCF-certified Kubernetes distribution built by Rancher Labs (now part of SUSE) and donated to the CNCF as a Sandbox project. The name is a play on "K8s" — take five characters out and you get "K3s", signalling that roughly half the footprint of standard Kubernetes has been removed. In practical terms, the whole control plane and worker run from a single Go binary of around 60 MB, with SQLite as the default datastore instead of etcd, and legacy/alpha features and non-default cloud providers stripped out.
Despite that, K3s passes the full Kubernetes conformance test suite. Every manifest, Helm chart, operator and kubectl command you run against "big" Kubernetes works identically on K3s. It is what powers Rancher's own edge offering, Home Assistant installations, Raspberry Pi clusters, CI runners, and a growing number of production SaaS deployments where the upstream distribution is simply overkill.
K3s ships with sane defaults that would otherwise take hours of extra work on a vanilla cluster: containerd as the container runtime, Flannel as the CNI, CoreDNS, Traefik v2 as the ingress controller, a local-path storage class, the Kubernetes metrics-server, and a bundled ServiceLB (klipper-lb) for LoadBalancer-typed services without needing a cloud provider. You can turn any of these off with a single flag if you want to swap in Cilium, NGINX ingress, MetalLB, Longhorn or anything else.
K3s vs Upstream Kubernetes (K8s)
| Dimension | K3s | Upstream Kubernetes |
|---|---|---|
| Binary size | Single 60 MB binary | Multiple binaries, hundreds of MB |
| RAM floor | ~512 MB for control plane | 2+ GB just for control plane |
| Datastore | SQLite (default), embedded etcd or external MySQL/Postgres | etcd only |
| Install command | One curl</td><td>sh line | kubeadm, manual etcd, CNI, etc. |
| Container runtime | containerd bundled | Bring your own |
| Ingress | Traefik bundled | Install separately |
| Load balancer | klipper ServiceLB bundled | Cloud provider or MetalLB |
| Certification | CNCF conformant | CNCF conformant |
| API compatibility | 100 percent | 100 percent |
| Typical use case | Edge, IoT, small-to-mid SaaS, dev clusters | Huge clusters (1000+ nodes) |
Prerequisites
Before you start, you will need:
- Server node: at least 1 vCPU / 2 GB RAM / 20 GB SSD. For
> Recommended: our CloudCore Business plan > (4 vCPU / 8 GB RAM / 200 GB NVMe) for the server node, plus one or more > smaller agents for a true multi-node cluster. Business-tier NVMe makes a > huge difference for etcd latency.
- Agent nodes (optional, for multi-node): 1 vCPU / 2 GB RAM each
- Ubuntu 24.04 LTS on every node (minimal image is fine)
- SSH access with a non-root sudo user on every node
- Stable private or public networking between all nodes on ports 6443
- A domain name with DNS A-records pointing to your server node
Step 1: Prepare the Ubuntu Host
Run these commands on every node (server and agents) before installing K3s itself.
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl ca-certificates gnupg iptables open-iscsi nfs-commoncurlis used by the K3s installer to fetch the binary.iptablesis required by kube-proxy. K3s will pin a compatible version.open-iscsiandnfs-commonare needed later if you attach block or NFS
Disable swap — Kubernetes requires it off for the kubelet to schedule reliably:
sudo swapoff -a
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstabEnable required kernel modules and sysctls so Flannel VXLAN and kube-proxy work correctly:
sudo tee /etc/modules-load.d/k3s.conf <<EOF br_netfilter overlay EOF sudo modprobe br_netfilter overlay
sudo tee /etc/sysctl.d/99-k3s.conf <<EOF net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system
Finally, make sure the hostname is unique per node and your /etc/hosts
has an entry for the node's own hostname. K3s uses the hostname as the
default node name.
Step 2: Install K3s (Single-Node)
On the server node, run:
curl -sfL https://get.k3s.io | sh -That is it. In under 60 seconds the installer will:
/usr/local/bin/k3s./etc/systemd/system/k3s.service and start it./etc/rancher/k3s/k3s.yaml.You can pass flags to the installer via INSTALL_K3S_EXEC if you want to
tweak defaults at install time. A common production recipe:
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
--write-kubeconfig-mode 644 \
--tls-san $(curl -s ifconfig.me) \
--disable servicelb \
--cluster-init" sh -What each flag does:
--write-kubeconfig-mode 644— lets your regular user read the kubeconfig
--tls-san— adds your public IP (or a DNS name) to the API server's TLS
--disable servicelb— turn off bundled klipper-lb if you plan to use
--cluster-init— bootstraps embedded etcd instead of SQLite so you can
Step 3: Verify the Cluster With kubectl
K3s ships its own kubectl built into the binary, but the real kubectl
is more convenient. Point it at the generated kubeconfig:
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
export KUBECONFIG=~/.kube/configNow run:
kubectl get nodes
kubectl get pods -AExpected output:
NAME STATUS ROLES AGE VERSION k3s-srv01 Ready control-plane,master 1m v1.30.3+k3s1
NAMESPACE NAME READY STATUS AGE kube-system coredns-6799fbcd5-h9x7g 1/1 Running 1m kube-system helm-install-traefik-xxx 0/1 Completed 1m kube-system local-path-provisioner-6c86858495-d2kkr 1/1 Running 1m kube-system metrics-server-54fd9b65b-7hhnk 1/1 Running 1m kube-system svclb-traefik-xxx 2/2 Running 1m kube-system traefik-7d647b7597-ppvq6 1/1 Running 1m
If every pod is Running or Completed and the node reports Ready, your
cluster is live.
Step 4: Add Agent Nodes (Multi-Node Cluster)
A single-node cluster is fine for dev, but production benefits from a real multi-node setup. Agents run workloads but no control plane components.
First, grab the cluster join token from the server node:
sudo cat /var/lib/rancher/k3s/server/node-tokenCopy the whole string (it starts with K10...). Then on each **agent
node**, run:
curl -sfL https://get.k3s.io | K3S_URL=https://<SERVER_IP>:6443 \
K3S_TOKEN=<TOKEN> sh -Replace <SERVER_IP> with the server's reachable IP and <TOKEN> with the
join token. Within a minute the agent will register.
Back on the server, verify:
kubectl get nodes -o wideYou should now see all nodes with role <none> (workers) or
control-plane. Add labels to describe node hardware if you have a mix:
kubectl label node k3s-agent01 node-type=worker disk=nvmeHA Control Plane (Advanced)
For a true HA cluster, install three server nodes. The first one uses
--cluster-init; the remaining two join as servers, not agents:
# On server-2 and server-3:
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
--server https://<SERVER_1_IP>:6443" \
K3S_TOKEN=<TOKEN> sh -You now have an embedded-etcd quorum tolerant of one node failing. Put a load balancer (HAProxy, DNS round-robin, or our CloudCore Load Balancer add-on) in front of port 6443.
Step 5: Production Cluster Hardening
Firewall
Open only what the cluster needs:
sudo ufw allow OpenSSH
sudo ufw allow 6443/tcp # Kubernetes API
sudo ufw allow 8472/udp # Flannel VXLAN
sudo ufw allow 10250/tcp # kubelet
sudo ufw allow 80,443/tcp # Traefik ingress
sudo ufw enableNon-Root Kubeconfig
Never commit /etc/rancher/k3s/k3s.yaml to git — it is cluster-admin. For
day-to-day work create namespaced service accounts with RBAC and issue
short-lived kubeconfigs.
Audit Logging
Enable the Kubernetes audit log by editing the K3s systemd unit and adding:
--kube-apiserver-arg=audit-log-path=/var/log/k3s-audit.log
--kube-apiserver-arg=audit-policy-file=/etc/rancher/k3s/audit-policy.yamlStep 6: Install Helm
Helm is the de-facto package manager for Kubernetes. Every add-on below (cert-manager, Prometheus, etc.) ships as a Helm chart.
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm versionAdd the most common chart repositories:
helm repo add jetstack https://charts.jetstack.io
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo updateStep 7: Deploy a Sample NGINX App
Let us verify the cluster can schedule and expose a workload. Create
nginx-demo.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
labels:
app: nginx-demo
spec:
replicas: 2
selector:
matchLabels:
app: nginx-demo
template:
metadata:
labels:
app: nginx-demo
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
apiVersion: v1
kind: Service
metadata:
name: nginx-demo
spec:
selector:
app: nginx-demo
ports:
- port: 80
targetPort: 80
type: ClusterIPApply it:
kubectl apply -f nginx-demo.yaml
kubectl get pods -l app=nginx-demo
kubectl get svc nginx-demoYou should see two Running pods and a ClusterIP service. Port-forward
to test:
kubectl port-forward svc/nginx-demo 8080:80
curl http://localhost:8080Step 8: Expose Services With Traefik Ingress
K3s ships Traefik v2 pre-installed. Adding ingress is just a manifest.
Create nginx-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-demo
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
spec:
rules:
- host: demo.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-demo
port:
number: 80kubectl apply -f nginx-ingress.yamlPoint demo.example.com's A record to your server IP, wait for DNS to
propagate, then visit http://demo.example.com — Traefik routes the
request to the NGINX pods.
You can explore Traefik's dashboard by port-forwarding:
kubectl -n kube-system port-forward $(kubectl -n kube-system get pods -l app.kubernetes.io/name=traefik -o name | head -1) 9000:9000Then open http://localhost:9000/dashboard/.
Step 9: Automatic SSL With cert-manager
cert-manager automates Let's Encrypt certificates for every ingress.
kubectl create namespace cert-manager
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--set installCRDs=true \
--set global.leaderElection.namespace=cert-managerCreate a ClusterIssuer for Let's Encrypt production. Save as
letsencrypt-prod.yaml:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: [email protected]
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: traefikkubectl apply -f letsencrypt-prod.yamlNow update the ingress to request a certificate:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-demo
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
tls:
- hosts:
- demo.example.com
secretName: demo-example-com-tls
rules:
- host: demo.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-demo
port:
number: 80Within a minute cert-manager will solve the HTTP-01 challenge and your site is live on HTTPS.
Step 10: Persistent Volumes (local-path-provisioner)
K3s ships Rancher's local-path-provisioner as the default storage class,
backed by /var/lib/rancher/k3s/storage on whichever node the pod lands on.
It is ideal for dev and for single-node Prometheus/Postgres deployments.
Create pvc-demo.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gikubectl apply -f pvc-demo.yaml
kubectl get pvcFor multi-node workloads that need the volume to follow the pod across hosts, install Longhorn (distributed block storage) or NFS CSI:
helm repo add longhorn https://charts.longhorn.io
kubectl create namespace longhorn-system
helm install longhorn longhorn/longhorn --namespace longhorn-systemStep 11: Monitoring With Prometheus and Grafana
The kube-prometheus-stack chart deploys Prometheus, Alertmanager, Grafana
and a suite of pre-built dashboards in one shot.
kubectl create namespace monitoring
helm install kps prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--set grafana.adminPassword='changeme' \
--set prometheus.prometheusSpec.retention=15d \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=local-path \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=20GiAccess Grafana:
kubectl -n monitoring port-forward svc/kps-grafana 3000:80Login at http://localhost:3000 with admin / changeme. Pre-installed
dashboards cover cluster health, node exporter, Kubernetes API, kubelet,
CoreDNS and more.
Step 12: Back Up etcd and Cluster State
K3s takes automatic etcd snapshots by default if you enabled embedded etcd
(--cluster-init). Snapshots live in /var/lib/rancher/k3s/server/db/snapshots/
every 12 hours, retaining the last five.
Trigger a manual snapshot:
sudo k3s etcd-snapshot save --name pre-upgrade-$(date +%Y%m%d)Ship snapshots offsite to S3:
sudo k3s etcd-snapshot save \
--s3 \
--s3-bucket=my-k3s-backups \
--s3-region=eu-central-1 \
--s3-access-key=<KEY> \
--s3-secret-key=<SECRET>Or configure it permanently in /etc/rancher/k3s/config.yaml:
etcd-s3: true
etcd-s3-bucket: my-k3s-backups
etcd-s3-region: eu-central-1
etcd-s3-access-key: <KEY>
etcd-s3-secret-key: <SECRET>
etcd-snapshot-schedule-cron: "0 /6 "
etcd-snapshot-retention: 10For application data, a good pattern is Velero — it handles PV snapshotting plus cluster-object backup to S3-compatible storage.
Step 13: Upgrading K3s
Upgrades are a single command. Snapshot first, then:
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.30.5+k3s1 sh -The installer stops the service, swaps the binary, and restarts. Control plane nodes should be upgraded one at a time; agents can be batched.
For zero-touch upgrades, install the system-upgrade-controller:
kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yamlThen drop in an upgrade Plan manifest and the controller drains and
upgrades nodes on your schedule.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
kubectl returns connection refused on port 6443 | API server not listening yet or firewall blocking | sudo systemctl status k3s, sudo ufw allow 6443/tcp |
Node stuck in NotReady | CNI (Flannel) cannot reach peers, often UDP 8472 blocked | Open sudo ufw allow 8472/udp on every node |
Pods stuck in Pending with no nodes available | Resource requests exceed what any node has | kubectl describe pod and lower requests or add a node |
ImagePullBackOff on private registries | Missing imagePullSecrets | Create a docker-registry secret and reference it in the pod spec |
| Certificates not issuing | cert-manager cannot reach Let's Encrypt or DNS wrong | kubectl describe certificate <name>, check HTTP-01 ingress route |
Out of disk on /var/lib/rancher | containerd image cache growth | sudo k3s crictl rmi --prune and expand the volume |
Agent cannot join, tls: bad certificate | Wrong token or server hostname changed | Re-fetch /var/lib/rancher/k3s/server/node-token and reinstall agent |
| High load average on server node | Single-node cluster is running too many pods | Add agent nodes or upgrade to our CloudCore Business plan |
sudo journalctl -u k3s -n 200 --no-pager
kubectl get events -A --sort-by=.lastTimestamp | tail -50
kubectl describe node <node>FAQ
Q: Is K3s production ready?
A: Yes. K3s is CNCF-certified Kubernetes and is used in production by thousands of organisations including at SUSE/Rancher themselves. Use the embedded-etcd HA mode with three server nodes for production workloads.
Q: K3s vs MicroK8s vs kind vs kubeadm — which should I pick?
A: K3s is the best fit for VPS-based production clusters: single binary, battery-included defaults, bundled Traefik and storage class. MicroK8s is Canonical's equivalent; kind runs in Docker for local dev only; kubeadm is the "hard way" — powerful but requires you to assemble every component yourself.
Q: Can I run K3s on ARM (Raspberry Pi, Ampere, AWS Graviton)?
A: Yes. K3s publishes arm64 and armv7 binaries and the install script picks the right one automatically. A 4 GB Pi 4 comfortably runs a dev cluster.
Q: How much RAM does K3s actually use at idle?
A: A fresh server node with Traefik, CoreDNS, metrics-server and local-path-provisioner running uses around 500-700 MB. Agents use around 150 MB. That leaves the vast majority of a 2 GB VPS for your workloads.
Q: Can I run Docker images without Docker?
A: Yes. K3s uses containerd directly. Any OCI image (which every Docker image is) runs unmodified.
Q: Do I need a load balancer for a single-node cluster?
A: No. The bundled klipper-lb (ServiceLB) binds LoadBalancer services directly to host ports. For multi-node clusters you can keep klipper-lb or swap in MetalLB / our CloudCore LB.
Q: How do I uninstall K3s?
A: On the server run sudo /usr/local/bin/k3s-uninstall.sh. On agents,
sudo /usr/local/bin/k3s-agent-uninstall.sh. Both scripts are created by
the installer and remove all data, unit files and binaries.
Q: Can I migrate from K3s to full upstream Kubernetes later?
A: Yes. Because the API is identical, every manifest and Helm chart moves unchanged. You would typically back up etcd, restore into a fresh kubeadm cluster, or use Velero to migrate namespaces one at a time.
Q: Where does K3s store its data?
A: /var/lib/rancher/k3s for the datastore and container images, plus
/etc/rancher/k3s for config and the kubeconfig. Back these up together
with your etcd snapshots.
Q: How do I add GPU support?
A: Install the NVIDIA container toolkit on the host, then add the
nvidia-device-plugin DaemonSet. K3s picks up GPUs automatically through
containerd.
Next Steps
friendly web UI on top of your cluster resilient block storage across nodes- How to Install ArgoCD — GitOps
- How to Set Up Automated Backups — ship etcd
### Skip the Manual Install>
We offer K3s clusters as a 1-click app on all VPS plans, with optional
pre-wired Traefik, cert-manager, Longhorn and Prometheus. Multi-node
clusters are deployed and joined automatically.>
Deploy K3s Now | CloudCore
Business from EUR 19.99/mo | 172+ 1-click apps | 9 global locations