Install K3s Lightweight Kubernetes on VPS
K3s is a lightweight, CNCF-certified Kubernetes distribution designed for resource-constrained environments like VPS servers with 1–2 GB of RAM. Unlike full Kubernetes which requires at least 2 GB just for the control plane, K3s packages the API server, scheduler, controller manager, and an embedded database into a single binary under 100 MB, and starts up with as little as 512 MB of RAM. This guide walks you through installing a K3s single-node cluster on Ubuntu 22.04/24.04, deploying your first application, and configuring the Traefik ingress controller that ships with K3s out of the box.
System Requirements
K3s runs on very modest hardware, but for a stable production setup these specs are recommended:
| Component | Minimum | Recommended |
|---|---|---|
| RAM | 512 MB | 1–2 GB+ |
| CPU | 1 vCore | 2 vCores+ |
| Disk | 2 GB | 20 GB+ |
| OS | Ubuntu 20.04+ | Ubuntu 22.04 / 24.04 |
# Check kernel version (requires 4.15+)
uname -r
# Check available RAM
free -h
# Check CPU cores
nproc
Install K3s (Server / Control Plane)
K3s installs with a single command. The script downloads the binary, sets up a systemd service, and starts the cluster automatically:
# Install K3s (latest stable)
curl -sfL https://get.k3s.io | sh -
# Check that the service is running
sudo systemctl status k3s
# Verify the node is Ready
sudo kubectl get nodes
After installation, K3s writes a kubeconfig to /etc/rancher/k3s/k3s.yaml. Copy it so you can use kubectl as a regular user:
# Set up kubeconfig for your user
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
export KUBECONFIG=~/.kube/config
# Confirm the cluster is reachable
kubectl cluster-info
Add Worker Nodes (Multi-Node Cluster)
To scale beyond a single node, retrieve the join token from the server and install K3s in agent mode on each worker:
# On the server node — get the token
sudo cat /var/lib/rancher/k3s/server/node-token
# On each worker node — install as agent
curl -sfL https://get.k3s.io | K3S_URL=https://SERVER_IP:6443 \
K3S_TOKEN=NODE_TOKEN sh -
# Back on the server — verify all nodes are Ready
kubectl get nodes -o wide
ufw allow 6443/tcp and ufw allow 10250/tcp.Deploy Your First Application
Test the cluster by deploying Nginx with two replicas:
# Create a deployment
kubectl create deployment nginx --image=nginx:alpine --replicas=2
# Expose it as a NodePort service
kubectl expose deployment nginx --port=80 --type=NodePort
# Check pods and service
kubectl get pods
kubectl get svc nginx
Note the assigned NodePort (e.g. 30xxx) and visit http://VPS_IP:NodePort to confirm Nginx is serving traffic.
Use the Built-in Traefik Ingress
K3s automatically installs Traefik as an ingress controller, listening on ports 80 and 443:
# Confirm Traefik is running
kubectl get pods -n kube-system | grep traefik
# View the Traefik service
kubectl get svc -n kube-system traefik
Create an Ingress resource to route traffic to the Nginx deployment above:
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: nginx.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
EOF
nginx.yourdomain.com DNS to your VPS IP first. Traefik will automatically route requests to the Nginx pods once the DNS propagates.Common kubectl Commands
# List all resources in the current namespace
kubectl get all
# View pod logs
kubectl logs POD_NAME
# Open a shell inside a container
kubectl exec -it POD_NAME -- /bin/sh
# Scale a deployment
kubectl scale deployment nginx --replicas=3
# Delete a deployment
kubectl delete deployment nginx
# Apply a manifest file
kubectl apply -f app.yaml
# List pods across all namespaces
kubectl get pods -A
Persistent Storage with Local Path Provisioner
K3s ships with a Local Path Provisioner for simple persistent volumes:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
EOF
# Check PVC status
kubectl get pvc
Upgrading and Uninstalling K3s
# Upgrade K3s to the latest version
curl -sfL https://get.k3s.io | sh -
# Uninstall K3s from the server node
/usr/local/bin/k3s-uninstall.sh
# Uninstall from an agent node
/usr/local/bin/k3s-agent-uninstall.sh
Verify K3s Is Working Correctly
# Check K3s service status
sudo systemctl status k3s
# Verify node status (should show Ready)
kubectl get nodes
# List all system pods in kube-system
kubectl get pods -n kube-system
# Confirm the API server responds
kubectl cluster-info
# Display K3s version
k3s --version
NotReady, check the logs with sudo journalctl -u k3s -f. The most common cause is the CNI plugin still initialising — wait 1–2 minutes and check again before taking further action.Troubleshooting Common Issues
Error: "node not found" or node stuck at NotReady
The CNI network plugin hasn't finished initialising, or a firewall is blocking VXLAN traffic between nodes.
# Read recent K3s logs
sudo journalctl -u k3s --no-pager | tail -50
# Check the Flannel CNI pod (default CNI in K3s)
kubectl get pods -n kube-system | grep flannel
# Open UDP 8472 for VXLAN (required for multi-node setups)
sudo ufw allow 8472/udp
Error: "x509: certificate signed by unknown authority"
The IP or hostname in your copied kubeconfig doesn't match the certificate's SAN entries.
# View the kubeconfig K3s generated (points to localhost)
sudo cat /etc/rancher/k3s/k3s.yaml
# Replace 127.0.0.1 with your VPS public IP for remote access
sed 's/127.0.0.1/VPS_PUBLIC_IP/g' /etc/rancher/k3s/k3s.yaml > ~/.kube/config
Error: "dial tcp: connection refused" on port 6443
The K3s API server isn't running, or port 6443 is blocked by the firewall.
# Check if K3s is listening on 6443
sudo ss -tlnp | grep 6443
# Restart K3s
sudo systemctl restart k3s
# Allow the port through the firewall
sudo ufw allow 6443/tcp
Pods being evicted / "Insufficient memory"
The VPS doesn't have enough RAM to schedule the requested pods. K3s evicts pods when node memory falls below eviction thresholds.
# Check allocated resources on the node
kubectl describe node | grep -A5 "Allocated resources"
# Add a 2 GB swap file to buy more headroom
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab
Traefik not receiving traffic on port 80/443
The Traefik LoadBalancer service stays in Pending because standard VPS providers don't supply an external load balancer IP.
# Check Traefik service status
kubectl get svc -n kube-system traefik
# Patch the service with your VPS public IP
kubectl patch svc traefik -n kube-system \
-p '{"spec":{"externalIPs":["VPS_PUBLIC_IP"]}}'
Frequently Asked Questions
How does K3s differ from full Kubernetes?
K3s is CNCF-certified, meaning its API and resources are identical to upstream Kubernetes. The key differences are that K3s uses SQLite instead of etcd on a single node (saving around 200 MB of RAM), bundles multiple components into one binary, and removes cloud-provider plugins that aren't needed on a VPS. The binary is roughly 10x smaller and boots much faster. For large production clusters that need etcd's write throughput, full Kubernetes is still the right choice.
Can I use Helm charts with K3s?
Yes — any Helm chart compatible with Kubernetes works on K3s without modifications. K3s also includes a built-in Helm Controller that lets you deploy charts via a HelmChart CRD without even installing the Helm CLI. If you prefer the CLI, install it the usual way: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash.
Is K3s suitable for production?
Yes. K3s is used in production by many organizations, particularly for edge computing, IoT gateways, and small-scale Kubernetes clusters on VPS. For high availability, you can run K3s with embedded etcd across three or more server nodes: curl -sfL https://get.k3s.io | K3S_TOKEN=SECRET sh -s - server --cluster-init. Additional server nodes then join with --server https://FIRST_SERVER:6443.