Install

One-liner install on Debian, Ubuntu, RHEL-family, openSUSE, or Alpine:

curl -sfL https://get.k3s.io | sh -

That script:

  1. Downloads the k3s binary into /usr/local/bin/.
  2. Generates a systemd unit at /etc/systemd/system/k3s.service.
  3. Starts the service.
  4. 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 DNS
  • local-path-provisioner — satisfies PersistentVolumeClaim by creating /var/lib/rancher/k3s/storage/<pvc-id> on the node
  • metrics-serverkubectl top data, HPA input
  • traefik — default ingress controller, listens on host ports 80/443
  • svclb-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.