All posts

k3s on a single VPS

k3s v1.37.2+k3s1 on one node: Traefik disabled, kubeconfig mode 600, one whoami Deployment, Caddy proxies to NodePort. Idle 1371.3 MiB k3s.service.

Written by NodeRings Inc.

NodeRings Inc. writes these notes. The company runs the marketplace and the agent that stays on the provider’s side of the hypervisor.

k3s v1.37.2+k3s1 held 1371.3 MiB idle on this lab host after the node was Ready and one whoami pod answered on NodePort 30080. That is cgroup memory.current for k3s.service, the median of twenty samples taken 30 seconds apart. The cgroup includes containerd and the running pods (CoreDNS, metrics-server, local-path, whoami). The k3s server process itself showed 600.3 MiB RSS and 538.5 MiB PSS, and CPU for that process sat at 4.8 percent median. Traefik was not installed. /etc/rancher/k3s/k3s.yaml was mode 600.

k3s is a lightweight Kubernetes distribution from Rancher: one binary (or install script) that brings up a control plane and kubelet suitable for edge and single-node labs. You get Deployments, Services, and the usual kubectl workflows without a full kubeadm control plane footprint. On one VPS it is useful for learning or hosting a few workloads; it is still Kubernetes, so RAM starts near a gigabyte before your apps.

The quick-start path is curl | sh with Traefik on 80/443 and the API on every interface at 6443. Competing guides stop at kubectl get nodes. Pin INSTALL_K3S_VERSION and pass --disable traefik, so host Caddy keeps 80 and 443. Deploy one workload, measure the unit, and close 6443 and 30080 on the panel firewall.

ReadingValue
k3s.service memory.current1371.3 MiB, median of 20
k3s server RSS600.3 MiB
k3s server PSS (smaps)538.5 MiB
CPU percent (k3s server)4.8, median
NodeReady, role control-plane
Traefik pods / Servicesnone
whoamiHTTP 200 on 127.0.0.1:30080
Skip thisWhat this run did
Floating install channelINSTALL_K3S_VERSION=v1.37.2+k3s1
Default Traefik on 80/443--disable traefik
World-readable kubeconfig--write-kubeconfig-mode 600
Stopping at get nodesOne Deployment + NodePort proof
Opening 6443 and 30080 on the panelLeft both closed; Caddy on 80/443
Bar chart. Idle k3s.service memory.current 1371.3 MiB, k3s server RSS 600.3 MiB, process PSS 538.5 MiB.
Idle after Ready and one whoami pod. More Deployments sit above this. The cgroup includes containerd and pods.

How do you operate a single-node cluster day to day?

Disable Traefik when the host already runs Caddy or another ingress. Expose apps with NodePort or hostPort and proxy from Caddy, or install an Ingress controller deliberately later. Keep k3s.yaml mode 600 and copy it only over SSH. Prefer local-path PVCs on the VPS disk; treat the node as cattle only if you have etcd/datastore backups. kubectl upgrades follow the k3s version pin; do not float INSTALL_K3S_VERSION.

Single-node has no HA: a reboot is a control-plane outage. That is acceptable for a lab or a personal app tier. For production HA you need three nodes and a different post. Close 6443 on the panel firewall when you only administer via SSH tunnel.

How do you install k3s on one VPS?

curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.37.2+k3s1 sh -s - \
  --write-kubeconfig-mode 600 \
  --disable traefik \
  --tls-san 127.0.0.1

This lab also set --node-name k3s-lab. The installer writes /etc/systemd/system/k3s.service with those flags and starts the unit. The binary should print the pinned version:

k3s -v
# k3s version v1.37.2+k3s1 (...)
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
kubectl get nodes -o wide

You want Ready and role control-plane. ls -l /etc/rancher/k3s/k3s.yaml should show -rw-------.

k3s.service ExecStart with write-kubeconfig-mode 600, disable traefik, tls-san 127.0.0.1.
Pinned with `INSTALL_K3S_VERSION`. Traefik is disabled in the unit.
kubectl get nodes showing k3s-lab Ready control-plane v1.37.2+k3s1 and kubeconfig mode 600.
Single node Ready. kubeconfig is mode 600.

Why turn Traefik off?

Traefik and Caddy both want 80 and 443. If the host already runs Caddy, or you are about to install it, the two servers collide on those ports. Disable Traefik at install time and keep Flannel, CoreDNS, metrics-server, and local-path.

kubectl get pods -A | grep -i traefik || echo no traefik pods
kubectl get svc -A | grep -i traefik || echo no traefik svc

This lab printed no traefik pods and no traefik svc. kube-system still had CoreDNS, metrics-server, local-path-provisioner, and a completed gateway-api CRD helm job.

Install command with --disable traefik and kubectl showing no traefik pods or services.
80 and 443 stay free for host Caddy.
kubectl get pods -A with CoreDNS, metrics-server, local-path, and whoami Running.
System pods Running, whoami in default, and no Traefik.

How do you prove one Deployment?

apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami
spec:
  replicas: 1
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
      - name: whoami
        image: traefik/whoami:v1.11.0
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: whoami
spec:
  type: NodePort
  selector:
    app: whoami
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30080
kubectl apply -f whoami.yaml
kubectl rollout status deploy/whoami
curl -sS http://127.0.0.1:30080/

This lab returned a whoami body with Hostname: whoami-… and HTTP 200. NodePort is DNAT through kube-proxy, so ss may not show a process on 30080 even when curl works. Leave 30080 closed on the panel firewall. Open 80 and 443 for Caddy only, and keep 6443 closed unless you have a separate admin path.

kubectl apply whoami and curl to 127.0.0.1:30080 returning Hostname whoami.
NodePort 30080 on loopback. The firewall keeps it off the public IP.

Where does Caddy go?

app.example.com {
	reverse_proxy 127.0.0.1:30080
}

Host Caddy terminates TLS and proxies to the NodePort on loopback. TLS setup is in the Caddy HTTPS post. Ingress and Traefik stay out of this install.

Caddyfile `reverse_proxy` to 127.0.0.1:30080 for app.example.com.
Public HTTPS on 443. whoami stays on the cluster NodePort.

How do you back up the datastore?

Default k3s uses sqlite under /var/lib/rancher/k3s/server/db. Stop the unit, archive db, start again:

systemctl stop k3s
tar -czf k3s-server-db.tgz -C /var/lib/rancher/k3s/server db
systemctl start k3s

On this lab the archive was about 3.6 MB after Ready plus one whoami Deployment. Point Restic at that tarball. If you later enable embedded etcd, use k3s etcd-snapshot instead.

Stop k3s, tar server/db into k3s-server-db.tgz at 3.6M, start again.
Stopped copy of the sqlite datastore.

When is one-node k3s the wrong tool?

If you only need Compose, use Compose. k3s pays for itself when you want Kubernetes manifests, Helm charts, or a path to more nodes later. On a 2 GB VPS the idle tax is large relative to a single binary app. Keep kubeconfig private. Upgrade k3s with the install script and the same flags you used on day one (--disable traefik here so host Caddy keeps 80/443). If you later keep Traefik but put MetalLB or hostNetwork ingress in front, also pass --disable=servicelb; otherwise ServiceLB claims 80/443 on every node (k3s networking, hilars.dev). Persist flags in /etc/rancher/k3s/config.yaml so reinstalls match. On a true single-node box, remove the control-plane NoSchedule taint if workloads stay Pending after install.

FAQ

Why is the cgroup larger than process RSS? k3s.service includes the server process, containerd, and the pods it runs. The RSS and PSS numbers are the k3s server process.

Docker on the same host? Possible. This lab already had Compose stacks, and k3s uses its own containerd. Do not point both at the same containerd socket.

Multi-node? This run is one VPS and one control-plane node.

ServiceLB / svclb? LoadBalancer Services use Klipper. This proof used NodePort only.

Uninstall? /usr/local/bin/k3s-uninstall.sh removes the unit and data. Run it only when you mean it.

6443? The API listens on *:6443. Restrict it on the panel firewall, and crop the kubeconfig contents in any screenshot.