K3s for One VPS: E Get Sense Pass Docker Compose?
K3s gives you the real Kubernetes API on one VPS, but e dey use RAM and port 80 fit collide during install. See when Docker Compose still better.
Wetin k3s be, and wetin one node of am dey give you
k3s na complete Kubernetes distribution wey dem package as one binary, and if you run am for one VPS, you get the real Kubernetes API without three-machine control plane. E be certified Kubernetes distribution, so any manifest wey apply here go still apply for managed cluster later. Installation na one command and e dey take about one minute. Wetin you pay na memory wey your applications no fit use again, plus some failure modes wey no dey happen with Docker Compose.
SUSE build k3s for edge sites and small installations, and every difference from upstream Kubernetes dey make am smaller. Default datastore na sqlite behind shim wey dem call kine, no be etcd, so you no get etcd quorum to maintain. containerd dey embedded inside the binary instead of installing am separately. The same binary still ships CoreDNS for cluster DNS, Traefik as ingress controller, ServiceLB (wey dem also call klipper-lb) so LoadBalancer services fit work without cloud provider behind dem, local-path provisioner for persistent volumes, metrics-server, and flannel for pod networking. All of dem dey start by default. Na why the port collision wey we describe below dey be the commonest first problem for VPS wey don already dey run something.
When one k3s node make sense
Use this rule. Run k3s when na Kubernetes API you want: you dey learn Kubernetes for machine wey you control, or the software wey you want only publish Helm chart. Manifest portability still count, because Deployment wey you write here fit move go managed cluster unchanged. Run Docker Compose when na the applications you want. Compose start the same containers with far fewer moving parts, and a Compose file for VPS dey easier to read one year later than directory of manifests.
Make you clear about wetin one node no give you.
- No high availability. When VPS reboot, every workload stop. Kubernetes fit reschedule pod go another node, but no other node dey.
- No rolling update wey keep service running, unless the application fit handle two replicas for one machine wey dey share one volume.
- Storage wey dey tied to the machine, because of the reason wey the local-path section below explain.
- Control plane wey dey use around one gigabyte of RAM whether you deploy anything or not.
None of this mean say k3s na bad choice. E mean say e na bad choice for the reason people usually give, wey be reliability. If wetin you really want na several machines so you fit build real multi-node cluster, make you decide that one first: Proxmox for your own hardware against rented VPS go settle where the nodes go come from before k3s settle wetin go run on dem.
Wetin k3s dey cost for RAM and CPU before you deploy anything
The k3s project dey publish figures wey dem measure, no be estimates. Read dem carefully, because the number wey people dey quote no be idle k3s.
The data behind this chart
[
{
"label": "Server, sqlite datastore",
"ram_mb": "1,596",
"cpu_percent_of_one_core": 6
},
{
"label": "Server, embedded etcd",
"ram_mb": "1,606",
"cpu_percent_of_one_core": 6
},
{
"label": "Agent node only",
"ram_mb": "275",
"cpu_percent_of_one_core": 3
}
]A server node for that test use 1,596 MB of RAM for the 95th percentile and about 6 percent of one core. Na published figures be these, no be measurements from this guide, and the test run k3s v1.26.5 with all packaged components enabled plus Prometheus and Grafana monitoring stack. So, the number include real workload, no be empty cluster. When dem replace sqlite with embedded etcd, e move to 1,606 MB. An agent node, wey dey run kubelet and containerd without control plane, use 275 MB. The documented minimum for a server na 2 cores and 2 GB of RAM. This minimum cover k3s and the packaged components before your workloads.
The practical meaning be say: for 2 GB VPS, the control plane and bundled add-ons go leave you with very little memory, and the first thing wey go happen under pressure na kubelet evicting pods. 4 GB na comfortable minimum for one node with a few small services. Measure your own box instead of trusting any published figure, including this one.
free -h
sudo k3s kubectl top node
sudo k3s kubectl get pods -ARun free -h before you install, and run am again after every pod for kube-system don read Running. The difference na wetin the control plane dey cost for your hardware. k3s kubectl top node dey return error: Metrics API not available for the first minute or two after installation, because metrics-server never scrape anything yet. That one no be fault. If you dey size one box for this work and other work at the same time, the calculation for how to size RAM and CPU for VPS still apply here without change.
Install k3s wey pinned to one release, no be latest
The quick start line wey everybody dey copy go take whichever release stable channel point to on the day wey you run am. For machine wey you plan to keep, pin am. k3s dey publish one channel for each Kubernetes minor version, so INSTALL_K3S_CHANNEL=v1.36 go follow patch releases inside v1.36 and e no go jump minor version by itself. As of August 2026, stable channel dey point to v1.36.3+k3s1.
Write the config file first, then install. k3s dey read /etc/rancher/k3s/config.yaml when e start, so anything wey dey inside am go apply for the first boot and every boot after that.
sudo mkdir -p /etc/rancher/k3s
sudo tee /etc/rancher/k3s/config.yaml >/dev/null <<'EOF'
tls-san:
- k3s.example.com
EOF
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=v1.36 sh -To pin one exact release instead of channel, use INSTALL_K3S_VERSION=v1.36.3+k3s1. The plus sign na part of the tag. Then check say e don start properly.
k3s --version
sudo systemctl status k3s
sudo k3s kubectl get node
sudo k3s kubectl get pods -Akubectl get node suppose list one node with STATUS Ready within about thirty seconds, and every pod for kube-system suppose reach Running or Completed. If node remain for NotReady, e usually mean say container runtime never start, so read sudo journalctl -u k3s -n 100 --no-pager. For unusual VPS image, run sudo k3s check-config before you debug anything else: e go report kernel features wey dey missing, and this answer dey faster pass reading logs.
Port 80 don already dey use, and wetin you need give up to fix am
Na this failure dey catch people for VPS wey don already dey serve something. The installation go succeed. But Traefik no go ever get address, and the site wey you dey run before go continue to work. So nothing go look broken until you try reach an ingress.
The mechanism be say: the bundled Traefik chart dey create Service of type LoadBalancer for ports 80 and 443. ServiceLB dey answer that request by creating DaemonSet of small pods, wey name start with svclb-, and dem go claim those port numbers as hostPort for every node. hostPort dey publish the container port directly for the node own network namespace, just like docker run -p 80:80 dey do. If nginx, Caddy, Apache, or another container don already hold port 80, the kernel no go give out the same port twice. So scheduler no get place to put the pod.
sudo k3s kubectl -n kube-system get pods
sudo k3s kubectl -n kube-system get svc traefik
sudo ss -lntp '( sport = :80 or sport = :443 )'You go see say the svclb pod dey Pending, and the service no get external address:
svclb-traefik-8f2c1a-r6k9x 0/2 Pending 0 3m
traefik LoadBalancer 10.43.62.11 <pending> 80:31480/TCP,443:30219/TCPRunning kubectl -n kube-system describe pod svclb-traefik-... go show the cause directly:
0/1 nodes are available: 1 node(s) didn't have free ports for the requested pod ports.ss -lntp go tell you which process dey hold the port. More than one solution dey, and each one get wetin you go give up.
Give the ports to k3s. Stop and disable the web server wey dey already run, then make Traefik own 80 and 443. This na the correct choice when the VPS go be k3s box only, and everything wey you dey serve go move behind an Ingress.
Disable ServiceLB and keep your existing proxy. Install with --disable=servicelb. Service of type LoadBalancer go still allocate a NodePort, so Traefik go remain reachable through high port like 31480, and your nginx or Caddy go proxy to 127.0.0.1:31480. Wetin you give up na the external address: the service go report <pending> forever. E fit look like fault, but na choice you make.
Disable Traefik and route with your own proxy. Install with --disable=traefik. You no go get ingress controller again, so Ingress objects no go do anything: dem go sit for the API without any controller wey dey watch dem. This dey okay when you route from a host proxy to NodePorts, and na the honest choice if you already know how you want HTTP to work. If you never decide wetin suppose dey in front, settle the choice between nginx, Caddy and Traefik as reverse proxy before you disable anything.
Both flags suppose dey on the installer, or inside the config file:
tls-san:
- k3s.example.com
disable:
- traefik
- servicelbEditing that file after installation and running sudo systemctl restart k3s go work too, because --disable no be only to skip one component during installation. E also deletes component wey don already deploy, so the change go take effect for cluster wey dey run.
If you want keep Traefik but change how the chart dey configured, no edit /var/lib/rancher/k3s/server/manifests/traefik.yaml. k3s dey rewrite that file with defaults every time e starts. Add separate file for the same directory instead, because anything inside /var/lib/rancher/k3s/server/manifests dey apply automatically at startup and whenever e change for disk.
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
ports:
web:
forwardedHeaders:
trustedIPs:
- 10.0.0.0/8That example dey set one Traefik chart value: trusted proxy addresses. The same mechanism fit set any other value wey the chart exposes, including its ports.
Persistent storage for one node
k3s dey ship with one default StorageClass wey dem call local-path, and Rancher's local-path provisioner dey back am. Any PersistentVolumeClaim wey no get storageClassName go use am. Volumes dey live for /var/lib/rancher/k3s/storage, with one subdirectory for each volume, for the node own disk.
sudo k3s kubectl get storageclass
sudo k3s kubectl get pvc -A
sudo ls -l /var/lib/rancher/k3s/storageTwo things follow from the fact say e dey for "the node own disk", and both fit cause problem later, no be today.
The StorageClass dey use volumeBindingMode: WaitForFirstConsumer, so new PVC go remain Pending until pod actually mount am. kubectl describe pvc go print:
waiting for first consumer to be created before bindingThis one normal. So if you create PVC by itself and wait, e no go ever clear Pending state.
After volume don bind, e go get node affinity for the node wey create am. This one pin every pod wey dey use that claim to the same node for the whole life of the volume. For one node, you no go notice am. If you add second node later, pod wey refuse move fit look like scheduler bug until you run kubectl get pv -o yaml and discover the hostname for nodeAffinity.
Na you dey responsible for backups. If you rebuild the VPS, that directory go comot. The uninstall script below go delete am too. Back up /var/lib/rancher/k3s/storage, plus the sqlite datastore for /var/lib/rancher/k3s/server/db/state.db after you stop the service, because na live database. Another option na to treat the cluster as disposable and keep every manifest for git.
Ingress and TLS
As Traefik still dey enabled, standard Ingress object na all wey you need.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
ingressClassName: traefik
tls:
- hosts:
- hello.example.com
secretName: hello-tls
rules:
- host: hello.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello
port:
number: 80Certificates no dey show by demself. The usual solution na cert-manager, wey you install from the manifest wey dem publish, plus one ClusterIssuer. Version v1.21.1 na the current one as of August 2026.
sudo k3s kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.21.1/cert-manager.yamlapiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
email: you@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- http01:
ingress:
ingressClassName: traefikHTTP-01 challenge mean say ACME (automatic certificate management environment) server go connect to http://hello.example.com/.well-known/acme-challenge/... from public internet. So DNS A record must don point to the VPS already, and port 80 must reach Traefik. If you disable ServiceLB and put your own proxy for front, that proxy must forward the challenge path too. If e no do am, cert-manager go stop and show this for the Challenge object:
Waiting for HTTP-01 challenge propagation: wrong status code '404', expected '200'Use sudo k3s kubectl describe certificate hello-tls and sudo k3s kubectl get order,challenge -A to monitor certificate issuance.
The kubeconfig, and why the API server stays private
k3s dey write admin credentials to /etc/rancher/k3s/k3s.yaml. root own the file, and by default e dey write am with mode 600. E hold client certificate wey get cluster-admin rights, so anybody wey fit read am don control the cluster.
sudo k3s kubectl get node
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
kubectl get nodeA separately installed kubectl wey no get KUBECONFIG set go fail with The connection to the server localhost:8080 was refused - did you specify the right host or port? because e go fall back to default wey no get anything to do with k3s. Set KUBECONFIG, or use sudo k3s kubectl, wey go read the correct file by itself.
You go see say dem dey recommend --write-kubeconfig-mode 644 so normal user fit run kubectl. Understand wetin this one dey do: e make cluster-admin credential readable by every local account for the machine. For single-admin box, this fit be acceptable trade-off. For shared box, e no safe. If you copy the file instead, na one user go get access without exposing am:
mkdir -p ~/.kube
sudo install -o $USER -g $USER -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/configThe server: line for that file dey read https://127.0.0.1:6443. To use kubectl from your laptop, no open 6443 to the internet. Public Kubernetes API na standing target, and exposed one fit make small clusters end up mining cryptocurrency for another person. Tunnel am through SSH and leave the address for the file as e be:
ssh -N -L 6443:127.0.0.1:6443 user@your-vps
KUBECONFIG=./k3s.yaml kubectl get nodeIf you must reach the API through private network address, install with tls-san wey list that name or address, then edit the copied file server: line make e match. If SAN (subject alternative name) entry no dey, kubectl go reject the connection:
x509: certificate is valid for 127.0.0.1, 10.43.0.1, not 203.0.113.10The documented inbound ports for cluster na TCP 6443 for the API, UDP 8472 for flannel VXLAN between nodes, and TCP 10250 for kubelet metrics. For single node, none of dem need make e open to the internet.
containerd no be Docker
k3s dey run im own embedded containerd, and e no share image store with Docker. Image wey you just build with docker build no dey visible to k3s, so the pod fails with ErrImagePull even though docker images list am. Import am explicitly:
docker save myapp:0.1 | sudo k3s ctr images import -
sudo k3s crictl imagesThen avoid :latest tag for that container, because :latest default to imagePullPolicy of Always and kubelet go registry anyway. Any other tag default to IfNotPresent, wey dey use the image wey you import. Running both for one VPS dey work, and e good make you know say normal Docker installation for VPS and k3s each keep their own image store and their own iptables rules for the same machine.
How to remove k3s
Installer dey write uninstall script. Partial removal no dey, and you no fit undo am.
sudo /usr/local/bin/k3s-uninstall.shE go stop and remove the service, delete the datastore, delete persistent volume data, remove the node configuration, and remove the tools wey installer add. For agent node, the script na k3s-agent-uninstall.sh instead. First copy anything wey dey under /var/lib/rancher/k3s/storage comot from the machine, because that directory go delete too. Afterward, use ip link show and sudo ss -lntp check say nothing still dey hold ports or interfaces. Any leftover cni0 or flannel.1 interface go clear for the next reboot.
To decide say one Kubernetes node na more machinery than the job need na normal outcome; e no mean say you fail. Moving those workloads back to Compose usually fit happen within one afternoon.
Failure modes and the strings you will see
Node NotReady, or k3s dey restart for loop. Read sudo journalctl -u k3s -n 200 --no-pager first. For small VPS, common cause na kernel out-of-memory killer wey dey kill the process. You go see am for dmesg as line wey name k3s-server. The documented 2 GB minimum na real minimum.
Pod stuck for Pending. kubectl describe pod go name the problem every time. Insufficient memory or Insufficient cpu mean say node no get space again. didn't have free ports na the hostPort collision wey we mention above. waiting for first consumer for PVC mean say WaitForFirstConsumer dey work as e suppose.
ImagePullBackOff. Either the tag no dey for any registry wey node fit reach, or you build the image with Docker but never import am into containerd.
Traefik dey answer, but app no dey. Response body of 404 page not found come from Traefik itself. E mean say request arrive, but no router match am. Check say Ingress host match the name wey you type, and say ingressClassName na traefik.
Cluster DNS fail while host resolve fine. Check CoreDNS with sudo k3s kubectl -n kube-system logs -l k8s-app=kube-dns. Message like plugin/loop: Loop ... detected for zone "." go stop CoreDNS from starting. E happen because CoreDNS dey forward request go resolver wey dey forward am back to CoreDNS. Na wetin loopback address for /etc/resolv.conf dey cause. Point k3s to the real upstream file with --resolv-conf /run/systemd/resolve/resolv.conf.
FAQ
k3s worth running for one VPS?
E worth am when na Kubernetes API you want: to learn am for machine wey you control, keep deployments portable as manifests, run software wey only publishes Helm chart, or build something wey you go later move go managed cluster. E no worth am when na only containers you want dey run, because Docker Compose dey do that with much less maintenance and roughly one gigabyte more free RAM. One node no give you high availability, so reliability no be reason to choose am.
Why my k3s LoadBalancer service dey stay Pending?
ServiceLB dey create svclb- pods wey claim the service ports as hostPort for the node, so dem fit only schedule where those ports dey free. If nginx or another proxy already dey hold port 80, the pod go stay Pending and the service no go ever receive external address. kubectl -n kube-system describe pod svclb-... dey report 0/1 nodes are available: 1 node(s) didn't have free ports for the requested pod ports. Free the port, or reinstall with --disable=servicelb and proxy to the NodePort wey the service still allocates.
How much RAM k3s need for VPS?
The documented minimum for server node na 2 cores and 2 GB, and that one cover k3s plus the components wey e package before your workloads. The project own profiling measure server node at 1,596 MB with monitoring stack running on am, so treat 2 GB as the floor and 4 GB as the first size wey one node fit run comfortably. Measure your own box with free -h before the install and again after every pod for kube-system read Running.
I fit run Docker and k3s for the same VPS?
Yes, and dem dey stay separate. k3s dey use im own embedded containerd, so image wey you build with docker build no go visible to am until you run docker save myapp:0.1 | sudo k3s ctr images import -. Each one also dey write im own iptables rules and im own bridge networks. Watch the memory total, because Docker plus k3s plus your containers no go fit inside 2 GB box.
How I fit remove k3s completely?
Run sudo /usr/local/bin/k3s-uninstall.sh for server node, or sudo /usr/local/bin/k3s-agent-uninstall.sh for agent. E go stop the service, delete the datastore, delete persistent volume data under /var/lib/rancher/k3s/storage, and remove the bundled tools. Copy any data wey you want keep out of the box first, because no undo dey. Any leftover cni0 or flannel.1 network interface go disappear for the next reboot.