Container Orchestration intermediate ⏱ 60 minutes

K0s: Zero-Friction Kubernetes for Homelabs

Single-binary Kubernetes with minimal dependencies. The easiest path to production-grade clusters.

By homelabbing.io • 2025-12-16
#k0s#kubernetes#containers#orchestration#lightweight

Overview

K0s (pronounced “kay-zero-s”) is a CNCF-certified Kubernetes distribution that delivers the full power of Kubernetes through a single binary with zero dependencies. Developed by Mirantis, k0s eliminates the complexity traditionally associated with Kubernetes deployment while maintaining 100% upstream compatibility and passing all CNCF conformance tests.

Unlike heavyweight Kubernetes distributions that require extensive prerequisites and configuration, k0s bundles everything needed - container runtime (containerd), networking (Kube-router or Calico), storage, and all Kubernetes components - into a single ~250MB binary. For homelabbers, this means going from zero to a production-grade Kubernetes cluster in under 10 minutes.

What you’ll learn:

  • Install k0s on single-node and multi-node clusters
  • Understand k0s architecture (controller, worker, controller+worker)
  • Deploy applications using kubectl and YAML manifests
  • Compare k0s with K3s and choose the right distribution
  • Configure storage, networking, and ingress
  • Manage cluster lifecycle (backup, restore, upgrade)
  • Implement best practices for homelab Kubernetes
  • Troubleshoot common k0s issues

Why k0s?

Instead of managing separate components (kubelet, kube-proxy, CNI plugins, container runtime), k0s provides a batteries-included distribution that just works. One binary, one config file, one command to start a cluster. Yet unlike simplified alternatives, k0s remains a full, conformant Kubernetes implementation - anything that works on “real” Kubernetes works on k0s.


Prerequisites

Before starting, ensure you have:

  • Linux system(s) (Ubuntu 20.04+, Debian 11+, RHEL 8+, or compatible)
  • Basic Kubernetes knowledge helpful but not required
  • Hardware requirements per node:
    • Single-node (controller+worker): 2 CPU cores, 2 GB RAM, 20 GB storage
    • Controller node: 1 CPU core, 1 GB RAM, 10 GB storage
    • Worker node: 2 CPU cores, 2 GB RAM, 20 GB storage
  • Network connectivity between cluster nodes
  • Open ports:
    • Controller: 6443 (API), 8132 (Konnectivity), 9443 (controller join)
    • Worker: 10250 (kubelet)
  • Root or sudo access for installation

Minimum viable cluster:

  • 1 node: Controller+worker combined (perfect for homelab testing)
  • 3 nodes: 1 controller + 2 workers (recommended for production-like setup)
  • HA: 3 controllers + N workers (high availability)

Understanding K0s Fundamentals

What is K0s?

K0s is a minimalist Kubernetes distribution focused on:

  1. Zero dependencies: Single binary, no host OS packages required
  2. Zero friction: Simple installation, sensible defaults
  3. Full Kubernetes: 100% upstream, CNCF certified
  4. Flexible deployment: Single-node to multi-node HA clusters

Architecture Components

Node types in k0s:

┌─────────────────────────────────────────┐
│         Controller Node                 │
│  ┌───────────────────────────────────┐  │
│  │ kube-apiserver                    │  │
│  │ kube-controller-manager           │  │
│  │ kube-scheduler                    │  │
│  │ etcd (embedded)                   │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│         Worker Node                     │
│  ┌───────────────────────────────────┐  │
│  │ kubelet                           │  │
│  │ kube-proxy                        │  │
│  │ containerd (bundled)              │  │
│  │ CNI (kube-router/calico)          │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│    Controller+Worker Node (All-in-One)  │
│  ┌───────────────────────────────────┐  │
│  │ All control plane components      │  │
│  │ + All worker components            │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

Controller nodes:

  • Run Kubernetes control plane (API server, scheduler, controller manager)
  • Store cluster state in embedded etcd
  • Don’t run workload pods by default (but can if combined with worker)

Worker nodes:

  • Run containerized workloads (your applications)
  • Communicate with controller via API server
  • Execute pod scheduling decisions

Controller+Worker nodes:

  • Combined mode (perfect for single-node homelabs)
  • Runs both control plane and workloads
  • Default mode for k0s installation

K0s vs K3s: Detailed Comparison

Both are lightweight Kubernetes, but different philosophies:

AspectK0sK3s
DeveloperMirantisRancher (SUSE)
PhilosophyModular, upstream-firstOpinionated, integrated
Binary Size~250 MB~70 MB
DependenciesZero (fully bundled)Zero (fully bundled)
Container Runtimecontainerd (bundled)containerd (bundled)
CNI Defaultkube-routerFlannel
CNI OptionsKube-router, Calico, customFlannel, Canal, custom
Storage DefaultNone (bring your own)Local-path provisioner
Service LBNone (bring your own)Klipper LB (built-in)
Databaseetcd (embedded)SQLite or etcd
Control PlaneTraditional KubernetesSimplified components
Airgap SupportNativeNative
ARM SupportYes (arm64)Yes (arm64, armhf)
UpdatesRolling updatesIn-place upgrades
ConfigurationYAML config fileCLI flags + config
ComplexityMore modularMore integrated
Use CaseFlexible infrastructureEdge/IoT focused

When to Choose K0s vs K3s

Choose K0s if:

  • You want maximum flexibility in component selection
  • You prefer vanilla Kubernetes experience
  • You plan to integrate custom CNI/storage solutions
  • You’re building infrastructure for learning production Kubernetes
  • You want clear separation between controller and worker nodes
  • You prefer configuration via YAML over CLI flags

Choose K3s if:

  • You want the simplest possible Kubernetes
  • You need built-in load balancer (Klipper)
  • You prefer opinionated, batteries-included defaults
  • You’re deploying to edge devices or IoT
  • You want the smallest possible footprint
  • You prioritize simplicity over flexibility

Both are excellent for homelabs! K0s feels more like “real Kubernetes,” while K3s feels more like “Kubernetes made easy.”


Installation and Initial Setup

Single-Node Installation (Controller+Worker)

Perfect for learning and small homelabs.

Step 1: Download k0s binary

# Download latest k0s
curl -sSLf https://get.k0s.sh | sudo sh

# Verify installation
k0s version

# Output:
# v1.29.1+k0s.0

Step 2: Install k0s as a systemd service

# Install k0s controller+worker (all-in-one mode)
sudo k0s install controller --enable-worker

# Start k0s service
sudo k0s start

# Check status
sudo k0s status

# Output:
# Version: v1.29.1+k0s.0
# Process ID: 12345
# Role: controller+worker
# Workloads: true

Step 3: Access your cluster

# Export kubeconfig
mkdir -p ~/.kube
sudo k0s kubeconfig admin > ~/.kube/config

# Install kubectl (if not already installed)
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

# Verify cluster access
kubectl get nodes

# Output:
# NAME         STATUS   ROLES    AGE   VERSION
# homelab-01   Ready    <none>   2m    v1.29.1+k0s

Step 4: Deploy test application

# Create nginx deployment
kubectl create deployment nginx --image=nginx:alpine

# Expose as service
kubectl expose deployment nginx --port=80 --type=NodePort

# Get service port
kubectl get svc nginx

# Access nginx (replace 32000 with actual NodePort)
curl http://localhost:32000

Congratulations! You have a working Kubernetes cluster in under 10 minutes.

Multi-Node Installation (HA-Capable)

For production-like homelab setups with separate controller and worker nodes.

Topology example:

  • controller-01: 192.168.1.10 (controller)
  • worker-01: 192.168.1.11 (worker)
  • worker-02: 192.168.1.12 (worker)

Step 1: Install k0s on controller node

On controller-01:

# Download k0s
curl -sSLf https://get.k0s.sh | sudo sh

# Install as controller only (no workloads)
sudo k0s install controller

# Start k0s
sudo k0s start

# Verify
sudo k0s status

Step 2: Generate worker join token

On controller-01:

# Create worker join token
sudo k0s token create --role=worker > worker-token.txt

# View token (long base64 string)
cat worker-token.txt

Step 3: Join worker nodes

On worker-01 and worker-02:

# Download k0s
curl -sSLf https://get.k0s.sh | sudo sh

# Install as worker with token
sudo k0s install worker --token-file /path/to/worker-token.txt

# Start k0s worker
sudo k0s start

# Verify
sudo k0s status

Step 4: Verify cluster

On controller-01:

# Export kubeconfig
mkdir -p ~/.kube
sudo k0s kubeconfig admin > ~/.kube/config

# Check nodes
kubectl get nodes

# Output:
# NAME          STATUS   ROLES    AGE   VERSION
# worker-01     Ready    <none>   2m    v1.29.1+k0s
# worker-02     Ready    <none>   2m    v1.29.1+k0s

High-Availability Setup (3 Controllers)

For maximum reliability:

Step 1: Install first controller

On controller-01:

curl -sSLf https://get.k0s.sh | sudo sh
sudo k0s install controller
sudo k0s start

Step 2: Generate controller join token

# Create controller join token
sudo k0s token create --role=controller > controller-token.txt

Step 3: Join additional controllers

On controller-02 and controller-03:

curl -sSLf https://get.k0s.sh | sudo sh
sudo k0s install controller --token-file /path/to/controller-token.txt
sudo k0s start

Step 4: Verify HA cluster

# Check etcd members
kubectl exec -n kube-system etcd-controller-01 -- etcdctl member list

# Output shows 3 etcd members

Configuration and Customization

K0s Configuration File

K0s uses a single YAML configuration file:

Generate default config:

# Create default config
sudo k0s config create > k0s.yaml

# View config
cat k0s.yaml

Example k0s.yaml:

apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
  name: k0s
spec:
  api:
    address: 192.168.1.10
    port: 6443

  storage:
    type: etcd
    etcd:
      peerAddress: 192.168.1.10

  network:
    provider: kube-router
    podCIDR: 10.244.0.0/16
    serviceCIDR: 10.96.0.0/12

  controllerManager:
    extraArgs:
      node-monitor-grace-period: 40s

  scheduler:
    extraArgs:
      v: "2"

  extensions:
    helm:
      repositories:
      - name: stable
        url: https://charts.helm.sh/stable
      charts:
      - name: metrics-server
        chartname: metrics-server/metrics-server
        version: "3.11.0"
        namespace: kube-system

Apply custom config:

# Install with custom config
sudo k0s install controller --config k0s.yaml

# Or update existing installation
sudo k0s stop
sudo k0s start --config /etc/k0s/k0s.yaml

Network Provider Options

Kube-router (default):

  • Lightweight, all-in-one networking
  • BGP-based routing
  • Network policy support

Calico:

  • Advanced network policies
  • Better for large-scale deployments

Enable Calico:

spec:
  network:
    provider: calico
    calico:
      mode: vxlan
      vxlanPort: 4789
      vxlanVNI: 4096

Storage Configuration

K0s doesn’t include default storage provisioner. Install one:

Option 1: OpenEBS (recommended for homelab):

# Install OpenEBS
kubectl apply -f https://openebs.github.io/charts/openebs-operator.yaml

# Verify installation
kubectl get pods -n openebs

# Set as default storage class
kubectl patch storageclass openebs-hostpath -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

Option 2: Local path provisioner:

# Install local-path provisioner
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.26/deploy/local-path-storage.yaml

# Set as default
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

Test storage:

# test-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
kubectl apply -f test-pvc.yaml
kubectl get pvc

# Output:
# NAME       STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS
# test-pvc   Bound    pvc-xxx    1Gi        RWO            openebs-hostpath

Deploying Applications

Example 1: Simple Web Application

# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
  type: LoadBalancer
kubectl apply -f nginx-deployment.yaml
kubectl get deployments
kubectl get pods
kubectl get svc nginx

Example 2: Stateful Application (PostgreSQL)

# postgres.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
data:
  POSTGRES_DB: mydb
  POSTGRES_USER: myuser
---
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
type: Opaque
data:
  POSTGRES_PASSWORD: bXlzZWNyZXRwYXNzd29yZA==  # base64: mysecretpassword
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        ports:
        - containerPort: 5432
        envFrom:
        - configMapRef:
            name: postgres-config
        - secretRef:
            name: postgres-secret
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: postgres-storage
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  selector:
    app: postgres
  ports:
  - port: 5432
  clusterIP: None
kubectl apply -f postgres.yaml
kubectl get statefulsets
kubectl get pvc

Example 3: Helm Chart Deployment

# Add Helm repo
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# Install WordPress
helm install my-wordpress bitnami/wordpress   --set wordpressUsername=admin   --set wordpressPassword=password   --set mariadb.auth.rootPassword=secretpassword

# Check status
helm list
kubectl get pods

Ingress and Load Balancing

Install Nginx Ingress Controller

# Install via Helm
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm install ingress-nginx ingress-nginx/ingress-nginx   --namespace ingress-nginx   --create-namespace   --set controller.service.type=NodePort

# Verify installation
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx

Create Ingress Resource

# nginx-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: nginx.homelab.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx
            port:
              number: 80
kubectl apply -f nginx-ingress.yaml

# Add to /etc/hosts
echo "192.168.1.10 nginx.homelab.local" | sudo tee -a /etc/hosts

# Access via browser
curl http://nginx.homelab.local

Install MetalLB for LoadBalancer Services

# Install MetalLB
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml

# Wait for pods
kubectl wait --namespace metallb-system   --for=condition=ready pod   --selector=app=metallb   --timeout=90s

# Configure IP pool
cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: homelab-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.1.100-192.168.1.110
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: homelab-l2
  namespace: metallb-system
spec:
  ipAddressPools:
  - homelab-pool
EOF

Now LoadBalancer services get real IPs!


Cluster Management

Backup and Restore

Backup k0s configuration and etcd:

# Backup k0s data
sudo k0s backup --save-path /backup/k0s-backup.tar.gz

# Backup includes:
# - etcd snapshot
# - k0s configuration
# - certificates

Restore from backup:

# Stop k0s
sudo k0s stop

# Restore backup
sudo k0s restore --from /backup/k0s-backup.tar.gz

# Start k0s
sudo k0s start

Automated backup script:

#!/bin/bash
# /usr/local/bin/k0s-backup.sh

BACKUP_DIR="/backups/k0s"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

sudo k0s backup --save-path "$BACKUP_DIR/k0s-backup-$DATE.tar.gz"

# Keep only last 7 backups
find "$BACKUP_DIR" -name "k0s-backup-*.tar.gz" -mtime +7 -delete

echo "Backup completed: k0s-backup-$DATE.tar.gz"

Schedule with cron:

# Daily backups at 2 AM
0 2 * * * /usr/local/bin/k0s-backup.sh

Upgrading K0s

# Download new k0s version
curl -sSLf https://get.k0s.sh | sudo K0S_VERSION=v1.30.0+k0s.0 sh

# Stop current k0s
sudo k0s stop

# Backup before upgrade
sudo k0s backup --save-path /backup/pre-upgrade-backup.tar.gz

# Upgrade (preserves configuration)
sudo k0s install controller --enable-worker --force

# Start upgraded k0s
sudo k0s start

# Verify upgrade
k0s version
kubectl get nodes

Monitoring with Prometheus

# Install kube-prometheus-stack via Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm install prometheus prometheus-community/kube-prometheus-stack   --namespace monitoring   --create-namespace   --set prometheus.prometheusSpec.retention=30d

# Access Grafana
kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80

# Default credentials: admin / prom-operator

Troubleshooting Common Issues

Issue: Nodes Not Ready

# Check node status
kubectl get nodes

# Describe node for details
kubectl describe node <node-name>

# Check k0s logs
sudo journalctl -u k0scontroller -f
sudo journalctl -u k0sworker -f

# Check kubelet logs
kubectl logs -n kube-system -l component=kubelet

Issue: Pods Stuck in Pending

# Describe pod
kubectl describe pod <pod-name>

# Common causes:
# 1. Insufficient resources
kubectl describe nodes | grep -A 5 "Allocated resources"

# 2. No storage class
kubectl get storageclass

# 3. Taints on nodes
kubectl describe nodes | grep Taints

Issue: Network Problems

# Check CNI pods
kubectl get pods -n kube-system -l app=kube-router

# Test pod-to-pod networking
kubectl run test-1 --image=nicolaka/netshoot --rm -it -- bash
# Inside pod:
curl <another-pod-ip>

# Check DNS
kubectl run test-dns --image=busybox --rm -it -- nslookup kubernetes.default

Issue: High Resource Usage

# Check resource usage
kubectl top nodes
kubectl top pods --all-namespaces

# Set resource limits on deployments
kubectl set resources deployment nginx   --limits=cpu=200m,memory=512Mi   --requests=cpu=100m,memory=256Mi

Best Practices for Homelab K0s

1. Use Namespaces

# Create namespace for each application category
kubectl create namespace media
kubectl create namespace monitoring
kubectl create namespace databases

# Deploy to specific namespace
kubectl apply -f app.yaml -n media

# Set default namespace
kubectl config set-context --current --namespace=media

2. Label Everything

metadata:
  labels:
    app: nginx
    environment: production
    tier: frontend
    managed-by: k0s

3. Use ConfigMaps and Secrets

# Create ConfigMap from file
kubectl create configmap app-config --from-file=config.yml

# Create Secret from literal
kubectl create secret generic db-password --from-literal=password=secret

4. Implement Resource Quotas

apiVersion: v1
kind: ResourceQuota
metadata:
  name: media-quota
  namespace: media
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    persistentvolumeclaims: "10"

5. Regular Backups

# Automated daily backups
# Backup both k0s and persistent data
# Test restore procedure quarterly

Conclusion and Next Steps

K0s delivers production-grade Kubernetes with zero friction - perfect for homelabbers who want to learn Kubernetes without wrestling with installation complexity. By bundling all dependencies into a single binary while maintaining full upstream compatibility, k0s provides the best of both worlds: simplicity and completeness.

Key Takeaways

  • Single binary: Everything needed in one ~250MB file
  • Zero dependencies: No host OS packages required
  • Full Kubernetes: 100% CNCF certified, upstream compatible
  • Flexible deployment: Single-node to multi-node HA clusters
  • Modular architecture: Choose your own CNI, storage, ingress
  • Production-ready: Used in real production environments

When to Use K0s

Perfect for:

  • Learning Kubernetes in homelab environment
  • Building production-like clusters
  • Flexible infrastructure requirements
  • Wanting vanilla Kubernetes experience
  • Multi-node cluster deployments
  • Teams migrating to cloud Kubernetes later

Consider K3s if:

  • Maximum simplicity is priority
  • Edge/IoT deployment
  • Smallest possible footprint
  • Built-in defaults preferred
  1. Start single-node: Deploy controller+worker on one machine
  2. Deploy applications: Practice with deployments and services
  3. Add storage: Install OpenEBS or local-path provisioner
  4. Set up ingress: Install nginx-ingress for HTTP routing
  5. Implement monitoring: Deploy Prometheus/Grafana stack
  6. Scale cluster: Add worker nodes as needed
  7. Enable backups: Automate daily k0s backups

Next Steps

Beginner:

  1. Install single-node k0s cluster
  2. Deploy sample applications
  3. Explore kubectl commands
  4. Set up storage provisioner

Intermediate:

  1. Build multi-node cluster
  2. Configure ingress controller
  3. Deploy stateful applications
  4. Implement monitoring stack

Advanced:

  1. Set up HA control plane (3 controllers)
  2. Implement GitOps with ArgoCD
  3. Configure advanced networking
  4. Build CI/CD pipelines

Resources


Document Version: 1.0 Last Updated: December 16, 2024 Based on: k0s v1.29+ Estimated Completion Time: 75 minutes (basic setup), 8-10 hours (production cluster)

Happy orchestrating!