k3s: Lightweight Kubernetes on Linux — Complete Guide

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.

Contents
  1. Prerequisites
  2. k3s vs Full Kubernetes
  3. Install k3s (Single Node)
    1. Configure kubectl Access Without sudo
    2. Installation Options Worth Knowing
  4. Essential kubectl Commands
  5. Deploy Your First Application
    1. Production-Style Deployment YAML
  6. Ingress with Traefik
  7. Persistent Storage
  8. Multi-Node Cluster
    1. Further Reading

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
  • curl installed (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.

Featurek3sFull Kubernetes (kubeadm)
Install time~2 minutes30–60 minutes
Minimum RAM512 MB2 GB
Binary size~70 MBMultiple components, several GBs total
Default datastoreSQLite (single node), etcd optionaletcd cluster required
Built-in ingressTraefik (included)None — install separately
Built-in load balancerServiceLB (klipper)None — requires cloud provider or MetalLB
kubectl compatibility100% compatible100% compatible
Production readyYesYes
HA multi-masterYes (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 k3s

Configure 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/proxy

Installation 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.10

Essential 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 yaml

Deploy 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 nginx

Production-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: LoadBalancer
kubectl apply -f myapp.yaml
kubectl get pods,svc -l app=myapp

Ingress 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 traefik

Save 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: 80
kubectl 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      30s

For 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   5m

Create 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-data
kubectl 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     20s

Note: 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+k3s1

Label your workers for scheduling clarity:

kubectl label node worker1 node


Go up