Set Up K3s (Lightweight Kubernetes) on a Single VPS

· 6 min read · 21 views · Getwebup

You don't need a three-node cluster and a platform team to run Kubernetes. If you've outgrown a single Docker container but full-blown Kubernetes still feels like overkill for your traffic, k3s gets you a real, API-compatible cluster on one VPS — with a single binary and none of the etcd/control-plane baggage. Here's how to bring it up cleanly, reach it from your laptop, and get past the errors that trip people up on the first boot.

Why k3s instead of full Kubernetes

Upstream Kubernetes wants etcd, multiple control-plane components, and generally more RAM than a single VPS wants to give it. k3s, built by Rancher, strips that down: it ships as one binary, uses SQLite (or an external DB if you scale later) instead of etcd, and runs comfortably in under 512MB of control-plane overhead. It's still real Kubernetes — same kubectl, same manifests, same CRDs — so anything you learn here carries over if you outgrow a single node later.

Before you start

  • 2GB RAM minimum, 4GB if you're running more than three or four small services. K3s itself is light, but your workloads aren't.
  • A 64-bit distro — Ubuntu 22.04/24.04 or Debian 12 are the most tested. Rocky/AlmaLinux work too, just watch the firewalld vs iptables difference below.
  • Root or sudo access, and a non-root user you're comfortable running kubectl as day to day.
  • A domain (optional but recommended) pointed at the VPS's A record if you want TLS via Let's Encrypt on the ingress.

Step 1: Open the ports k3s actually needs

If you're running UFW or CSF, open these before you install — k3s will still start without them, but pods won't schedule properly and you'll waste an hour blaming the wrong thing:

sudo ufw allow 6443/tcp   # Kubernetes API server
sudo ufw allow 80/tcp     # HTTP (Traefik ingress)
sudo ufw allow 443/tcp    # HTTPS (Traefik ingress)
sudo ufw allow from 10.42.0.0/16   # pod network, if using UFW strictly
sudo ufw allow from 10.43.0.0/16   # service network
sudo ufw reload

Those 10.42.0.0/16 and 10.43.0.0/16 ranges are k3s's default pod and service CIDRs. On a single node they mostly matter for pods talking to each other and to the API server — skip the internal rules only if your firewall already trusts loopback and internal bridges.

Step 2: Install k3s

The official install script handles the binary, systemd service, and a working default config in one shot:

curl -sfL https://get.k3s.io | sh -

That's genuinely it for a single-node cluster. It starts a k3s.service, installs kubectl (aliased as k3s kubectl), and writes a kubeconfig to /etc/rancher/k3s/k3s.yaml. Check it came up clean:

sudo systemctl status k3s
sudo k3s kubectl get nodes

You should see one node in Ready state. If it's stuck on NotReady for more than a minute or two, jump to the troubleshooting table below.

If you want a specific version pinned (sensible for anything you'll run in production), set it explicitly instead of pulling latest:

curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="v1.30.4+k3s1" sh -

Step 3: Reach the cluster from your own machine

Running kubectl over SSH every time gets old fast. Copy the kubeconfig down and point kubectl at the VPS's public IP instead of 127.0.0.1:

# on your laptop
scp root@your-vps-ip:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s-vps.yaml
sed -i '' 's/127.0.0.1/your-vps-ip/' ~/.kube/k3s-vps.yaml   # macOS sed
export KUBECONFIG=~/.kube/k3s-vps.yaml
kubectl get nodes

The kubeconfig file has full cluster-admin rights baked in, so treat it like a root password — don't commit it, don't email it, and rotate the node token if you think it's leaked (/var/lib/rancher/k3s/server/node-token on the VPS).

Step 4: Deploy something and expose it

K3s ships with Traefik as its ingress controller by default, so you don't need to install one separately. A minimal deploy-and-expose looks like this:

kubectl create deployment hello --image=nginxdemos/hello --port=80
kubectl expose deployment hello --port=80
kubectl apply -f - <

Point hello.yourdomain.com at the VPS's A record and it should resolve within a few minutes. For TLS, the simplest route on k3s is installing cert-manager and letting it issue Let's Encrypt certs against your Traefik ingress — worth its own setup, but a five-minute Helm install once you're at this point:

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

Common startup errors and how to fix them

SymptomLikely causeFix
Node stuck on NotReadyCNI (flannel) can't bind, usually a firewall or swap issueCheck sudo journalctl -u k3s -f for flannel errors; confirm ports above are open
connection refused on port 6443UFW/CSF blocking the API port, or k3s still initializingRe-check firewall rules; give it 60–90 seconds on first boot before panicking
Pods stuck in PendingNot enough memory/CPU requests available, or a taint on the nodekubectl describe pod <name> to see the scheduler's actual complaint
Traefik ingress not routingDNS not pointed at the VPS yet, or wrong ingress.class annotationConfirm dig hello.yourdomain.com resolves to the VPS IP; re-check the annotation matches your k3s version's default class
k3s.yaml permission deniedTrying to read the kubeconfig as a non-root usersudo chmod 644 /etc/rancher/k3s/k3s.yaml, or better, copy it out and lock it down locally

Backups and upgrades

Single-node k3s uses an embedded SQLite database by default, which lives at /var/lib/rancher/k3s/server/db. Back that directory up the same way you'd back up any other stateful directory on the VPS — a nightly rsync or snapshot job covers it. K3s also takes automatic etcd-style snapshots if you switch to its embedded etcd mode later, but for a single node the filesystem backup is simpler and sufficient.

Upgrades are a re-run of the install script with a pinned version:

curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="v1.31.2+k3s1" sh -

Take a VPS snapshot first. A failed upgrade on a single-node cluster means downtime for everything on it, not just the one app you were touching.

Prevention checklist

  • Pin your k3s version in production instead of tracking latest — surprise upgrades break ingress annotations more often than you'd think.
  • Set resource requests/limits on every deployment; without them, one runaway pod can starve everything else on the node.
  • Keep the kubeconfig off any machine that isn't yours — it's cluster-admin by default, with no scoping.
  • Monitor disk space separately from Kubernetes metrics. A full disk takes down k3s's SQLite store just as fast as any other database.
  • If you're running anything customer-facing, put a second VPS in the mix before you need it — k3s supports multi-server HA mode, and migrating early is a lot less stressful than migrating during an outage.

Questions people actually ask