# Quick start<no value>

<!-- Generated by website/sync-docs.py from docs/quickstart.md. Edit that file, not this one. -->

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:

```bash
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-data
```

The 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](/docs/guides/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`](https://oras.land), `curl`, or a browser.
  Any of the three works from macOS, Windows or Linux, and none needs `sudo`.

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](#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](https://github.com/Corium-OS/Corium/releases/latest)** — 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](/docs/install/downloads/), which also covers how to check that what you
received is what was published:

```bash
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.com
```

The 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](#build-your-own-image).

---

## 2. Write a node configuration

A complete single-node cluster:

```yaml
#cloud-config
corium:
  role: single

users:
  - name: core
    groups: [wheel]
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... you@example.com
```

`role` 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:

```bash
go run ./cmd/corium-agent validate node.yaml
```

Validation 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/`](https://github.com/Corium-OS/Corium/blob/main/docs/examples) for workers, add-ons, a custom CNI, and HA.

---

## 3. Boot it

### Proxmox

```bash
# 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 140
```

Installing 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:

```bash
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-data
```

### Without 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:

```yaml
role: worker
cluster:
  name: edge
join:
  tokenFrom:
    url: https://secrets.example.com/corium/worker-token
```

---

## 4. Check it worked

```bash
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 -A
```

A 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:

```bash
ssh core@192.168.0.190 sudo k0s kubeconfig admin > kubeconfig
KUBECONFIG=./kubeconfig kubectl get nodes
```

The 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:

```bash
mise run build          # produces bin/cctl
```

Make 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:

```bash
cctl pki init
cctl pki issue --role admin
```

`pki 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:

```yaml
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:

```bash
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 > kubeconfig
```

The 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](/docs/reference/cli/) covers every command, the three roles, and what the
API deliberately will not do.

---

## Where to go next

- **Add workers** — [`examples/worker.yaml`](https://github.com/Corium-OS/Corium/blob/main/docs/examples/worker.yaml). Mint a token
  on the controller with `k0s token create --role=worker --expiry=1h`.
- **Highly available control plane** —
  [`examples/ha-controller-first.yaml`](https://github.com/Corium-OS/Corium/blob/main/docs/examples/ha-controller-first.yaml), or
  `deploy/proxmox/create-ha-cluster.sh` to 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](/docs/guides/cilium/), start to finish.
- **Anything Corium does not model** — `k0s.patch` is applied verbatim to the
  rendered `k0s.yaml`, so every k0s setting stays reachable.
- **Every field in detail** — the [configuration reference](/docs/reference/configuration/).
- **What is and is not supported** — [feature support](/docs/reference/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](/docs/guides/upgrades/) covers doing this to a cluster without losing
quorum; the short version:

```bash
sudo bootc upgrade --apply     # reboots into the new version
sudo bootc rollback            # if it went badly
```

Drain 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](https://mise.jdx.dev), 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.

```bash
podman --version     # 4.x or newer
nproc; free -g       # 2 cores and 4 GB are enough to build
```

The Go toolchain is **not** required: the agent is compiled inside the container
build.

### Build the OS image

```bash
mise run image
```

This produces an ordinary OCI image. Inspect it like any other:

```bash
podman run --rm localhost/corium:dev k0s version
podman run --rm localhost/corium:dev corium-agent version
```

The 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:

```bash
REGISTRY=ghcr.io/you IMAGE_TAG=v0.1.0 mise run push
```

### Turn 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](#2-write-a-node-configuration) with
the disk you just built.
