Install
One-liner install on Debian, Ubuntu, RHEL-family, openSUSE, or Alpine:
curl -sfL https://get.k3s.io | sh -
That script:
- Downloads the
k3sbinary into/usr/local/bin/. - Generates a systemd unit at
/etc/systemd/system/k3s.service. - Starts the service.
- Writes a kubeconfig to
/etc/rancher/k3s/k3s.yaml.
To pin a version, install with token preset, or skip some default components, set env vars first:
curl -sfL https://get.k3s.io | \
INSTALL_K3S_VERSION="v1.31.4+k3s1" \
INSTALL_K3S_EXEC="server --disable traefik --disable servicelb" \
K3S_TOKEN="my-shared-secret" \
sh -
Wait 10–20 seconds for first start, then:
sudo k3s kubectl get nodes
sudo k3s kubectl get pods -A
Both kubectl and crictl are subcommands of k3s, so the standalone binaries aren't required. To use a regular kubectl:
# Make the kubeconfig readable by your user
sudo chmod 644 /etc/rancher/k3s/k3s.yaml
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
kubectl get nodes
What's actually running
Fresh install pods, all in kube-system:
coredns— cluster DNSlocal-path-provisioner— satisfiesPersistentVolumeClaimby creating/var/lib/rancher/k3s/storage/<pvc-id>on the nodemetrics-server—kubectl topdata, HPA inputtraefik— default ingress controller, listens on host ports 80/443svclb-traefik-*— LoadBalancer service implementation via host network
The control plane itself runs in-process inside the single k3s binary, not as separate pods. ps shows just one k3s server process — everything else lives inside it as goroutines.
Apply a manifest
cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
spec:
replicas: 2
selector:
matchLabels: { app: hello }
template:
metadata:
labels: { app: hello }
spec:
containers:
- name: hello
image: traefik/whoami:latest
ports: [ { containerPort: 80 } ]
---
apiVersion: v1
kind: Service
metadata:
name: hello
spec:
selector: { app: hello }
ports: [ { port: 80, targetPort: 80 } ]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
rules:
- host: hello.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello
port: { number: 80 }
EOF
Point DNS at the host, hit https://hello.example.com. Traefik will request a Let's Encrypt cert automatically if the traefik HelmChartConfig has ACME enabled (see below).
Traefik ACME / Let's Encrypt
K3s deploys Traefik via a HelmChart custom resource. Override its values by writing a HelmChartConfig at /var/lib/rancher/k3s/server/manifests/traefik-config.yaml — K3s watches that directory and applies anything dropped in.
# /var/lib/rancher/k3s/server/manifests/traefik-config.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
certificatesResolvers:
letsencrypt:
acme:
email: ops@example.com
storage: /data/acme.json
httpChallenge:
entryPoint: web
additionalArguments:
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=false"
persistence:
enabled: true
size: 128Mi
Add traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt to each Ingress and the cert is issued + renewed automatically.
Storage
Out of the box, any PVC with storageClassName: local-path (the default) gets a directory under /var/lib/rancher/k3s/storage/. That's perfect for a single-node deployment — the data is on local disk, restic backs it up.
For real multi-node, Longhorn (also from Rancher) is the natural pairing — replicated block storage built on top of K3s nodes themselves.
Adding workers
On the server, grab the node token:
sudo cat /var/lib/rancher/k3s/server/node-token
On each worker:
curl -sfL https://get.k3s.io | \
K3S_URL="https://<server-ip>:6443" \
K3S_TOKEN="<node-token>" \
sh -
Workers register and appear in kubectl get nodes within seconds.
Disabling defaults you don't want
For a "K3s but I'll bring my own ingress and CNI" install (e.g. Cilium + nginx ingress):
curl -sfL https://get.k3s.io | \
INSTALL_K3S_EXEC="server \
--flannel-backend=none \
--disable-network-policy \
--disable=traefik \
--disable=servicelb" \
sh -
Then install Cilium / Calico / nginx-ingress via Helm as on any cluster.
SQLite vs etcd
K3s defaults to SQLite as the kine-backed datastore — one file at /var/lib/rancher/k3s/server/db/state.db. That is fine for a single-node K3s. For HA control plane, pass --cluster-init on the first server and --server https://<first-server>:6443 on additional ones; K3s automatically switches to embedded etcd. Or point at external Postgres/MySQL with --datastore-endpoint.
Backups for the single-node case
/var/lib/rancher/k3s/server/db/— the SQLite/etcd datastore (cluster state, secrets, RBAC, all applied YAML)./var/lib/rancher/k3s/storage/— PVC-backed data./etc/rancher/k3s/— config, kubeconfig./var/lib/rancher/k3s/server/manifests/— any HelmChartConfigs you wrote.
All four directories under a nightly restic job, plus a periodic k3s etcd-snapshot if running in etcd mode, covers full state. Restoring is uninstall + reinstall + drop those directories back.
When K3s is the wrong answer
If you want one box to run an app, plain Podman/Quadlet (see that tutorial) is lighter — no Kubernetes vocabulary needed. K3s makes sense when:
- You'll grow to multiple nodes later and want the same vocabulary now.
- You want declarative Ingress + cert-manager + ConfigMap/Secret/HPA without re-implementing them.
- You're shipping Helm charts you didn't write and want to run them as-is.
Below that bar, K3s is overkill. Above it, K3s is the smallest tool that still gives you the real thing.