Full Kubernetes is overkill for a single VPS. k3s is a CNCF-certified lightweight Kubernetes distribution that installs in 30 seconds, uses ~512 MB RAM, but remains 100% compatible with kubectl and Helm. It's ideal for developers learning Kubernetes or running production workloads on a budget VPS. In this comprehensive guide, we'll walk you through installation, configuration, deployment, and best practices for k3s on AsiaGB VPS.
Why Choose k3s Over Docker Compose or Full Kubernetes?
Docker Compose is great for running a few containers on a single host, but as your application grows in complexity, you'll need container orchestration features that Docker Compose simply cannot provide. Here's why k3s is the sweet spot between simplicity and capability:
- Auto-healing — When a container crashes, k3s automatically restarts it without manual intervention
- Rolling updates — Deploy new versions of your app with zero downtime, smoothly transitioning traffic from old to new pods
- Resource limits — Define CPU and memory requests/limits per pod to prevent resource starvation and OOM (Out of Memory) crashes
- Declarative configuration — Store all infrastructure as code (YAML files) in git, enabling reproducible deployments and easy audits
- Helm support — Install complex software stacks (databases, message brokers, monitoring tools) in one command
- Service discovery — Automatic DNS names for pods and services within the cluster
- Load balancing — Built-in Traefik ingress controller handles HTTP/HTTPS routing and TLS termination
Compared to full Kubernetes, k3s reduces overhead: no need to manage etcd separately, no kube-controller-manager as a separate service. k3s bundles everything into a single binary, making it perfect for VPS with limited resources. Yet you get the same kubectl and Helm ecosystem, so skills transfer directly to enterprise Kubernetes environments.
System Requirements and Prerequisites
- Operating System: Ubuntu 20.04 LTS, 22.04 LTS, or newer (also works on Debian, CentOS, but this guide uses Ubuntu)
- RAM: Minimum 2 GB (comfortable for small workloads); 4-8 GB recommended for production
- CPU: 2 cores minimum (4 cores recommended for multiple containers)
- Storage: 10 GB free disk space (SSD for better performance)
- Network: Stable internet connection; port 6443 (k3s API) must be accessible from local machine
- Sudo access: Installation requires root or sudo privileges
AsiaGB VPS plans (4 GB RAM / 2 CPU) are ideal starting points for k3s production clusters. They provide enough headroom for control plane + container workloads without CPU throttling.
Step 1 — Installing k3s on Your VPS
The k3s installation is refreshingly simple thanks to the official installer script. SSH into your VPS and run the following command:
# Download and run k3s installer
curl -sfL https://get.k3s.io | sh -
# Wait 1-2 minutes for the control plane to initialize
sudo systemctl status k3s
# Verify the cluster is ready
sudo k3s kubectl get nodes
The installer automatically:
- Downloads the k3s binary (50 MB) to `/usr/local/bin/k3s`
- Creates necessary system files and directories
- Installs containerd (the container runtime)
- Starts the k3s service (systemd)
- Sets up kubeconfig at `/etc/rancher/k3s/k3s.yaml`
When the node status shows Ready, your k3s cluster is online. If you see NotReady, wait a bit longer and check system logs with `journalctl -u k3s -f`.
Step 2 — Configuring kubectl on Your Local Machine
To manage your cluster from your laptop or desktop, you need to copy the kubeconfig file from the VPS and configure local kubectl access:
# On the VPS, print the kubeconfig
sudo cat /etc/rancher/k3s/k3s.yaml
Copy the entire output. Then on your local machine:
# Create .kube directory if it doesn't exist
mkdir -p ~/.kube
# Create a new kubeconfig file (don't overwrite existing ones)
nano ~/.kube/config-k3s
# Paste the kubeconfig content
# IMPORTANT: Replace 127.0.0.1 with your VPS's actual IP address or domain
# Change: server: https://127.0.0.1:6443
# To: server: https://YOUR_VPS_IP:6443
# Set environment variable to use this kubeconfig
export KUBECONFIG=~/.kube/config-k3s
# Test the connection
kubectl get nodes
kubectl get pods -A # View all pods across all namespaces
You should see your k3s node in the output. If you get connection refused errors, double-check the IP address and that port 6443 is open in your VPS firewall.
Step 3 — Deploying Your First Application
Now that your cluster is running, let's deploy a simple NGINX web server with proper resource management, health checks, and service exposure:
Create a Deployment with Health Checks
# Save this as nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
Deploy and monitor:
kubectl apply -f nginx-deployment.yaml
kubectl get pods -w # Watch pods come online
kubectl describe pod nginx-XXX # View pod details
kubectl logs -f nginx-XXX # Stream container logs
Expose the Deployment with a Service
# Save as nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP # Internal service (no external IP)
kubectl apply -f nginx-service.yaml
kubectl get svc # List services
Create an Ingress with Traefik (Built-in to k3s)
k3s comes with Traefik pre-installed as the default ingress controller. It automatically handles HTTP/HTTPS routing, TLS certificates, and load balancing:
# Save as nginx-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: traefik
rules:
- host: nginx.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
tls:
- hosts:
- nginx.example.com
secretName: nginx-tls
Apply it with: `kubectl apply -f nginx-ingress.yaml`. Traefik will automatically provision a Let's Encrypt certificate if you have cert-manager installed (see Step 4).
Step 4 — Installing Helm for Package Management
Helm is the package manager for Kubernetes, similar to apt for Ubuntu. It allows you to install pre-configured applications with a single command:
# Download and install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Verify installation
helm version
Installing Applications with Helm
Let's install cert-manager, which automatically manages TLS certificates for your ingress controllers:
# Add the Jetstack Helm repository (maintainers of cert-manager)
helm repo add jetstack https://charts.jetstack.io
helm repo update
# Install cert-manager with CRD support
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set installCRDs=true \
--set global.leaderElection.namespace=cert-manager
# Verify the installation
kubectl get pods -n cert-manager
kubectl get crds | grep cert-manager
With cert-manager running, your Ingress resources can automatically obtain and renew TLS certificates from Let's Encrypt, keeping your applications secure without manual effort.
Step 5 — ConfigMaps and Secrets for Application Configuration
Never hardcode configuration values or passwords into your container images. Instead, use ConfigMaps for non-sensitive config and Secrets for sensitive data:
Create ConfigMap for Application Settings
# Create a ConfigMap from command line
kubectl create configmap app-config \
--from-literal=DB_HOST=postgres.default.svc.cluster.local \
--from-literal=DB_PORT=5432 \
--from-literal=CACHE_ENABLED=true \
--from-literal=LOG_LEVEL=info
# Or define it in YAML (ConfigMap.yaml)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DB_HOST: postgres.default.svc.cluster.local
DB_PORT: "5432"
CACHE_ENABLED: "true"
LOG_LEVEL: info
nginx.conf: |
server {
listen 80;
location / { return 200 "OK"; }
}
Create Secrets for Sensitive Data
# Create a Secret from command line (base64 encoded automatically)
kubectl create secret generic app-secret \
--from-literal=DB_PASSWORD=supersecretpass123 \
--from-literal=API_KEY=sk_live_abc123def456 \
--from-literal=JWT_SECRET=your-jwt-signing-key
# Verify (values are base64 encoded for security)
kubectl get secret app-secret -o yaml
Use ConfigMap and Secret in a Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
env:
# ConfigMap values
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
# Secret values
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secret
key: DB_PASSWORD
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-secret
key: API_KEY
# ConfigMap as mounted file (nginx.conf)
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
volumes:
- name: config-volume
configMap:
name: app-config
Step 6 — Persistent Volumes for Stateful Applications
Containers are ephemeral by default — data is lost when a pod is deleted. For databases, file storage, or any stateful data, use Persistent Volumes and Persistent Volume Claims:
Create a PVC for PostgreSQL
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path # k3s built-in storage class
resources:
requests:
storage: 20Gi
Deploy PostgreSQL with Persistent Storage
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
Data in `/var/lib/postgresql/data` is now persisted to disk. Even if the pod is recreated, the data remains intact.
Essential kubectl Commands for Daily Operations
Master these commands to effectively manage your k3s cluster:
# Cluster and node information
kubectl cluster-info
kubectl get nodes -o wide
kubectl describe node k3s-node-1
# View resources across all namespaces
kubectl get all -A
kubectl get pods -A
# Inspect specific resources
kubectl get deployment myapp
kubectl describe deployment myapp
kubectl get svc -A
kubectl get pvc
# Container logs and debugging
kubectl logs deployment/nginx # Latest logs from deployment
kubectl logs pod-name -f --tail=50 # Stream 50 latest lines
kubectl logs pod-name --previous # Logs from previous crashed container
kubectl exec -it pod-name -- /bin/sh # Interactive shell in container
# Scaling and updates
kubectl scale deployment nginx --replicas=5
kubectl set image deployment/nginx nginx=nginx:latest
kubectl rollout history deployment/nginx
kubectl rollout undo deployment/nginx # Rollback to previous version
# Port forwarding for debugging
kubectl port-forward service/nginx 8080:80 # Access pod via localhost:8080
# Events and troubleshooting
kubectl get events --sort-by=.metadata.creationTimestamp
kubectl get events -A
kubectl describe pod stuck-pod-name
Enable Resource Metrics Monitoring
By default, k3s doesn't come with metrics-server. Install it to enable resource monitoring and horizontal pod autoscaling:
# Install metrics-server
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# Wait for metrics-server to be ready (30-60 seconds)
kubectl get pods -n kube-system -w | grep metrics-server
# Once ready, you can monitor resource usage
kubectl top nodes
kubectl top pods -A
Networking Deep Dive: Services and Ingress
Understanding networking in k3s is crucial for exposing your applications correctly:
Service Types
- ClusterIP (default): Only accessible within the cluster; used for inter-pod communication
- NodePort: Exposes port on all nodes (high port number like 30000+); accessible from outside via
node-ip:nodeport - LoadBalancer: Provisions an external load balancer (if cloud provider integration exists); not applicable on single VPS
- ExternalName: Maps service to external DNS name (e.g.,
my-database.external-service.com)
For web applications on a VPS, use Ingress with Traefik (comes pre-installed) or NodePort for simple cases.
Storage Classes Beyond Local Path
The built-in local-path storage class works well for single-node clusters. For advanced scenarios, consider:
- Longhorn: Distributed block storage with replication across nodes
- NFS: Network File System for shared storage between pods
- Ceph: Enterprise-grade distributed storage (overkill for single-node k3s)
For most single-VPS setups, local-path is sufficient. If you scale to 3+ nodes, add Longhorn for automatic backups and replication.
Troubleshooting Common Issues
Pod Stuck in Pending State
kubectl describe pod stuck-pod
# Check Events section for reasons:
# - Insufficient CPU/memory
# - PVC not bound
# - Image pull errors
ImagePullBackOff Error
# The pod is trying to pull an image that doesn't exist or is private
# Fix: Use correct image name from Docker Hub
kubectl set image deployment/myapp myapp=correct-image:tag
CrashLoopBackOff
# Container is crashing on startup
# Check logs:
kubectl logs pod-name
# Common causes: wrong environment variables, config file missing, port conflicts
No External Access to Service
# Verify service exists and has endpoints
kubectl get svc -o wide
# Check if selector matches pod labels
kubectl get pods --show-labels
# If using Ingress, check ingress status
kubectl describe ingress myapp
Performance Optimization Tips
- Use Alpine images: Nginx:alpine is 50 MB vs nginx:latest at 150 MB
- Set resource requests/limits: Helps scheduler place pods efficiently and prevents resource starvation
- Use health checks: Liveness and readiness probes keep only healthy pods in rotation
- Enable pod disruption budgets: Ensure minimum availability during cluster updates
- Use StatefulSets for databases: Provides stable identities and ordered startup/shutdown
k3s vs Docker Compose — Detailed Comparison
| Feature | k3s | Docker Compose |
|---|---|---|
| Auto-restart pods | ✅ Via Liveness Probe | restart: unless-stopped |
| Zero-downtime deployment | ✅ Rolling Update | ❌ Brief downtime |
| Resource limits (CPU/mem) | ✅ Native support | Limited |
| Learning curve | High (Kubernetes concepts) | ✅ Very easy |
| RAM overhead | ~300-500 MB | ✅ ~50 MB |
| Helm chart support | ✅ Full compatibility | ❌ None |
| Industry adoption | Growing (startups, edge) | Widespread (dev/small apps) |
| Migration to K8s | ✅ Skills transfer directly | Need to rewrite YAML |
Security Best Practices for k3s
- Enable RBAC (Role-Based Access Control): Control who can access what in the cluster
- Use Network Policies: Restrict traffic between pods (default: all pods can talk to each other)
- Scan container images: Use tools like Trivy to find vulnerabilities before deploying
- Keep k3s updated: Regularly update to get security patches: `curl -sfL https://get.k3s.io | sh -`
- Backup your data: Regularly backup ConfigMaps, Secrets, and persistent volumes
- Use private container registries: Store sensitive images in private Docker registries with authentication
Pro Tip — Access k3s from Multiple Machines: Copy `/etc/rancher/k3s/k3s.yaml` from your VPS to multiple local machines (replace 127.0.0.1 with VPS IP on each). This lets your entire team manage the cluster, or use CI/CD pipelines to deploy applications automatically.
Ready to Run k3s on a VPS?
AsiaGB VPS with 4–8 GB RAM provides the perfect foundation for k3s clusters. Full root access, SSD storage, and 99% uptime SLA ensure your Kubernetes workloads stay online. Starting at 500 THB/month.
View VPS Plans