This page turns a spare tower or mini-PC into a single-node Kubernetes cluster. You need two USB sticks. One stick installs the operating system. The other stick tells the node what it is.

For a dedicated server that you reach through the rescue system of a provider, see rescue mode. For a VM, see Proxmox or the quick start.

A CI run tests the Proxmox page, but no CI run tests this page. Nothing in the project boots the ISO on physical hardware. If your machine does not behave as a step here says, open an issue.

Before you start

  • A 64-bit x86 machine that boots UEFI. Corium boots UEFI only.
  • A whole disk that you can lose. The installer erases its target disk. If the machine has more than one disk, disconnect the others.
  • Two USB sticks. The stick for the ISO must be 4 GB or more, because the ISO is about 2.4 GB. The stick for the seed can be any size.
  • Wired ethernet. Corium does not configure wireless.
  • 4 GB of RAM and 32 GB of disk. 8 GB of RAM is better.
  • A public SSH key.

How the install is shaped

Two separate things happen:

  1. The ISO installs the operating system. This step needs no operator. Anaconda deploys the bootc image that is inside the ISO. It shows no prompts, and you do not write a kickstart file. It never asks what the node is.
  2. cloud-init configures the node on its first real boot. cloud-init applies the corium: block and creates your login account.

A desktop PC has no metadata service for step 2. You supply the data from a NoCloud seed: a USB stick with a filesystem that has the label CIDATA. Corium looks for this datasource first, and it is the only datasource that works offline. You can also install without a seed, and that state is recoverable.


1. Write the ISO to a stick

Download corium-0.5.0-x86_64.iso and verify it. downloads gives all three download routes and the verification steps.

Run the listing command first. If dd writes to the wrong device, it destroys that device.

# linux
lsblk -o NAME,SIZE,MODEL,TRAN
sudo dd if=corium-0.5.0-x86_64.iso of=/dev/sdX bs=4M status=progress oflag=sync
# macOS
diskutil list
diskutil unmountDisk /dev/diskN
sudo dd if=corium-0.5.0-x86_64.iso of=/dev/rdiskN bs=4m

On macOS, use /dev/rdiskN and not /dev/diskN. For a write of this size, the raw device is approximately ten times faster.

A graphical writer also works, for example Rufus, balenaEtcher, or GNOME Disks. The writer must copy the image exactly as it is. In Rufus, use DD image mode, not ISO mode.

2. Write the configuration seed

A complete single-node cluster:

#cloud-config
corium:
  role: single

users:
  - name: core
    groups: [wheel]
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... you@example.com

role is the only required field. See examples for workers, add-ons, and a custom CNI. See the reference for every field. If you have a checkout of the repository, validate the file before you go to the machine. Validation works offline and reports all problems at once. mise exec supplies the pinned Go toolchain, so the command works on a machine that never installed Go:

mise exec -- go run ./cmd/corium-agent validate node.yaml

Put two files on a filesystem with the label CIDATA. cloud-init looks for this label. Nothing else about the stick is important.

# linux. /dev/sdY is the *second* stick, not the one holding the ISO.
sudo mkfs.vfat -n CIDATA /dev/sdY1
sudo mount /dev/sdY1 /mnt

sudo cp node.yaml /mnt/user-data
printf 'instance-id: corium-01\nlocal-hostname: corium-01\n' | sudo tee /mnt/meta-data
sudo umount /mnt
# macOS
diskutil eraseDisk FAT32 CIDATA MBRFormat /dev/diskN

cp node.yaml /Volumes/CIDATA/user-data
printf 'instance-id: corium-01\nlocal-hostname: corium-01\n' > /Volumes/CIDATA/meta-data

Both files must be at the root, with exactly these names. meta-data can be almost empty, but it must exist. If it does not exist, cloud-init rejects the seed.

3. Set up the firmware

Open the setup screen. Usually you press Del, F2, or F12 at power-on. Set these three values:

SettingValueWhy
Boot modeUEFI, CSM/legacy offCorium boots UEFI only
Boot orderInternal disk first, USB secondSee below
Secure BootOff, if the install does not bootSee below

Disk first, USB second is correct. An empty disk has no UEFI boot entry, so the firmware goes to the stick and installs. After the install, the disk has an entry and boots first. If the stick is first, the machine installs itself again at every reboot. The install shows no prompts, so nothing on the screen tells you.

Secure Boot is not tested on bare metal. The image includes the signed shim from Fedora, so Secure Boot can work. If the machine does not boot after the install, turn Secure Boot off.

4. Install

Connect only the ISO stick, and turn on the machine. The installer then has one less disk to look at. Anaconda reboots when it is done, after a few minutes on an SSD. Remove the stick during that reboot.

5. Configure it

Connect the seed stick and boot the machine.

cloud-init finds CIDATA and creates your user. Then corium-bootstrap reads the corium: block and starts k0s. The node is Ready approximately one minute after power-on. The console banner shows the role of the node, its cluster, and whether k0s is running.

When the node is up, you can remove the seed stick or leave it in.

6. Check it worked

ssh core@<the node's address>
sudo k0s kubectl get nodes

For the kubeconfig and the next steps, continue with the quick start. Management of the node with cctl is a separate step. The management API is off by default, and you must claim a node before it answers.


If you install without a seed

The seed is optional. Without a seed, the ISO installs a node that waits for its configuration. The node shows a pairing code on its console, and you can claim it from your own machine. The seed stick is the path that needs no operator, but it is not the only path.

After the install
corium-bootstrapRan, found nothing, wrote no marker
corium-apidServing on 7443, enrolment open, pairing code on the console
k0sNot started. A node nobody has claimed is in no cluster
AccountsNone. Corium creates no default user, and PermitRootLogin is no

The console shows the code and the fingerprint. Use the fingerprint to make sure that you claim the correct node:

  This node is not part of a cluster.
  address     192.168.0.42
  pairing     K4M2-8QT9
  fingerprint SHA256:...

  cctl enroll 192.168.0.42 --code K4M2-8QT9

Run this command from your own machine. You must already have an operator PKI. cctl shows how to make one.

cctl enroll 192.168.0.42 --code K4M2-8QT9 --config node.yaml

node.yaml is the same document as the seed in step 2, with the users: block. The users: block gives the machine its first account. The node pins your CA, builds the role, and starts as a cluster.

This is the default since ADR 11. Before ADR 11, a machine installed this way had no account and no API. The only way in was to go back to the machine with a USB stick.

The seed stick still works, and it is still the better choice when you have one. It needs no operator: you read no console and type no code. Your CA owns the node from its first boot, not only after the minute between boot and claim. Connect a CIDATA stick and reboot. The node wrote no bootstrap marker, so step 5 runs as if it were the first boot.

Know these two limits before you rely on enrolment:

  • It closes when the node bootstraps. You cannot claim a node that built its role, whatever its configuration says. This is intentional. It stops a person who reads the screen from taking over a machine that runs your workloads. As a result, you can claim a node configured by seed only during the minute that it takes to come up. After that, you must use cctl reset to take it over.
  • It needs the console. The pairing code is on a channel that an attacker on the network cannot see. All of its security comes from this. On a headless machine with no IPMI, that channel is a monitor that you must go and connect.

corium.config=<url> on the kernel command line is the third option, and the weakest. It configures the node but creates no account. The result is a cluster that you cannot log in to.


What goes wrong on a desktop that does not on a hypervisor

It installs itself again on every boot. The boot order causes this. Put the disk first and USB second, and remove the ISO stick after the install.

The node comes up with no user and no cluster. cloud-init did not find the seed. Make sure that the filesystem label is exactly CIDATA. Make sure that user-data and meta-data are both at the root of the filesystem. Make sure that user-data starts with the #cloud-config line. On the node, sudo cloud-init query --all shows which datasource cloud-init selected.

The node has an address but resolves no names. The cause is a static address without a resolver. Keep the machine on DHCP for the first boot. If the address must be static, set it in the network: block of cloud-init, and give that block its own nameserver.

Nothing on the console after the firmware. The cause is almost always the boot mode. A firmware in CSM/legacy mode finds no bootloader on a UEFI-only image. Its message makes the machine look dead.

For other problems, see troubleshooting. It lists them by symptom.