k3s: Lightweight Kubernetes on Linux — Complete Guide

Tested on: Ubuntu 26.04 LTS · Debian 12 · Raspberry Pi OS (64-bit) — Last updated: June 2026
k3s is a production-ready Kubernetes distribution from Rancher (SUSE) that ships as a single ~70 MB binary with SQLite as its default datastore, Traefik as its ingress controller, and ServiceLB for load balancing — all included out of the box. It runs on hardware as modest as a Raspberry Pi 4 and installs in under two minutes, making it the fastest path to a real Kubernetes environment without the operational overhead of kubeadm or managed cloud clusters.
Prerequisites
- A Linux server or VM running Ubuntu 26.04 LTS, Debian 12, or compatible distro (ARM64/ARMv7 also supported)
- Minimum 512 MB RAM (1 GB recommended for running workloads); 2 GB+ for multi-node
- Root or sudo access
- Ports 6443 (API server) and 10250 (kubelet) open between nodes for multi-node setups
curlinstalled (sudo apt install curl)
k3s vs Full Kubernetes
Before installing, understand what k3s trades away versus full Kubernetes — and why most of those trade-offs are irrelevant for the majority of deployments.
| Feature | k3s | Full Kubernetes (kubeadm) |
|---|---|---|
| Install time | ~2 minutes | 30–60 minutes |
| Minimum RAM | 512 MB | 2 GB |
| Binary size | ~70 MB | Multiple components, several GBs total |
| Default datastore | SQLite (single node), etcd optional | etcd cluster required |
| Built-in ingress | Traefik (included) | None — install separately |
| Built-in load balancer | ServiceLB (klipper) | None — requires cloud provider or MetalLB |
| kubectl compatibility | 100% compatible | 100% compatible |
| Production ready | Yes | Yes |
| HA multi-master | Yes (embedded etcd or external DB) | Yes |
The kubectl skills and YAML manifests you write for k3s run unchanged on EKS, GKE, and AKS. k3s is not a learning toy — it is what you actually deploy.
Install k3s (Single Node)
The official installer script handles everything: downloading the binary, registering the systemd service, generating kubeconfig, and starting the cluster.
# Install k3s — single command:
curl -sfL https://get.k3s.io | sh -
# What this creates:
# /usr/local/bin/k3s — the binary (kubectl, crictl, ctr all built in)
# /etc/rancher/k3s/k3s.yaml — kubeconfig (root-owned by default)
# /var/lib/rancher/k3s/ — data directory
# systemd service k3s.service — auto-starts on boot
# Verify the node is Ready:
sudo kubectl get nodes# Expected output:
NAME STATUS ROLES AGE VERSION
myserver Ready control-plane,master 60s v1.30.x+k3s1# Check the service:
sudo systemctl status k3sConfigure kubectl Access Without sudo
By default, the kubeconfig is owned by root. Copy it to your user's home directory so you can run kubectl without sudo:
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
chmod 600 ~/.kube/config
# Verify:
kubectl get nodes
kubectl cluster-info# Expected cluster-info output:
Kubernetes control plane is running at https://127.0.0.1:6443
CoreDNS is running at https://127.0.0.1:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxyInstallation Options Worth Knowing
# Pin a specific version:
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.30.2+k3s1 sh -
# Disable Traefik (if you want to use nginx-ingress instead):
curl -sfL https://get.k3s.io | sh -s - --disable traefik
# Disable ServiceLB (if you have MetalLB or an external LB):
curl -sfL https://get.k3s.io | sh -s - --disable servicelb
# Bind to a specific IP (useful on multi-NIC servers):
curl -sfL https://get.k3s.io | sh -s - --node-ip 192.168.1.10Essential kubectl Commands
# List resources:
kubectl get nodes
kubectl get pods # default namespace
kubectl get pods -A # all namespaces
kubectl get pods -n kube-system # specific namespace
kubectl get deployments
kubectl get services
kubectl get all # pods + services + deployments + replicasets
# Detailed info:
kubectl describe pod mypod
kubectl describe node myserver
kubectl describe svc myapp
# Logs:
kubectl logs mypod
kubectl logs mypod -f # stream logs
kubectl logs mypod --previous # logs from a crashed/restarted container
kubectl logs -l app=myapp # logs from all pods matching label
# Execute into a container:
kubectl exec -it mypod -- bash
kubectl exec -it mypod -- sh # if bash isn't available (Alpine-based images)
# Apply and delete manifests:
kubectl apply -f deployment.yaml
kubectl delete -f deployment.yaml
kubectl delete pod mypod # delete a specific pod (will be recreated by deployment)
# Watch for changes:
kubectl get pods -w
# Get resource YAML:
kubectl get deployment myapp -o yamlDeploy Your First Application
Start with the imperative approach to validate your cluster, then move to declarative YAML for anything real.
# Quick smoke test:
kubectl create deployment nginx --image=nginx:alpine
kubectl expose deployment nginx --port=80 --type=LoadBalancer
# Check the external IP assigned by ServiceLB:
kubectl get svc nginx# Output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.43.112.34 192.168.1.10 80:31234/TCP 15s# Scale and update:
kubectl scale deployment nginx --replicas=3
kubectl set image deployment/nginx nginx=nginx:1.27
kubectl rollout status deployment/nginx
# Roll back if the update causes issues:
kubectl rollout undo deployment/nginx
# Clean up:
kubectl delete deployment nginx
kubectl delete service nginxProduction-Style Deployment YAML
Save this as myapp.yaml. It includes resource limits — essential for preventing a single pod from starving the node:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: nginx:alpine
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 80
type: LoadBalancerkubectl apply -f myapp.yaml
kubectl get pods,svc -l app=myappIngress with Traefik
k3s ships Traefik v2 as its ingress controller. Use Ingress resources to route multiple domains or paths to different services — all sharing port 80/443 on the same node IP.
# Confirm Traefik is running:
kubectl get pods -n kube-system -l app.kubernetes.io/name=traefik
kubectl get svc -n kube-system traefikSave this as ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80kubectl apply -f ingress.yaml
# Verify:
kubectl get ingress
# NAME CLASS HOSTS ADDRESS PORTS AGE
# myapp-ingress traefik myapp.example.com 192.168.1.10 80 30sFor TLS, add a tls block to the Ingress spec and use cert-manager with Let's Encrypt. Install cert-manager with Helm (covered in the Helm section below) or apply the official manifest from cert-manager.io.
Persistent Storage
k3s includes the local-path storage provisioner, which automatically creates hostPath volumes under /var/lib/rancher/k3s/storage/. It's the default StorageClass — no configuration needed.
# Check available storage classes:
kubectl get storageclass# Output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer 5mCreate a PVC and mount it in a deployment:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myapp-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stateful
spec:
replicas: 1
selector:
matchLabels:
app: myapp-stateful
template:
metadata:
labels:
app: myapp-stateful
spec:
containers:
- name: myapp
image: nginx:alpine
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: myapp-datakubectl apply -f stateful-app.yaml
kubectl get pvc
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
# myapp-data Bound pvc-abc123... 5Gi RWO local-path 20sNote: local-path volumes are node-local. If a pod reschedules to a different node, it loses access to the data. For multi-node persistence, use NFS, Longhorn (installable via Helm), or a cloud block storage CSI driver.
Multi-Node Cluster
Adding worker nodes to k3s requires two pieces of information from the server: its IP address and the node token.
# On the SERVER (control-plane) node — already running k3s:
# Get the node token:
sudo cat /var/lib/rancher/k3s/server/node-token
# K10a3b2c1d4e5...::server:f6g7h8i9j0token
# Get the server IP (replace eth0 with your interface if different):
ip -4 addr show eth0 | grep inet | awk '{print $2}' | cut -d/ -f1
# 192.168.1.10# On each WORKER node (replace MASTER_IP and NODE_TOKEN with real values):
curl -sfL https://get.k3s.io |
K3S_URL=https://192.168.1.10:6443
K3S_TOKEN=K10a3b2c1d4e5...::server:f6g7h8i9j0token
sh -
# The agent registers automatically. Back on the server:
kubectl get nodes# Expected output after workers join:
NAME STATUS ROLES AGE VERSION
master Ready control-plane,master 10m v1.30.x+k3s1
worker1 Ready <none> 2m v1.30.x+k3s1
worker2 Ready <none> 90s v1.30.x+k3s1Label your workers for scheduling clarity:
kubectl label node worker1 node
