Previous topic

Using userdata in CloudCIX Compute Instances

Next topic

Appendices

Adding a Remote LXD Image Server

This tutorial describes how to add a remote LXD image server to an LXD host, enabling the host to pull images from a custom image server.

Overview

In this tutorial, we will:

  1. Configure DNS records for the image server on saas.cloudcix.com

  2. Set up an LXD image server

  3. Copy the LXD host’s client certificate to the image server

  4. Add the LXD host to the image server’s trust list

  5. Configure the LXD host to use the remote image server

  6. Verify the configuration

Step 1: Configure DNS Records

Before setting up the LXD image server, you need to create DNS records so that the server is accessible via a hostname.

Access the Forward DNS Interface

  1. Navigate to the CloudCIX SaaS portal at https://saas.cloudcix.com

  2. Go to the IaaS App

  3. Access the Forward DNS page: https://saas.cloudcix.com/iaas/transaction/forward_dns/

Add DNS Records

Before setting up the LXD image server, DNS records must be created so that the server is reachable via a hostname.

Important

The CloudCIX IaaS App (Rage4 DNS) can only manage domains that are explicitly authorised for the CloudCIX Rage4 account.

Many customers manage DNS with their own domain registrar (e.g. joker.com, GoDaddy, Namecheap). In this case, DNS records must be created with the customer’s registrar, not in CloudCIX.

Both A (IPv4) and AAAA (IPv6) records are required, regardless of where DNS is managed.

Add IPv4 Address (A Record)

Fill in the following fields:

  • Type: Select A from the dropdown

  • Name: Enter the full record including domain: images-boole.cloudcix.com

  • Content: Enter the IPv4 address of your image server (e.g., 217.74.60.51)

  • TTL: Enter 3600 (Time To Live in seconds)

  • Priority: Enter 1

  • Failover: Select NO

  • Georegion: Select Global

Click the Save button to create the record.

Add IPv6 Address (AAAA Record)

Repeat the process with the following fields:

  • Type: Select AAAA from the dropdown

  • Name: Enter the full record including domain: images-boole.cloudcix.com

  • Content: Enter the IPv6 address of your image server (e.g., 2a02:2078:10:1e45::12b)

  • TTL: Enter 3600

  • Priority: Enter 1

  • Failover: Select NO

  • Georegion: Select Global

Click the Save button to create the record.

Verify DNS Resolution

After creating the DNS records, verify that they are resolving correctly:

# Check IPv4 resolution
dig images-boole.cloudcix.com A

# Check IPv6 resolution
dig images-boole.cloudcix.com AAAA

Note

DNS propagation can take a few minutes. Wait until the records are resolving correctly before proceeding to the next step.

Step 2: Set Up the LXD Image Server

Now that the DNS records are set up, we can configure the LXD image server.

Set the Hostname

First, set the hostname of the image server to match the DNS record:

hostnamectl set-hostname images-boole.cloudcix.com

Verify the hostname has been set correctly:

hostname

This should return images-boole.cloudcix.com.

Install LXD

Install LXD using snap:

snap install lxd

Initialise LXD

Run the LXD initialization wizard:

lxd init

LXD will ask about storage pools, network configuration, and other options. When configuring the storage pool, ensure you allocate enough space to host multiple images.

Important

  • For an image server, allocate as much available storage as possible

  • Each OS image can range from 200MB to several GB in size

  • Consider future growth when sizing this storage pool

Configure HTTPS Access

Configure LXD to listen on HTTPS (port 443 or 8443):

lxc config set core.https_address :8443

Note

If you want to use port 443 instead of 8443, replace :8443 with :443. Note that using port 443 may require additional configuration if you have other services running on that port.

Verify the Configuration

Check that LXD is listening on the configured port:

lxc config get core.https_address

This should return :8443 (or :443 if you chose that port).

Step 3: Copy the Client Certificate

Every LXD host uses a client certificate for authentication. We need to copy this certificate to the image server.

On the LXD Host

Navigate to the LXD configuration directory:

cd ~/snap/lxd/common/config/

Display the client certificate:

cat client.crt

You should see output similar to:

-----BEGIN CERTIFICATE-----
MIIBrzCCATWgAwIBAgIQfK9mN2Pq8YvRzL3HxTnBgjAKBggqhkjOPQQDAzAhMQww
CgYDVQQKEwNMWEQxETAPBgNVBAMMCHJvb3RATFhEMB4XDTI2MDExMjA5MjQzNVoX
DTM2MDExMDA5MjQzNVowITEMMAoGA1UEChMDTFhEMREwDwYDVQQDDAhyb290QExY
RDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNxQR7jK4mPvZ8WnHfYt2LqX9TcPmJeV
s3GyWkN4pRvD8HzLmFqJ6TwUBX2hKrYvN5PqLz8RtWnEj4YfGxM3ZpKuH9VrQa7N
eP2fLxDj5KmRtZ8vBwXcYnH4Tp9JsLKWRqM1MDMwDgYDVR0PAQH/BAQDAgWgMBMG
A1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCgYIKoZIzj0EAwMDaAAw
ZQIxALmTy4vHnX8PqR2Jw6FbKt9sU3hNxYpQrL5eWmK7DjN8fxVzH3MpYtRkE6Qw
Vn2xHgIwPkN7R4Zq8LmWvTxJY3KrHd9F2nQs6PtVmXhLwJ9YcGn5RpMtK8UvE2xZ
qDfH4Lpm
-----END CERTIFICATE-----

Copy the entire certificate content (including the BEGIN and END lines).

On the Image Server

Create a new file to store the certificate:

nano lxd_node001.crt

Paste the certificate content you copied from the LXD host:

-----BEGIN CERTIFICATE-----
MIIBrzCCATWgAwIBAgIQfK9mN2Pq8YvRzL3HxTnBgjAKBggqhkjOPQQDAzAhMQww
CgYDVQQKEwNMWEQxETAPBgNVBAMMCHJvb3RATFhEMB4XDTI2MDExMjA5MjQzNVoX
DTM2MDExMDA5MjQzNVowITEMMAoGA1UEChMDTFhEMREwDwYDVQQDDAhyb290QExY
RDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNxQR7jK4mPvZ8WnHfYt2LqX9TcPmJeV
s3GyWkN4pRvD8HzLmFqJ6TwUBX2hKrYvN5PqLz8RtWnEj4YfGxM3ZpKuH9VrQa7N
eP2fLxDj5KmRtZ8vBwXcYnH4Tp9JsLKWRqM1MDMwDgYDVR0PAQH/BAQDAgWgMBMG
A1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCgYIKoZIzj0EAwMDaAAw
ZQIxALmTy4vHnX8PqR2Jw6FbKt9sU3hNxYpQrL5eWmK7DjN8fxVzH3MpYtRkE6Qw
Vn2xHgIwPkN7R4Zq8LmWvTxJY3KrHd9F2nQs6PtVmXhLwJ9YcGn5RpMtK8UvE2xZ
qDfH4Lpm
-----END CERTIFICATE-----

Save and close the file (in nano: Ctrl+X, then Y, then Enter).

Step 4: Add the LXD Host to the Trust List

On the image server, add the LXD host to the trust list using the certificate file:

lxc config trust add lxd_node001.crt

Note, you will need to do this for each LXD host that you want to allow access to the image server.

Verify the Trust Configuration

Confirm that the certificate has been added successfully:

lxc config trust ls

Expected output:

+--------+--------------+-------------+--------------+-------------------------------+-------------------------------+
|  TYPE  |     NAME     | COMMON NAME | FINGERPRINT  |          ISSUE DATE           |          EXPIRY DATE          |
+--------+--------------+-------------+--------------+-------------------------------+-------------------------------+
| client | lxd_node001.crt | root@LXD    | 7f4a9c92b6e1 | Jan 12, 2026 at 9:24am (UTC)  | Jan 10, 2036 at 9:24am (UTC)  |
+--------+--------------+-------------+--------------+-------------------------------+-------------------------------+

Step 5: Add the Remote Image Server to the LXD Host

Now that the image server trusts the LXD host, we need to configure the LXD host to use the remote image server.

On the LXD Host

Add the remote image server:

lxc remote add images-boole https://images-boole.cloudcix.com:443 --public

You will be prompted to verify the certificate fingerprint:

Certificate fingerprint: a2f5c7b93e4d6f18a72c59b8e3e1d04a6c8b2e8f7d5a3b1c4e6f870b2d4c6e8a
ok (y/n/[fingerprint])? yes

Type yes and press Enter to confirm.

Step 6: Verify the Configuration

Confirm that the remote has been added successfully:

lxc remote ls

You should see images-boole in the list of remotes:

+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
|         NAME         |                        URL                        |   PROTOCOL    |  AUTH TYPE  | PUBLIC | STATIC | GLOBAL |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
| images               | https://images.lxd.canonical.com                  | simplestreams | none        | YES    | YES    | NO     |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
| images-boole         | https://images-boole.cloudcix.com:443            | lxd           | tls         | YES     | NO     | NO     |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
| local (current)      | unix://                                           | lxd           | file access | NO     | YES    | NO     |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
| ubuntu               | https://cloud-images.ubuntu.com/releases/         | simplestreams | none        | YES    | YES    | NO     |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
| ubuntu-daily         | https://cloud-images.ubuntu.com/daily/            | simplestreams | none        | YES    | YES    | NO     |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
| ubuntu-minimal       | https://cloud-images.ubuntu.com/minimal/releases/ | simplestreams | none        | YES    | YES    | NO     |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
| ubuntu-minimal-daily | https://cloud-images.ubuntu.com/minimal/daily/    | simplestreams | none        | YES    | YES    | NO     |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+

Using the Remote Image Server

Now that the remote image server is configured, you can pull images from it using:

lxc image list images-boole:

To launch a container from an image on the remote server:

lxc launch images-boole:<image-name> <container-name>

For example:

lxc launch images-boole:ubuntu-22.04 my-container

Building Custom Images with Distrobuilder

Now that your image server is set up, you can create custom VM images using distrobuilder.

Install Distrobuilder and Dependencies

On the image server, install distrobuilder and required dependencies:

snap install distrobuilder --classic
sudo apt install -y btrfs-progs dosfstools qemu-kvm

Create a Configuration File

Create a directory for the image you are going to build:

mkdir -p /root/distrobuilder/test1

Create a YAML configuration file that defines your custom image. Here’s an example for Ubuntu 20.04 (Focal):

nano /root/distrobuilder/ubuntu_test1.yaml

Example configuration:

image:
  name: ubuntu-focal-x86_64
  distribution: ubuntu
  release: focal
  architecture: x86_64
  description: Ubuntu 20.04 LTS (Incus/LXD VM)

source:
  downloader: debootstrap
  same_as: focal
  url: http://archive.ubuntu.com/ubuntu
  keyserver: keyserver.ubuntu.com
  keys:
    - 0x790BC7277767219C42C86F933B4FE6ACC0B21F32

targets:
  lxd:
    vm:
      enabled: true
    container:
      enabled: false

packages:
  manager: apt
  update: true
  cleanup: true
  sets:
    - action: install
      types:
        - vm
      packages:
        - systemd
        - systemd-sysv
        - linux-virtual
        - grub-efi-amd64-signed
        - shim-signed
        - cloud-init
        - openssh-server
        - sudo
        - vim
        - netplan.io
        - acpid

files:
  - path: /etc/hostname
    generator: hostname

  - path: /etc/hosts
    generator: hosts

  - path: /etc/machine-id
    generator: dump

  - name: meta-data
    generator: cloud-init
    types:
      - vm

  - name: user-data
    generator: cloud-init
    types:
      - vm

  - name: network-config
    generator: cloud-init
    types:
      - vm

  - name: vendor-data
    generator: cloud-init
    types:
      - vm

  - name: ext4
    generator: fstab
    types:
      - vm

  - name: incus-agent
    generator: incus-agent
    types:
      - vm

  - path: /etc/default/grub
    generator: dump
    types:
      - vm
    content: |-
      GRUB_DEFAULT=0
      GRUB_TIMEOUT=0
      GRUB_DISTRIBUTOR=Ubuntu
      GRUB_CMDLINE_LINUX="console=ttyS0 console=tty1"
      GRUB_TERMINAL=console

actions:
  - trigger: post-packages
    types:
      - vm
    action: |-
      #!/bin/sh
      set -eux

      useradd -m -s /bin/bash -G sudo ubuntu
      echo "ubuntu ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-ubuntu
      chmod 0440 /etc/sudoers.d/90-ubuntu

      update-initramfs -c -k all

      systemctl enable systemd-networkd
      systemctl enable cloud-init
      systemctl enable ssh

  - trigger: post-files
    types:
      - vm
    action: |-
      #!/bin/sh
      set -eux

      grub-install \
        --target=x86_64-efi \
        --efi-directory=/boot/efi \
        --no-nvram \
        --removable

      update-grub

mappings:
  architecture_map: debian

Build the Image

Run distrobuilder to build your custom VM image:

sudo distrobuilder build-incus /root/distrobuilder/ubuntu_test1.yaml /root/distrobuilder/test1/ --vm

This process will:

  1. Download the base Ubuntu system

  2. Install the specified packages

  3. Configure the system according to your YAML file

  4. Create a bootable VM image

Note

The build process can take a few minutes depending on your system resources and network speed.

Import the Image to LXD

After the build completes, import the image into your LXD image server:

lxc image import /root/distrobuilder/test1/incus.tar.xz /root/distrobuilder/test1/disk.qcow2 --alias ubuntu-focal-custom --public

LXD needs to import both the metadata and the disk image, so ensure you provide both the incus.tar.xz and disk.qcow2 files. Note, when you are importing images to the image server, ensure that you use the --public flag if you want the images to be accessible to all LXD hosts that trust the server.

List your images to verify:

lxc image list

Or from a host that has the image server added as a remote:

lxc image list images-boole:

The custom image is now available on your image server and can be used by any LXD host that has added this server as a remote.

Importing VM Images with the LXD Migration Tool

This section describes how to import existing VM images (RAW or qcow2 format) into LXD using the official LXD migration tool.

Prerequisites

This guide assumes:

  • LXD version: 5.21.4 LTS (or compatible version)

  • Image format: RAW or qcow2

  • Image path: /root/vm-image (replace with your actual file)

Step 1: Prepare Your Image

Make sure your VM image exists:

ls -lh /root/vm-image

Note

Supported formats: RAW or qcow2

Step 2: Import the Image Using the LXD Migration Tool

Download the Migration Tool

Download the migration tool for your LXD version:

wget https://github.com/canonical/lxd/releases/download/lxd-5.21.4/bin.linux.lxd-migrate.x86_64

Make it Executable

chmod u+x ./bin.linux.lxd-migrate.x86_64

Run the Migration Tool

Execute the migration tool:

./bin.linux.lxd-migrate.x86_64

Follow the prompts:

  1. Target: The local LXD server is the target [default=yes] → press Enter

  2. Instance type: Container (1) or Virtual Machine (2) → type 2

  3. Name: Name of the new instance → e.g., my-new-image-instance

  4. Source: Please provide the path to the block device or disk image file/root/vm-image

  5. UEFI Secure Boot: [default=no] → type yes if your VM supports it

You’ll see a summary:

Instance to be created:
  Name: my-new-image
  Project: default
  Type: virtual-machine
  Source: /root/vm-image
  Profiles:
  - default

Additional options (optional) at the next prompt:

1) Begin the migration with the above configuration
2) Override profile list
3) Set additional configuration options
4) Change instance storage pool or volume size
5) Change instance network

Choose 1 to start the migration.

Wait for Migration

Example output:

Transferring instance: 391.20MB (78.24MB/s)

Note

After completion, the VM is created in the STOPPED state.

Step 3: Publish the Image

Once the VM instance is ready, publish it as a reusable image:

lxc publish my-new-image --public --alias my-new-image

Step 4: Verify the Image

List all images:

lxc image list --format yaml

Example output:

- aliases:
  - name: my-new-image
    description: ""
  architecture: x86_64
  cached: false
  public: true
  filename: ""
  fingerprint: 9f78447cb20cae720e543882aea578aec6709937dffd99a23a733a34b8100b93
  size: 920175172
  auto_update: false
  type: virtual-machine
  created_at: 2026-01-15T14:20:41.868917937Z
  expires_at: 0001-01-01T00:00:00Z
  last_used_at: 2026-01-15T14:30:21.915101512Z
  uploaded_at: 2026-01-15T14:20:41.870556432Z
  properties:
    architecture: x86_64
    description: Ubuntu 22.04
    os: ubuntu
    release: jammy
  profiles:
  - default
  project: default

You should see my-new-image in the list.

Step 5: Launch a New VM from the Image

Launch a new VM instance from the imported image:

lxc launch my-new-image new-vm-instance