Kubernetes has a reputation for appetite. The standard distribution demands multiple nodes, hungry control-plane components, and an etcd cluster humming along in the background just to keep the lights on. That cost made sense for massive fleets but looked absurd for a small SaaS, a homelab, or an edge appliance with a single board computer inside. K3s changed the calculation entirely. Packaged as a single binary under 70 megabytes, it strips the fat from upstream Kubernetes without dropping conformance, then bakes everything a real workload needs into one tidy installation. This article shows how to stand up a single-server K3s cluster that can actually run production traffic, what to tune for reliability, and where the trade-offs live.

Why K3s Makes Sense As A Lightweight Alternative To Vanilla Kubernetes For Small And Edge Deployments

Running full Kubernetes on one machine felt like parking a cargo ship in a bathtub. Too much overhead for too little benefit. K3s took a different route. The team behind it collapsed the API server, controller manager, scheduler, kubelet, and kube-proxy into a single process, removed storage drivers and cloud providers that most users never touch, and replaced etcd with embedded SQLite as the default datastore. The result keeps full CNCF certification while cutting memory usage roughly in half.

The distribution arrives batteries included. Containerd serves as the container runtime. Flannel handles pod networking. CoreDNS resolves service names. Traefik provides ingress out of the box. A local-path provisioner supplies a working StorageClass from day one. Every one of these components can be swapped out or disabled, but nothing is missing on first boot. That completeness matters because most Kubernetes tutorials spend three chapters wiring up the same plumbing before any real application runs.

The minimum resource footprint hovers around 512 MB of RAM and a single CPU core. Real workloads need more, of course, but the overhead K3s itself adds rarely exceeds 250 MB. A modest virtual machine with 2 GB of RAM and two vCPUs can comfortably host a small production service plus monitoring and an ingress controller.

Preparing The Linux Host With Sensible System Settings Before Touching The Installer

A fresh Ubuntu or Debian server needs a few adjustments before K3s touches it. Swap should be disabled, because Kubernetes scheduling assumes predictable memory behaviour. Kernel modules for bridging and overlay networking need to load automatically. Firewall rules must allow the ports K3s uses internally.

# Disable swap permanently
sudo swapoff -a
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

# Load required kernel modules
sudo modprobe br_netfilter overlay
echo -e "br_netfilter\noverlay" | sudo tee /etc/modules-load.d/k3s.conf

# Enable required sysctl values
cat <<EOF | sudo tee /etc/sysctl.d/k3s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sudo sysctl --system

For a single-server deployment, the firewall only needs to allow SSH for administration and whatever ports the workload exposes publicly. The internal Kubernetes ports stay on the loopback interface because there are no other nodes to coordinate with.

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Time synchronisation matters more than most guides admit. A drifting clock breaks TLS certificate validation and causes mysterious authentication failures weeks later. Installing chrony or systemd-timesyncd takes thirty seconds and prevents an entire category of future problems.

Installing K3s Through The Official Script With Production Oriented Flags

The get.k3s.io installer is the canonical path. It downloads the binary, configures a systemd unit, and starts the service. What separates a throwaway install from a production one is the set of flags passed through the INSTALL_K3S_EXEC environment variable.

curl -sfL https://get.k3s.io | \
  INSTALL_K3S_EXEC="server \
    --write-kubeconfig-mode 0644 \
    --tls-san 203.0.113.42 \
    --tls-san k3s.example.com \
    --disable traefik \
    --disable servicelb \
    --node-taint CriticalAddonsOnly=true:NoExecute" \
  sh -

Each flag earns its place. The write-kubeconfig-mode setting lets non-root users read the kubeconfig file without sudo gymnastics. The tls-san entries register additional names or IPs in the API server certificate, which becomes essential for reaching the cluster remotely through kubectl. Disabling Traefik and ServiceLB leaves room for dedicated replacements like ingress-nginx and MetalLB when those fit the architecture better. The node taint prevents arbitrary workloads from landing on the control plane on multi-node expansions, though it can be omitted for truly single-node setups.

Verification takes seconds:

sudo systemctl status k3s
sudo k3s kubectl get nodes
sudo k3s kubectl get pods -A

A healthy cluster shows the node in Ready state and every system pod running. If something refuses to come up, journalctl -u k3s -f tells the whole story.

Configuring Kubectl Access From The Server And Any Remote Workstation

The installer drops a kubeconfig at /etc/rancher/k3s/k3s.yaml that points to https://127.0.0.1:6443. That works locally but breaks from anywhere else. Copying the file and rewriting the server address fixes that instantly.

# On the server, make the kubeconfig accessible to your user
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config

# From a remote workstation
scp Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s-prod.yaml
sed -i 's|127.0.0.1|203.0.113.42|' ~/.kube/k3s-prod.yaml
export KUBECONFIG=~/.kube/k3s-prod.yaml
kubectl get nodes

Installing a standalone kubectl on the workstation, rather than relying on the bundled k3s kubectl, makes plugins and custom tooling behave predictably. Helm, k9s, stern, and other ecosystem utilities all expect a regular kubectl binary on the PATH.

Swapping Default Components For Production Grade Alternatives That Fit Real Workloads

The bundled components are practical defaults. They are not always the right choice for production. Teams commonly replace them with more capable options, and K3s makes that swap trivial because each default can be disabled individually.

A typical production loadout on a single server looks like this:

  • Ingress-nginx replaces Traefik when the team already knows nginx syntax and needs advanced features like custom error pages, rate limiting, or ModSecurity integration
  • MetalLB takes over from the default ServiceLB in bare-metal environments, assigning real IP addresses to LoadBalancer services from a pool the operator controls
  • Longhorn or OpenEBS steps in for persistent storage when the local-path provisioner cannot provide replication, snapshots, or volume expansion
  • Cert-manager handles TLS certificates through Let's Encrypt automatically, removing the manual toil of renewing and rotating certs
  • Prometheus and Grafana through the kube-prometheus-stack Helm chart give visibility into cluster health and application metrics from the first day

Installing these replacements follows standard Kubernetes practice. Ingress-nginx, for example, goes on with a single Helm command:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.metrics.enabled=true

The modularity is the point. K3s does not lock anyone into its defaults, and the installer flags that disable bundled components produce a cleaner slate than most full Kubernetes distributions ever offer.

Hardening The Cluster With Backups, Automatic Upgrades, And Realistic Disaster Recovery

Single-server deployments trade high availability for simplicity. That trade is defensible only when backups are taken seriously and recovery has been rehearsed. The SQLite database at /var/lib/rancher/k3s/server/db/state.db holds every cluster resource, and snapshotting it regularly protects against the worst outcomes.

K3s supports native etcd snapshots when configured with --cluster-init, but for SQLite-backed single-server clusters the simplest reliable approach is stopping the service briefly and copying the database, or using a filesystem-level snapshot if the underlying disk supports it.

# Simple scheduled backup via cron
sudo mkdir -p /var/backups/k3s
cat <<'EOF' | sudo tee /etc/cron.daily/k3s-backup
#!/bin/bash
systemctl stop k3s
cp /var/lib/rancher/k3s/server/db/state.db \
   /var/backups/k3s/state-$(date +%F).db
systemctl start k3s
find /var/backups/k3s -name 'state-*.db' -mtime +14 -delete
EOF
sudo chmod +x /etc/cron.daily/k3s-backup

Automatic upgrades deserve equal attention. The Rancher system-upgrade-controller can roll out new K3s versions through Kubernetes-native Plan resources, cordoning the node and applying the binary update without manual intervention. For a single-server cluster, where every upgrade briefly interrupts service, operators typically schedule these rollouts during maintenance windows and pair them with health checks that trigger rollback if something breaks.

Deploying A First Workload And Verifying That The Cluster Actually Works End To End

A freshly built cluster should prove itself with a real workload before anything important lands on it. A simple nginx deployment behind an ingress confirms that scheduling, networking, DNS, and ingress all work together.

kubectl create deployment demo --image=nginx:stable --replicas=2
kubectl expose deployment demo --port=80

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
    - host: demo.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: demo
                port:
                  number: 80
EOF

Pointing DNS at the server address and hitting the hostname in a browser should return the nginx welcome page. If it does, every layer of the stack is functioning. If it does not, the troubleshooting path is narrow enough that a single-server setup usually reveals the problem within minutes.

Final Thoughts On Running Kubernetes Lean Without Sacrificing Production Quality

K3s made a bet that full Kubernetes fidelity could survive aggressive simplification, and the bet paid off. Small teams, edge deployments, and resource-constrained environments gained access to the same declarative API, the same controllers, and the same ecosystem tooling that power global-scale platforms. The binary is smaller, the memory footprint is lighter, and the learning curve flattens significantly.

The single-server deployment pattern described here trades high availability for operational simplicity and cost efficiency. That trade makes sense for staging environments, internal services, homelab projects, and even some production workloads where a brief outage during host maintenance is acceptable. When the workload outgrows one machine, K3s scales naturally into a multi-server high-availability configuration using embedded etcd or an external datastore, reusing the same binary and the same configuration habits. The investment made on day one keeps paying off as the cluster grows, and that continuity is perhaps the quietest but most valuable feature of the entire distribution.