Back to blog
#kubernetes #k3s #devops #homelab

Why I Settled on k3s After Trying minikube and microk8s

Setting up Kubernetes on a personal VPS sounds straightforward until you actually try it. I went through minikube, microk8s, and finally k3s while building out infrastructure for a hobby project (estreetz.com). Here’s what I hit and why k3s ended up being the only one I could actually run.

minikube: Two Dead Ends

I started with minikube since it’s the most commonly recommended tool for learning Kubernetes.

The first problem was the Docker driver. I was running as root on the VPS (which is typical for fresh cloud instances), and minikube flat out refuses:

❌  Exiting due to DRV_AS_ROOT: The "docker" driver should not be used with root privileges.

Fine. Switch to the none driver, which runs Kubernetes directly on the host:

minikube start --driver none

That hits a different wall:

❌  Exiting due to GUEST_MISSING_CONNTRACK: Sorry, Kubernetes 1.31.0 requires conntrack to be installed in root's path

conntrack is the Linux kernel’s connection tracking module, which Kubernetes uses to manage packet routing between pods and services. The none driver needs it installed as a binary. It’s not in the base Ubuntu image, and installing it is one more thing to manage.

At this point minikube had cost me an hour and left me with nothing running. I moved on.

microk8s: Works, But…

microk8s from Canonical installed fine via snap:

sudo snap install microk8s --classic
microk8s status --wait-ready

It came up, showed the system pods healthy, and had nice built-in addons for DNS, Helm, and cert-manager. The problem was it felt heavy and opinionated. It ships Calico as the CNI, bundles its own kubectl (microk8s kubectl), and relies on snap for lifecycle management.

For a hobby project running on a single low-spec VPS, it also ran noticeably hot at idle. I abandoned it after getting Traefik wired up but before anything else.

k3s: One Binary, Works First Try

k3s is a production-grade Kubernetes distribution from Rancher, packaged as a single binary under 70MB. It strips out cloud-specific storage drivers and replaces them with lightweight alternatives, and uses sqlite instead of etcd for single-node setups.

Install:

curl -Lo /usr/local/bin/k3s \
  https://github.com/k3s-io/k3s/releases/download/v1.26.5+k3s1/k3s
chmod a+x /usr/local/bin/k3s

Start it:

# Foreground (to confirm it starts)
k3s server --write-kubeconfig-mode=644

# Detached
nohup k3s server --write-kubeconfig-mode=644 > k3s.log 2>&1 &

The --write-kubeconfig-mode=644 flag makes the kubeconfig at /etc/rancher/k3s/k3s.yaml readable without sudo, which matters when you’re running kubectl commands as root.

That’s it. No driver issues, no conntrack complaints, no snap. The API server comes up, CoreDNS starts, and you have a working cluster.

Adding Persistent Storage

k3s ships with local-path-provisioner to handle dynamic PVC provisioning, but I had to apply it explicitly once:

k3s kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

After that, any PVC using the local-path StorageClass gets a real volume backed by a directory on the host.

Portainer for a UI

I run Portainer as a StatefulSet in the cluster for a quick visual dashboard. With local-path provisioner in place, the PVC binds immediately:

k3s kubectl get pvc
# NAME                         STATUS   VOLUME                                     CAPACITY   STORAGECLASS
# portainer-data-portainer-0   Bound    pvc-a217b1f1-...                           1Gi        local-path

What k3s Gave Up

k3s replaces etcd with sqlite by default, which means it’s not suitable for high-availability multi-node setups out of the box. For a single-node homelab VPS, that doesn’t matter. It also strips out some alpha features, but nothing I needed.

The tradeoff was worth it. Three hours of fighting minikube and microk8s, and k3s just worked.