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.
In this tutorial, we will:
Configure DNS records for the image server on saas.cloudcix.com
Set up an LXD image server
Copy the LXD host’s client certificate to the image server
Add the LXD host to the image server’s trust list
Configure the LXD host to use the remote image server
Verify the configuration
Before setting up the LXD image server, you need to create DNS records so that the server is accessible via a hostname.
Navigate to the CloudCIX SaaS portal at https://saas.cloudcix.com
Go to the IaaS App
Access the Forward DNS page: https://saas.cloudcix.com/iaas/transaction/forward_dns/
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.
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.
Now that the DNS records are set up, we can configure the LXD image server.
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 using snap:
snap install 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 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.
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).
Every LXD host uses a client certificate for authentication. We need to copy this certificate to the image server.
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).
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).
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.
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) |
+--------+--------------+-------------+--------------+-------------------------------+-------------------------------+
Now that the image server trusts the LXD host, we need to configure the LXD host to use the remote image server.
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.
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 |
+----------------------+---------------------------------------------------+---------------+-------------+--------+--------+--------+
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
Now that your image server is set up, you can create custom VM images using distrobuilder.
On the image server, install distrobuilder and required dependencies:
snap install distrobuilder --classic
sudo apt install -y btrfs-progs dosfstools qemu-kvm
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
Run distrobuilder to build your custom VM image:
sudo distrobuilder build-incus /root/distrobuilder/ubuntu_test1.yaml /root/distrobuilder/test1/ --vm
This process will:
Download the base Ubuntu system
Install the specified packages
Configure the system according to your YAML file
Create a bootable VM image
Note
The build process can take a few minutes depending on your system resources and network speed.
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.
This section describes how to import existing VM images (RAW or qcow2 format) into LXD using the official LXD migration tool.
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)
Make sure your VM image exists:
ls -lh /root/vm-image
Note
Supported formats: RAW or qcow2
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
chmod u+x ./bin.linux.lxd-migrate.x86_64
Execute the migration tool:
./bin.linux.lxd-migrate.x86_64
Follow the prompts:
Target: The local LXD server is the target [default=yes] → press Enter
Instance type: Container (1) or Virtual Machine (2) → type 2
Name: Name of the new instance → e.g., my-new-image-instance
Source: Please provide the path to the block device or disk image file → /root/vm-image
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.
Example output:
Transferring instance: 391.20MB (78.24MB/s)
Note
After completion, the VM is created in the STOPPED state.
Once the VM instance is ready, publish it as a reusable image:
lxc publish my-new-image --public --alias my-new-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.
Launch a new VM instance from the imported image:
lxc launch my-new-image new-vm-instance