1"use strict";(self.webpackChunkkairos_docs=self.webpackChunkkairos_docs||[]).push([["15726"],{63941(e,n,o){o.r(n),o.d(n,{metadata:()=>r,default:()=>h,frontMatter:()=>l,contentTitle:()=>i,toc:()=>c,assets:()=>a});var r=JSON.parse('{"id":"examples/cluster-setup/k0s-cluster","title":"Deploy a k0s Cluster Step by Step","description":"A complete beginner-friendly walkthrough to bootstrap a multi-node Kubernetes cluster with the k0s provider, choosing exactly which nodes act as controllers and which as workers.","source":"@site/versioned_docs/version-v4.2.0/examples/cluster-setup/k0s-cluster.md","sourceDirName":"examples/cluster-setup","slug":"/examples/k0s-cluster","permalink":"/docs/v4.2.0/examples/k0s-cluster","draft":false,"unlisted":false,"editUrl":"https://github.com/kairos-io/kairos-docs/tree/main/versioned_docs/version-v4.2.0/examples/cluster-setup/k0s-cluster.md","tags":[],"version":"v4.2.0","lastUpdatedBy":"github-actions[bot]","lastUpdatedAt":1787082255000,"frontMatter":{"title":"Deploy a k0s Cluster Step by Step","sidebar_label":"k0s cluster step by step","description":"A complete beginner-friendly walkthrough to bootstrap a multi-node Kubernetes cluster with the k0s provider, choosing exactly which nodes act as controllers and which as workers.","slug":"/examples/k0s-cluster"},"sidebar":"tutorialSidebar","previous":{"title":"Cluster Setup","permalink":"/docs/v4.2.0/examples/cluster-setup"},"next":{"title":"Self-coordinating P2P multi-node cluster HA with KubeVIP","permalink":"/docs/v4.2.0/examples/multi-node-p2p-ha-kubevip"}}'),s=o(74848),t=o(28453);let l={title:"Deploy a k0s Cluster Step by Step",sidebar_label:"k0s cluster step by step",description:"A complete beginner-friendly walkthrough to bootstrap a multi-node Kubernetes cluster with the k0s provider, choosing exactly which nodes act as controllers and which as workers.",slug:"/examples/k0s-cluster"},i,a={},c=[{value:"How roles work with the k0s provider",id:"how-roles-work-with-the-k0s-provider",level:2},{value:"What you need",id:"what-you-need",level:2},{value:"Step 1: Install the first controller",id:"step-1-install-the-first-controller",level:2},{value:"Step 2: Verify the controller and create join tokens",id:"step-2-verify-the-controller-and-create-join-tokens",level:2},{value:"Step 3: Join the workers",id:"step-3-join-the-workers",level:2},{value:"Step 4 (optional): Add more controllers for high availability",id:"step-4-optional-add-more-controllers-for-high-availability",level:2},{value:"Step 5: Verify the cluster and get the kubeconfig",id:"step-5-verify-the-cluster-and-get-the-kubeconfig",level:2},{value:"Troubleshooting",id:"troubleshooting",level:2},{value:"Next steps",id:"next-steps",level:2}];function d(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["This guide walks you through deploying a complete Kubernetes cluster with Kairos and the ",(0,s.jsx)(n.a,{href:"https://k0sproject.io/",children:"k0s"})," provider, from downloading an image to retrieving a working ",(0,s.jsx)(n.code,{children:"kubeconfig"}),". Unlike the ",(0,s.jsx)(n.a,{href:"/docs/installation/p2p",children:"P2P self-coordinating setup"}),", here ",(0,s.jsx)(n.strong,{children:"you decide explicitly which nodes are controllers and which are workers"})," \u2014 the role of each node is determined by the configuration file you give it at install time."]}),"\n",(0,s.jsx)(n.p,{children:"By the end you will have:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"One controller node (optionally more, for a highly available control plane)"}),"\n",(0,s.jsx)(n.li,{children:"One or more worker nodes"}),"\n",(0,s.jsxs)(n.li,{children:["A ",(0,s.jsx)(n.code,{children:"kubeconfig"})," to manage the cluster from your own machine"]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"how-roles-work-with-the-k0s-provider",children:"How roles work with the k0s provider"}
1),"\n",(0,s.jsxs)(n.p,{children:["Each Kairos node gets its role from which block you enable in its cloud-config. There is no election or autodiscovery in this flow \u2014 the config ",(0,s.jsx)(n.em,{children:"is"})," the role assignment:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Block in cloud-config"}),(0,s.jsx)(n.th,{children:"Role of the node"}),(0,s.jsx)(n.th,{children:"k0s service started"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"k0s:"})}),(0,s.jsx)(n.td,{children:"Controller (control plane)"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"k0scontroller"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"k0s-worker:"})}),(0,s.jsx)(n.td,{children:"Worker"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"k0sworker"})})]})]})]}),"\n",(0,s.jsx)(n.p,{children:"Enable exactly one of the two blocks per node."}),"\n",(0,s.jsxs)(n.p,{children:["If you are cross-reading the ",(0,s.jsx)(n.a,{href:"https://docs.k0sproject.io/stable/k0s-multi-node/",children:"upstream k0s documentation"}),": its ",(0,s.jsx)(n.code,{children:"k0s install controller/worker ... --start"})," steps are handled for you by Kairos \u2014 the config block selects which service runs, and anything you put in ",(0,s.jsx)(n.code,{children:"args:"})," is passed to the ",(0,s.jsx)(n.code,{children:"k0s controller"})," or ",(0,s.jsx)(n.code,{children:"k0s worker"})," command. Everything else (tokens, ports, k0s behavior) works exactly as upstream documents."]}),"\n",(0,s.jsx)(n.admonition,{title:"Controllers don't run workloads by default",type:"warning",children:(0,s.jsxs)(n.p,{children:["A plain k0s controller does ",(0,s.jsx)(n.strong,{children:"not"})," run a kubelet. It won't show up in ",(0,s.jsx)(n.code,{children:"kubectl get nodes"})," and won't schedule any pods \u2014 this is expected k0s behavior, not a broken install. If you want a controller to also act as a worker (common in small clusters), add ",(0,s.jsx)(n.code,{children:"--enable-worker"})," to its args, as shown below."]})}),"\n",(0,s.jsx)(n.h2,{id:"what-you-need",children:"What you need"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Two or more machines (bare metal or VMs) on the same network, each with at least 2GB of RAM and 40GB of disk"}),"\n",(0,s.jsxs)(n.li,{children:["A Kairos ",(0,s.jsx)(n.strong,{children:"standard"})," image that bundles k0s. Standard images are published for every release, e.g. ",(0,s.jsx)(n.code,{children:"quay.io/kairos/opensuse:leap-15.6-standard-amd64-generic-v3.5.0-k0s-v1.33.3-k0s.0"}),". ISOs are available from the ",(0,s.jsx)(n.a,{href:"https://github.com/kairos-io/kairos/releases",children:"Kairos releases page"})," \u2014 pick an artifact with ",(0,s.jsx)(n.code,{children:"standard"})," and ",(0,s.jsx)(n.code,{children:"k0s"})," in its name."]}),"\n",(0,s.jsxs)(n.li,{children:["A way to boot the machines with the image and a cloud-config: see the ",(0,s.jsx)(n.a,{href:"/docs/installation/",children:"Installation"})," section. The simplest path is booting the ISO and using ",(0,s.jsx)(n.a,{href:"/docs/installation/manual",children:"manual"})," or ",(0,s.jsx)(n.a,{href:"/docs/installation/automated",children:"automated"})," installation."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"step-1-install-the-first-controller",children:"Step 1: Install the first controller"}),"\n",(0,s.jsx)(n.p,{children:"Create a cloud-config for the node you want as your (first) controller. This is the only node you boot for now."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",children:"#cloud-config\n\nhostname: controller-{{ trunc 4 .MachineID }}\nusers:\n- name: kairos # Change to your own user\n passwd: kairos # Change to your own password\n groups:\n - admin # This user needs to be part of the admin group\n ssh_authorized_keys:\n - github:<YOUR_GITHUB_USER> # replace with your github user\n\nk0s:\n enabled: true\n"})}),"\n",(0,s.jsx)(n.p,{children:"If you want this controller to also schedule workloads (a good fit for a 2\u20133 machine cluster), enable the embedded worker:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",children:"k0s:\n enabled: true\n args:\n - --enable-worker\n"})}),"\n",(0,s.jsx)(n.p,{children:"Install the node with this config and let it reboot into the installed system."}),"\n",(0,s.jsx)(n.admonition,{title:"Single node?",type:"tip",children:(0,s.jsxs)(n.p,{children:["If this machine will be your entire cluster, use ",(0,s.jsx)(n.code,{children:'args: ["--single"]'})," instead and stop here \u2014 see the ",(0,s.jsx)(n.a,{href:"/docs/examples/single-node",children:"single-node example"}),"."]})}),"\n",(0,s.jsx)(n.h2,{id:"step-2-verify-the-controller-and-create-join-tokens",children:"Step 2: Verify the controller and create join tokens"}),"\n",(0,s.jsxs)(n.p,{children:["SSH into the controller (",(0,s.jsx)(n.code,{children:"ssh kairos@<CONTROLLER_IP>"}),") and check that k0s is up:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"sudo k0s status\n"})}),"\n",(0,s.jsxs)(n.p,{children:["You should see ",(0,s.jsx)(n.code,{children:"Role: controller"})," and a running process. Now create the tokens the other nodes will use to join. Tokens are role-specific:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"# One token for the worker nodes\nsudo k0s token create --role=worker\n\n# Only if you plan to add more controllers (Step 4)\nsudo k0s token create --role=controller\n"})}),"\n",(0,s.jsx)(n.p,{children:"Each command prints a long base64 string. Copy them somewhere safe \u2014 you'll paste them into the configs of the joining nodes."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["Tokens are credentials: anyone holding one can join a node to your cluster. Treat them like passwords and prefer short
1-lived tokens (",(0,s.jsx)(n.code,{children:"--expiry 1h"}),") if the machines join right away."]})}),"\n",(0,s.jsx)(n.h2,{id:"step-3-join-the-workers",children:"Step 3: Join the workers"}),"\n",(0,s.jsxs)(n.p,{children:["For every worker node, use this cloud-config. Note the block is ",(0,s.jsx)(n.code,{children:"k0s-worker"}),", and the token is the one created with ",(0,s.jsx)(n.code,{children:"--role=worker"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",children:'#cloud-config\n\nhostname: worker-{{ trunc 4 .MachineID }}\nusers:\n- name: kairos # Change to your own user\n passwd: kairos # Change to your own password\n groups:\n - admin # This user needs to be part of the admin group\n ssh_authorized_keys:\n - github:<YOUR_GITHUB_USER> # replace with your github user\n\nk0s-worker:\n enabled: true\n args:\n - --token-file /etc/k0s/token\n\nwrite_files:\n- path: /etc/k0s/token\n permissions: "0600"\n content: |\n <WORKER_TOKEN> # the output of `k0s token create --role=worker` on the controller\n'})}),"\n",(0,s.jsx)(n.p,{children:"The same config (with the same token) works for all workers \u2014 the hostname template gives each machine a unique name. Install each worker and let it reboot; it will connect to the controller and register itself."}),"\n",(0,s.jsx)(n.h2,{id:"step-4-optional-add-more-controllers-for-high-availability",children:"Step 4 (optional): Add more controllers for high availability"}),"\n",(0,s.jsxs)(n.p,{children:["To make the control plane highly available, join additional controller nodes. Use an ",(0,s.jsx)(n.strong,{children:"odd total number of controllers"})," (1, 3, 5, ...) so etcd can keep quorum."]}),"\n",(0,s.jsxs)(n.p,{children:["The config for a joining controller uses the ",(0,s.jsx)(n.code,{children:"k0s"})," block \u2014 same as the first controller \u2014 plus the token created with ",(0,s.jsx)(n.code,{children:"--role=controller"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",children:'#cloud-config\n\nhostname: controller-{{ trunc 4 .MachineID }}\nusers:\n- name: kairos # Change to your own user\n passwd: kairos # Change to your own password\n groups:\n - admin # This user needs to be part of the admin group\n ssh_authorized_keys:\n - github:<YOUR_GITHUB_USER> # replace with your github user\n\nk0s:\n enabled: true\n args:\n - --token-file /etc/k0s/token\n\nwrite_files:\n- path: /etc/k0s/token\n permissions: "0600"\n content: |\n <CONTROLLER_TOKEN> # the output of `k0s token create --role=controller` on the first controller\n'})}),"\n",(0,s.jsx)(n.admonition,{title:"Production tip",type:"info",children:(0,s.jsxs)(n.p,{children:["In production, put a load balancer in front of the controllers instead of pointing everything at the first controller's IP. See the ",(0,s.jsx)(n.a,{href:"https://docs.k0sproject.io/stable/high-availability/",children:"k0s control plane HA documentation"})," for the details, and our ",(0,s.jsx)(n.a,{href:"/docs/examples/multi-node-p2p-ha-kubevip/",children:"KubeVIP example"})," for a self-hosted virtual IP approach."]})}),"\n",(0,s.jsx)(n.h2,{id:"step-5-verify-the-cluster-and-get-the-kubeconfig",children:"Step 5: Verify the cluster and get the kubeconfig"}),"\n",(0,s.jsx)(n.p,{children:"On the controller, check that all workers registered:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"sudo k0s kubectl get nodes\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"NAME STATUS ROLES AGE VERSION\nworker-9a3f Ready <none> 2m v1.33.3+k0s\nworker-c81d Ready <none> 1m v1.33.3+k0s\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Remember: controllers only appear in this list if they were installed with ",(0,s.jsx)(n.code,{children:"--enable-worker"})," (workers showing ",(0,s.jsx)(n.code,{children:"ROLES: <none>"})," is also normal \u2014 k0s doesn't label them). To list the control plane itself, ask etcd on any controller:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"sudo k0s etcd member-list\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'{"members":{"controller-27a0":"https://192.168.122.55:2380"}}\n'})}),"\n",(0,s.jsx)(n.p,{children:"Every controller that joined in Step 4 shows up here \u2014 this is the way to verify a highly available control plane."}),"\n",(0,s.jsx)(n.p,{children:"To manage the cluster from your own machine, export an admin kubeconfig from the controller:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"sudo k0s kubeconfig admin > kubeconfig\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Copy it over (e.g. ",(0,s.jsx)(n.code,{children:"scp kairos@<CONTROLLER_IP>:kubeconfig ."}),"), replace the ",(0,s.jsx)(n.code,{children:"https://localhost:6443"})," server address inside it with the controller's IP if needed, and point ",(0,s.jsx)(n.code,{children:"kubectl"})," at it:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"export KUBECONFIG=$PWD/kubeconfig\nkubectl get nodes\n"})}),"\n",(0,s.jsx)(n.h2,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsxs)(n.strong,{children:["The controller doesn't show up in ",(0,s.jsx)(n.code,{children:"kubectl get nodes"})]})," \u2014 expected unless it runs with ",(0,s.jsx)(n.code,{children:"--enable-worker"})," or ",(0,s.jsx)(n.code,{children:"--single"}),". See the warning at the top of this page."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"A node didn't join"})," \u2014 on the affected node, check the service logs: ",(0,s.jsx)(n.code,{children:"sudo systemctl status k0scontroller"})," or ",(0,s.jsx)(n.code,{children:"sudo systemctl status k0sworker"}),", and ",(0,s.jsx)(n.code,{children:"sudo k0s status"}),". The most common causes are a wrong or expired token, a token created for the wrong role, or the controller's ports not being reachable (6443 for the Kubernetes API, 8132 for konnectivity, 9443 for the controller join API)."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Wrong role token"})," \u2014 worker nodes need a ",(0,s.jsx)(n.code,{children:"--role=worker"})," token, joining controllers need a ",(0,s.jsx)(n.code,{children:"--role=controller"})," token. They are not interchangeable."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Config didn't apply"})," \u2014 verify the cloud-config landed on the node: ",(0,s.jsx)(n.code,{children:"sudo cat /oem/90_custom.yaml"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"next-steps",children:"Next steps"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Deploy workloads automatically at boot with ",(0,s.jsx)(n.a,{href:"/docs/reference/configuration#kubernetes-manifests",children:"Kubernetes manifests"})]}),"\n",(0,s.jsxs)(n.li,{children:["Explore all ",(0,s.jsx)(n.code,{children:"k0s"})," and ",(0,s.jsx)(n.code,{children:"k0s-worker"})," options in the ",(0,s.jsx)(n.a,{href:"/docs/reference/configuration#provider-configs",children:"configuration reference"})]}),"\n",(0,s.jsxs)(n.li,{children:["Prefer nodes that discover each other and assign roles automatically? See the ",(0,s.jsx)(n.a,{href:"/docs/installation/p2p",children:"P2P self-coordinating setup"})]}),"\n",(0,s.jsxs)(n.li,{children:["Learn how providers work under the hood in the ",(0,s.jsx)(n.a,{href:"/docs/architecture/providers",children:"providers architecture page"})]}),"\n"]})]})}function h(e={}){let{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},28453(e,n,o){o.d(n,{R:()=>l,x:()=>i});var r=o(96540);let s={},t=r.createContext(s);function l(e){let n=r.useContext(t);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:l(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.