> ## Documentation Index
> Fetch the complete documentation index at: https://partner.celum.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy the appliance from a qcow2

> Run the Celum appliance on Proxmox VE or libvirt/KVM from its qcow2 disk image, configured with cloud-init.

The Celum appliance ships as a vSphere OVA and, for KVM hypervisors, as a qcow2 disk image. Both carry the same system: Harbor, Authentik, OpenBao, PowerDNS, Omni and Celum, with every container image already inside, so the first boot needs no internet access.

On vSphere, the OVA asks its questions in the deploy wizard. A KVM hypervisor has no such form, so the qcow2 takes its whole configuration from **cloud-init user-data** that writes one file: `/etc/celum-appliance/appliance.yaml`.

> [!NOTE]
>
> This guide covers Proxmox VE and libvirt/KVM. On vSphere, use [Deploy on vSphere (OVA)](https://partner.celum.io/appliance/deploy-ova.md) instead.

## Before you start

* **Host resources:** 8 vCPU, 16 GB of RAM and room for a 120 GB disk. The disk is thin, so it takes only the space that is written.

* **A static IPv4 address**, with its gateway and your DNS servers. DHCP works, but Omni needs a fixed address: Talos nodes dial an endpoint that cannot follow a lease.

* **A domain for the appliance**, for example `celum.example.com`. The appliance serves the names below, and every one of them must resolve to its address. A wildcard record such as `*.celum.example.com` covers them all.

  `app` · `api` · `harbor` · `auth` · `omni` · `vault` · `pdns` · `pdns-api` · `home`

* **An SSH public key** for root (`ssh-ed25519`, or `ssh-rsa` of at least 2048 bits). The appliance allows key login only. Without a key, only the hypervisor console reaches it.

* **Network access** to the appliance on these ports:

| Port | Used by |
| - | - |
| 22/tcp | SSH |
| 80/tcp, 443/tcp | every web interface and the API |
| 8090/tcp, 8100/tcp, 50180/udp | Omni: SideroLink API, Kubernetes proxy, WireGuard |
| 53/tcp, 53/udp | the appliance's DNS server, when DNS is on. It answers anyone who reaches it, so restrict it on untrusted networks. |

## Deploy

**1. Download and check the image**

The image comes as `celum-appliance-VERSION.qcow2`; see [Where do I start?](https://partner.celum.io/appliance/get-celum-image.md) The email that brings it gives the release and the file's SHA-256 checksum. Set both as variables, then check the file before you import it:

```bash Linux theme={null}
VERSION=1.300.0   # the release you received
CHECKSUM=...      # its SHA-256 checksum, from the email
echo "$CHECKSUM  celum-appliance-$VERSION.qcow2" | sha256sum -c
```

```bash macOS theme={null}
VERSION=1.300.0   # the release you received
CHECKSUM=...      # its SHA-256 checksum, from the email
echo "$CHECKSUM  celum-appliance-$VERSION.qcow2" | shasum -a 256 -c
```

A good file prints `OK`. Anything else means the file is damaged or is not the one the email describes: download it again, and do not import it.

**2. Write the user-data**

Save this as `celum-user-data.yaml` and change the domain, network, email and SSH key. Everything under `content:` is the appliance's configuration file.

```yaml celum-user-data.yaml theme={null}
#cloud-config
write_files:
  - path: /etc/celum-appliance/appliance.yaml
    owner: root:root
    permissions: "0600"
    content: |
      # Required. Every name the appliance serves is derived from it.
      domain: celum.example.com

      # Leave the whole block out for DHCP. Omni needs a static address.
      network:
        address: 10.20.30.40/24
        gateway: 10.20.30.1
        nameservers: [10.20.30.2]
        searchDomains: [example.com]

      admin:
        email: ops@example.com      # required: the first administrator
        # password: ""              # blank: generated
        sshKeys:                    # root; also seal the backup escrow bundle
          - ssh-ed25519 AAAA... ops@laptop

      tls:
        mode: selfsigned            # selfsigned | byo | acme-dns01

      components:
        home:
          enabled: true
          username: admin           # password: at least 12 characters; blank: generated
        harbor:    { enabled: true }
        authentik: { enabled: true }
        vault:     { enabled: true }
        dns:       { enabled: true }
        omni:
          enabled: true
          # eula: { name: "Jane Doe" }             # accept Sidero's EULA at first start
          # wireguardAddress: 203.0.113.10:50180   # when nodes reach it through NAT
        celum:
          enabled: true
          iamEnabled: false         # turn on once you have signed in
          airGap: "off"             # off | strict
```

* **Only `domain` and `admin.email` are required.** Every component is on by default.
* **Put the SSH keys in `admin.sshKeys`**, not in cloud-init's own `users` or `ssh_authorized_keys`: the appliance authorises them for root itself.
* **Leave passwords blank** to have them generated. They are printed at the end of the first boot.
* **The appliance rejects unknown keys**, so a typo stops the first boot with an error that names the key, rather than being silently ignored.
* **Omni** is under the Business Source License: production use needs an agreement with Sidero Labs. Set `omni.enabled: false` if you do not have one.
* **Your own certificate or CA, ACME, update credentials, backups, the Image Library and other names** are optional blocks of the same file. See [Optional settings](#optional-settings), and the [appliance.yaml reference](https://partner.celum.io/appliance/appliance-yaml.md) for every key.

> [!WARNING]
>
> Anything you put in the user-data stays readable wherever it is stored: the Proxmox snippet, the seed ISO, and `/var/lib/cloud/` on the appliance. Prefer generated passwords, and remove the user-data after the first boot (see below).

**3. Create the VM**

**Proxmox VE**

1. Enable the **Snippets** content type on a storage (**Datacenter → Storage → local → Edit → Content**), and copy the user-data to it, for example `/var/lib/vz/snippets/celum-user-data.yaml`.
2. Create the VM, import the disk and attach the cloud-init drive:

```bash theme={null}
VERSION=1.300.0   # the release you received
ID=9100           # a free VM ID
qm create $ID --name celum --ostype l26 --cores 8 --memory 16384 \
  --scsihw virtio-scsi-pci --net0 virtio,bridge=vmbr0
qm disk import $ID celum-appliance-$VERSION.qcow2 local-lvm
qm set $ID --scsi0 local-lvm:vm-$ID-disk-0 --boot order=scsi0
qm set $ID --ide2 local-lvm:cloudinit \
  --cicustom "user=local:snippets/celum-user-data.yaml"
qm start $ID
```

`qm disk import` prints the name of the volume it created. Use that name in `--scsi0` if it differs, for example on directory storage.

> [!NOTE]
>
> Proxmox's **IP Config** for cloud-init has no effect: the appliance manages its own network from `network:` in the user-data. The image has no QEMU guest agent, so Proxmox does not show its address: read it on the console or with `celum-appliance status`.

**libvirt / KVM**

Copy the image into libvirt's storage, then import it with the user-data:

```bash theme={null}
VERSION=1.300.0   # the release you received
sudo cp celum-appliance-$VERSION.qcow2 /var/lib/libvirt/images/celum.qcow2
sudo virt-install --name celum --vcpus 8 --memory 16384 \
  --os-variant ubuntu24.04 --import \
  --disk path=/var/lib/libvirt/images/celum.qcow2,bus=virtio \
  --network bridge=br0,model=virtio \
  --cloud-init user-data=celum-user-data.yaml \
  --noautoconsole
```

`--cloud-init` needs virt-install 3.0 or later. With an older one, or with another tool, build a NoCloud seed ISO and attach it as a CD-ROM:

```bash theme={null}
cloud-localds celum-seed.iso celum-user-data.yaml
# then add to virt-install: --disk path=celum-seed.iso,device=cdrom
```

If your osinfo database does not know `ubuntu24.04`, use `ubuntu22.04`.

**4. Watch the first boot**

The appliance configures itself in stages, as a systemd unit. Follow it on the console or over SSH:

```bash theme={null}
celum-appliance status                       # addresses, logins, stages
journalctl -u celum-appliance-bootstrap -f   # the bootstrap as it runs
```

Each stage records when it is done. If one fails, fix the cause and run `celum-appliance bootstrap` again: it resumes where it stopped.

When it finishes, the generated credentials are printed on the console and written to `/root/celum-appliance-credentials.txt`.

**5. Remove the user-data**

After the first boot, `/etc/celum-appliance/appliance.yaml` is the configuration, and the user-data has done its job. Detach it, so the secrets leave the hypervisor and a later change to it cannot rewrite the file:

* **Proxmox:** `qm set $ID --delete ide2`, then delete the snippet.
* **libvirt:** detach the seed ISO if one is still attached, and delete it.

## Sign in

| Address | Sign in as |
| - | - |
| `https://home.DOMAIN` | the start page account: `admin` and its password |
| `https://app.DOMAIN` (Celum) | through Authentik, with the administrator email and password |
| `https://harbor.DOMAIN` | `admin`, with the Harbor password |
| `https://omni.DOMAIN`, `https://vault.DOMAIN`, `https://pdns.DOMAIN` | through Authentik, as a member of `celum-admins` |

Every password is in `/root/celum-appliance-credentials.txt`, and `celum-appliance status` prints them again.

* **Trust the appliance CA.** With `tls.mode: selfsigned`, every certificate is signed by a CA the appliance creates. Copy `/etc/celum-appliance/tls/ca.crt` and add it to your trust store. Until a client trusts it, it fails before HTTP: `curl` reports `http=000`, which looks like a network fault.
* **Turn on IAM** once you have signed in to Celum and confirmed you are in `celum-admins`. Set `iamEnabled: true` under `components.celum` in `/etc/celum-appliance/appliance.yaml`, then render the files again and restart Celum:
  ```bash theme={null}
  celum-appliance bootstrap --only 50 --force
  celum-appliance bootstrap --only 80 --force
  ```
* **Talos nodes managed by Omni** must trust the appliance CA too, through a `TrustedRootsConfig` document.

## Optional settings

Each block below goes into the same `appliance.yaml`, under `content:` in the user-data. Leave a block out to keep its default.

> [!NOTE]
>
> Certificates and keys go into the file **as PEM**, indented under `|`. The base64 that the OVA's form asks for is refused here, with `tls.cert: not PEM`.

### Your own certificate

```yaml theme={null}
tls:
  mode: byo
  cert: |
    -----BEGIN CERTIFICATE-----
    ...the server certificate first, then its chain
    -----END CERTIFICATE-----
  key: |
    -----BEGIN PRIVATE KEY-----
    ...
    -----END PRIVATE KEY-----
```

The certificate must cover every name the appliance serves; a wildcard such as `*.celum.example.com` covers them all. The first boot checks it before anything starts. If it misses any name other than `home`, the boot stops and lists the missing names.

### Your own CA

```yaml theme={null}
tls:
  mode: selfsigned
  ca:
    cert: |
      -----BEGIN CERTIFICATE-----
      ...the signing CA first, then any chain above it
      -----END CERTIFICATE-----
    key: |
      -----BEGIN PRIVATE KEY-----
      ...
      -----END PRIVATE KEY-----
```

The CA signs every certificate the appliance serves, so clients that trust it keep trusting the appliance after a redeploy. To create one, see [Use your own CA](https://partner.celum.io/appliance/deploy-ova.md#use-your-own-ca).

### Certificates from ACME (DNS-01)

```yaml theme={null}
tls:
  mode: acme-dns01
  acme:
    email: ops@example.com
    provider: cloudflare        # a lego provider code
    credentials:                # the provider's own environment variables
      CF_DNS_API_TOKEN: "..."
```

* **`credentials` is a map**, with one variable per line, not a comma-separated list.
* **Omni is switched off in this mode.** Omni serves its own certificate, and ACME certificates stay inside Traefik.

### Updates

```yaml theme={null}
updates:
  username: "robot$..."         # the robot account from your licence
  password: "..."
```

The registry defaults to `registry.celum.io`. You can also enter the account later, on the start page.

### Off-box backups

```yaml theme={null}
backup:
  s3:
    endpoint: https://s3.example.com   # scheme and host only
    bucket: celum-backups              # must exist already
    accessKey: "..."
    secretKey: "..."
```

Set all four or none. Snapshots are written under `celum-appliance/DOMAIN/` unless you set `prefix`. For an endpoint with a private CA, add its PEM as `ca: |`.

### Image Library source

```yaml theme={null}
components:
  celum:
    imageLibrary:
      endpoint: https://s3.example.com
      bucket: celum-images
      accessKey: "..."
      secretKey: "..."
```

This is the S3 bucket that Celum's Image Library imports guest images from. Set all four or none. Supervisor nodes download from the endpoint directly, so it must be reachable from them and serve a certificate they trust.

### Other names

```yaml theme={null}
hostnames:
  harbor: registry              # registry.celum.example.com
  celum: celum.example.org      # a name with a dot is used as it is
```

The keys are `home`, `celum` (the web interface, `app` by default), `api`, `harbor`, `auth`, `omni`, `vault`, `pdns-api` and `pdns`.

### More DNS zones

```yaml theme={null}
components:
  dns:
    zones: [apps.example.com]
```

These zones are created empty, for external-dns in tenant clusters to fill. The appliance's own domain is always served.

### Several network interfaces

```yaml theme={null}
network:
  interface: ens18
```

With more than one NIC, name the one the appliance serves on. The first boot stops if it cannot tell them apart.

## Change the configuration later

Edit `/etc/celum-appliance/appliance.yaml` on the appliance, then check it: `celum-appliance config` shows the configuration as the appliance reads it, with warnings, or the error that would stop it.

A plain `celum-appliance bootstrap` skips every stage that has already finished, so after the first boot it applies nothing. Render the files again from `appliance.yaml`, then re-run the stage of the component you changed:

```bash theme={null}
celum-appliance bootstrap --only 50 --force   # render every file from appliance.yaml
celum-appliance bootstrap --only 80 --force   # restart the component, here Celum
```

| Component | Stage |
| - | - |
| Harbor | `60` |
| DNS | `62` |
| OpenBao | `65` |
| Authentik | `70` |
| Omni | `75` |
| Celum, including IAM, air gap and the Image Library | `80` |

This does not cover `network` and `tls`: earlier stages, `20` and `40`, apply those.

To enlarge the disk, resize it in the hypervisor. The filesystem grows at the next boot.

## Troubleshooting

> **The bootstrap says there is no appliance.yaml and no OVF properties**
>
> The user-data never reached the VM, so the appliance found no configuration. Check that the cloud-init drive or seed ISO is attached, then read `cloud-init status --long` and `/var/log/cloud-init.log`. Fix it, reboot, and the bootstrap starts again.

> **Nothing resolves, or every page refuses**
>
> Check DNS first: all the names under **Before you start** must resolve to the appliance. This is by far the most common cause.

> **The appliance has no address, or SSH refuses**
>
> Open the console and run `celum-appliance status`. Without a `network:` block, the appliance asks DHCP on its first Ethernet interface. With one, `address` (as CIDR) and `gateway` must both be set. SSH accepts only the keys in `admin.sshKeys`.

> **I am locked out of Celum**
>
> IAM was enforced before your group membership was in place. Set `iamEnabled: false` under `components.celum` in `/etc/celum-appliance/appliance.yaml`, then run `celum-appliance bootstrap --only 50 --force` and `celum-appliance bootstrap --only 80 --force`.

> **Talos nodes do not join Omni**
>
> Omni needs a static address. With a self-signed certificate, the nodes must trust the appliance CA. Nodes behind NAT need `omni.wireguardAddress` set to an address they can reach. Omni also refuses every call until its EULA is accepted: set `omni.eula.name`, or accept it in the Omni interface.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.