Quick start
From nothing to a working Kubernetes node in four steps, and a fifth for managing it without SSH.
The short version, if you are in a hurry:
oras pull ghcr.io/corium-os/corium-qcow2:0.1.0 # download a ready-made disk
printf '#cloud-config\ncorium:\n role: single\n' > node.yaml
# boot the disk with node.yaml as cloud-init user-dataThe rest of this page explains each step, and the last section covers the mistakes that cost the most time.
If you would rather understand the model before typing anything, read concepts first.
Before you start
Every release publishes a ready-made, signed disk image, so the quick path needs no build host at all. You only need two things:
- Somewhere to run the node — Proxmox, KVM/libvirt, or any cloud that accepts a qcow2 or an ISO.
- A way to download it —
oras,curl, or a browser. Any of the three works from macOS, Windows or Linux, and none needssudo.
You do not need podman, a Linux host, or 20 GB of build space unless you
mean to change the image and rebuild it yourself — that path is covered in
build your own image at the end.
1. Get a bootable disk
Pull the artefact that matches where the node will run:
| Artefact | Pull it with | Use it for |
|---|---|---|
| qcow2 disk | oras pull ghcr.io/corium-os/corium-qcow2:0.1.0 | Proxmox, KVM, libvirt |
| Installer ISO | oras pull ghcr.io/corium-os/corium-iso:0.1.0 | Bare metal. Installs unattended |
The tag above is only an example. The exact coordinates for a given version, with their digests, are on that version’s release page — they change every release, so they are published with it rather than written down here.
No oras? A plain curl or a browser download works just as well. See
downloads, which also covers how to check that what you
received is what was published:
cosign verify ghcr.io/corium-os/corium-qcow2:0.1.0 \
--certificate-identity-regexp 'https://github.com/Corium-OS/Corium/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comThe ISO is unattended: it deploys the image embedded in it with no prompts and no kickstart to write.
Changed the image and need your own disk instead? Build a qcow2, a raw disk or an ISO from it — see build your own image.
2. Write a node configuration
A complete single-node cluster:
#cloud-config
corium:
role: single
users:
- name: core
groups: [wheel]
ssh_authorized_keys:
- ssh-ed25519 AAAA... you@example.comrole is the only required field. Everything else has a default, and the
document stays an ordinary cloud-config — write_files, runcmd and the rest
keep working.
If you have the repository checked out, validate it before you boot anything:
go run ./cmd/corium-agent validate node.yamlValidation is offline and reports every problem at once, so you do not discover your mistakes one reboot at a time.
The four roles:
| Role | What it is |
|---|---|
single | A self-contained cluster. Cannot gain nodes later |
controller | Control plane only, runs no workloads |
controller+worker | Control plane that also accepts workloads |
worker | Workloads only; joins an existing cluster with a token |
See
examples/ for workers, add-ons, a custom CNI, and HA.
3. Boot it
Proxmox
# on the Proxmox node
VMID=140 \
DISK_IMAGE=/path/to/disk.qcow2 \
CLOUD_CONFIG=/path/to/node.yaml \
IP_CONFIG="ip=192.168.0.190/24,gw=192.168.0.1" \
NAMESERVER=192.168.0.1 \
bash deploy/proxmox/create-vm.sh
qm start 140Installing from the ISO instead uses deploy/proxmox/create-vm-iso.sh, which
takes the same variables plus ISO=local:iso/corium-install.iso.
Anywhere else
Boot the disk and hand node.yaml to the platform as cloud-init user-data.
Corium probes NoCloud, ConfigDrive, OpenStack, EC2, Azure, GCE, Hetzner, VMware
and OVF, so one image works across all of them.
For a NoCloud seed ISO:
printf 'instance-id: node-01\nlocal-hostname: node-01\n' > meta-data
cp node.yaml user-data
genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-dataWithout cloud-init
Bare metal with no seed device, PXE, or a preconfigured appliance: put the configuration where the agent will find it. Sources are tried in this order and the first that answers wins.
| Source | For |
|---|---|
/etc/corium/config.yaml | An operator’s answer for this machine |
| cloud-init | Clouds and hypervisors |
corium.config= on the kernel command line | PXE and netboot |
/usr/share/corium/config.yaml | A default baked into a derived image |
Outside a cloud-config, write the schema on its own — no corium: wrapper:
role: worker
cluster:
name: edge
join:
tokenFrom:
url: https://secrets.example.com/corium/worker-token4. Check it worked
ssh core@192.168.0.190
systemctl status corium-bootstrap # what the agent did, and why if it failed
sudo k0s kubectl get nodes
sudo k0s kubectl get pods -AA single node takes roughly a minute from power-on to Ready. The API answers
well before the node registers, so Ready is the milestone to wait for.
Use the cluster from your workstation:
ssh core@192.168.0.190 sudo k0s kubeconfig admin > kubeconfig
KUBECONFIG=./kubeconfig kubectl get nodesThe address in that file is whatever k0s decided, which is the node’s own — right
for a single node, wrong for a cluster with a virtual IP, where a kubeconfig
aimed at one controller stops working the first time that controller does.
Check the server: field before you rely on it. Step 5 removes that worry.
5. Manage it without SSH (optional)
Everything above works over SSH, and on an immutable OS that is a poor fit: the
shell you land in is a shell over a system where almost nothing you type
persists. Corium ships a management API for the things you actually want —
reading a node, restarting k0s, upgrading it, getting a kubeconfig — and a
client called cctl.
It is off unless you ask for it. A node with no api: block runs no daemon
and binds no port, which is what the four steps above produced.
cctl has no release artefact yet, so build it from a checkout:
mise run build # produces bin/cctlMake the operator CA. Its certificate is what nodes are told to trust; the key beside it stays on your machine and is never sent anywhere:
cctl pki init
cctl pki issue --role adminpki init prints the certificate ready to paste. Add it to the node
configuration from step 2 — it is a certificate, not a secret, so it is safe in
cloud-init in the open:
corium:
role: single
api:
operatorCA: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----Reprovision the node with that configuration, and it claims itself at boot. Then, from your workstation:
cctl status 192.168.0.190 # role, image digest, k0s, greenboot, uptime
cctl logs 192.168.0.190 --unit k0scontroller --since 15m
cctl kubeconfig 192.168.0.190 > kubeconfigThe first call asks you to confirm the node’s fingerprint against its journal,
and remembers it; a node signs its own certificate, so the fingerprint is what
identifies it rather than its name. cctl kubeconfig points the file at the
cluster’s virtual IP where there is one, which is the worry step 4 leaves you
with.
The cctl page covers every command, the three roles, and what the
API deliberately will not do.
Where to go next
- Add workers —
examples/worker.yaml. Mint a token on the controller withk0s token create --role=worker --expiry=1h. - Highly available control plane —
examples/ha-controller-first.yaml, ordeploy/proxmox/create-ha-cluster.shto bootstrap all three at once. No load balancer and no certificates required. - Add-ons — declare Helm charts under
addons:and k0s installs them at bootstrap. No Helm binary, no in-cluster operator. - A different CNI — installing Cilium, start to finish.
- Anything Corium does not model —
k0s.patchis applied verbatim to the renderedk0s.yaml, so every k0s setting stays reachable. - Every field in detail — the configuration reference.
- What is and is not supported — feature support, including everything reachable through the k0s passthrough.
Upgrades
Kubernetes ships with the OS, so upgrading means booting a new image. The upgrades guide covers doing this to a cluster without losing quorum; the short version:
sudo bootc upgrade --apply # reboots into the new version
sudo bootc rollback # if it went badlyDrain the node first if it carries workloads you care about.
Troubleshooting
The node boots and does nothing. Read the journal:
journalctl -u corium-bootstrap, or cctl logs <node> --unit corium-bootstrap
if you did step 5. A node with no Corium configuration says so and stops, which
is a valid outcome — you get a host, not a Kubernetes node.
cctl says the node is not enrolled. It is waiting to be claimed:
api.enabled: true with no CA is maintenance mode, and such a node holds its
bootstrap until somebody runs cctl enroll with the pairing code from its
console. If you meant it to come up on its own, give it api.operatorCA
instead.
The node is NotReady and stays there. If you set cni: custom, this is
expected: nothing has installed a network yet and the cluster is waiting for
you. Otherwise check kubectl -n kube-system describe pod for the CNI
DaemonSet.
Images will not pull, everything else works. Almost always DNS. A static
address configured through a hypervisor usually carries no resolver — Proxmox’s
ipconfig0 has no field for one — so set NAMESERVER. The symptom is
misleading: the node pings, SSH works, the API answers, and Kubernetes hangs
with lookup quay.io: Try again.
Installing from the ISO loops forever. The boot order must be disk first, ISO second. An empty disk has no UEFI boot entry, so the firmware falls through to the ISO and installs; afterwards the disk has an entry and wins. With the ISO first, the node reinstalls itself on every reboot.
Do not stop the VM during an install, either. Anaconda wipes the disk early, so an interrupted install leaves nothing bootable behind.
Two nodes have the same name. They should not: Corium derives a stable name
from the machine ID when the hostname is still generic. If you cloned a disk
after first boot, the machine ID came along with it — clear /etc/machine-id
on the clone, or set node.name explicitly.
Build your own image
Everything above uses a published image. You only need this section when you have changed the image and want a disk built from your own version.
This is the one part that needs a Linux host with podman and about 20 GB of
free disk, plus
mise, which runs every command below and
provisions the toolchain they need. mise install once, in the checkout.
macOS and Windows cannot build the disk images. bootc-image-builder mounts the
root filesystem it creates in order to populate it, which needs a real Linux
kernel. A Linux VM is fine; so is the hypervisor you are deploying to, which is
often the most convenient place.
podman --version # 4.x or newer
nproc; free -g # 2 cores and 4 GB are enough to buildThe Go toolchain is not required: the agent is compiled inside the container build.
Build the OS image
mise run imageThis produces an ordinary OCI image. Inspect it like any other:
podman run --rm localhost/corium:dev k0s version
podman run --rm localhost/corium:dev corium-agent versionThe build ends with bootc container lint. If it reports warnings, read them —
they catch real problems, such as content written to /var that will not
survive an upgrade.
To publish it:
REGISTRY=ghcr.io/you IMAGE_TAG=v0.1.0 mise run pushTurn it into a disk
Pick the artefact that matches where the node will run:
| Command | Produces | Use it for |
|---|---|---|
mise run artefact-qcow2 | output/qcow2/disk.qcow2 | Proxmox, KVM, libvirt |
mise run artefact-raw | output/image/disk.raw | Bare metal, most cloud import paths |
mise run artefact-anaconda-iso | output/bootiso/install.iso | Bare-metal installs |
mise run artefacts | all three |
Each takes several minutes and needs sudo, because the builder runs
privileged. From here, pick up at
step 2 with
the disk you just built.